mirror of
https://github.com/gnosygnu/xowa.git
synced 2026-03-02 03:49:30 +00:00
Res: Add resources from xowa_app_windows_64_v4.5.26.1810
This commit is contained in:
155
res/bin/any/xowa/xtns/Graph/Xograph.js
Normal file
155
res/bin/any/xowa/xtns/Graph/Xograph.js
Normal file
@@ -0,0 +1,155 @@
|
||||
xo.xtns = xo.xtns || {};
|
||||
xo.xtns.graph = new function() {
|
||||
var xolog = new xo.logs.logger('xo.xtns.graph');
|
||||
var vega_cbks = {} // map of callbacks for tabular
|
||||
// var page_cache = {}; // TOMBSTONE: do not try to cache requests; multiple requests for same page may start at same time
|
||||
|
||||
this.load_xowa = function(url, opt, callback) {
|
||||
this.init_xowa_props(opt);
|
||||
|
||||
// cache callback
|
||||
var vega_cbk_guid = Uuid_.make();
|
||||
vega_cbks[vega_cbk_guid] = callback;
|
||||
|
||||
// call xo_server
|
||||
var page_guid = xowa.page.guid;
|
||||
xolog.info("get_page.bgn", "page", opt.xowa_page);
|
||||
xo.server.send_by_bridge('xowa.app.util.misc', 'page_get',
|
||||
{ 'page_get_cbk':'xo.xtns.graph.load_xowa_recv'
|
||||
, 'protocol':opt.xowa_protocol
|
||||
, 'wiki':opt.xowa_wiki
|
||||
, 'page':opt.xowa_page
|
||||
, 'page_guid':page_guid
|
||||
, 'vega_cbk_guid':vega_cbk_guid
|
||||
});
|
||||
}
|
||||
|
||||
this.load_xowa_recv = function(msg_str) {
|
||||
// parse msg
|
||||
var msg = JSON.parse(msg_str);
|
||||
xolog.info("get_page.end", "page", msg.page);
|
||||
|
||||
// get page_text
|
||||
var page_text = msg.page_text;
|
||||
var protocol = msg.protocol;
|
||||
switch (protocol) {
|
||||
case 'map:':
|
||||
case 'tabular:':
|
||||
page_text = "{\"jsondata\":" + page_text + "}"; // format as "{jsondata:page_text}"; note that this is done by JsonConfig api
|
||||
break;
|
||||
}
|
||||
|
||||
// get callback and process page_text
|
||||
var callback = vega_cbks[msg.vega_cbk_guid];
|
||||
callback(null, page_text);
|
||||
|
||||
return "pass:load_xowa_recv"; // return something for xoajax
|
||||
}
|
||||
|
||||
this.init_xowa_props = function(opt) {
|
||||
// get protocol
|
||||
var url = opt.url;
|
||||
var protocol_end = url.indexOf(':/');
|
||||
var protocol = 'unknown';
|
||||
if (protocol_end != -1) {
|
||||
protocol = url.substring(0, protocol_end + 1).toLowerCase(); // +1 to capture ":"; note that graph2.compiled.js compares to "wikiraw:", "map:", etc.
|
||||
}
|
||||
|
||||
// get page
|
||||
var page = '';
|
||||
var wiki = 'commons.wikimedia.org';
|
||||
switch (protocol) {
|
||||
case 'map:':
|
||||
page = replace_at_bgn_or_null(url, "map:///", "Data:");
|
||||
break;
|
||||
case 'tabular:':
|
||||
page = replace_at_bgn_or_null(url, "tabular:///", "Data:");
|
||||
break;
|
||||
case 'wikiraw:':
|
||||
page = replace_at_bgn_or_null(url, "wikiraw:///", "");
|
||||
wiki = xowa.page.wiki;
|
||||
break;
|
||||
}
|
||||
if (page == null) {
|
||||
xolog.warn("unknown page protocol", "protocol", protocol, "url", url);
|
||||
}
|
||||
|
||||
// set xowa_props
|
||||
opt.xowa_protocol = protocol;
|
||||
opt.xowa_wiki = wiki;
|
||||
opt.xowa_page = page;
|
||||
}
|
||||
|
||||
// XOWA: from /modules/graph1.js|graph2.js; modified b/c MW use mw.hook('wikipage.content'); DATE:2018-02-25
|
||||
this.drawVegaGraph = function(version, elem, data, callback)
|
||||
{
|
||||
if (version == 1)
|
||||
{
|
||||
vg.parse.spec
|
||||
( data
|
||||
, function (chart)
|
||||
{
|
||||
if (chart)
|
||||
{
|
||||
chart({el: elem}).update();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
vg.parse.spec
|
||||
( data
|
||||
, function (error, chart)
|
||||
{
|
||||
if (!error)
|
||||
{
|
||||
chart({el:elem}).update();
|
||||
}
|
||||
if (callback)
|
||||
{
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.exec = function()
|
||||
{
|
||||
xolog.info("running graphs");
|
||||
|
||||
var $ = jQuery;
|
||||
var $content = $('#content');
|
||||
// from graph2.compiled.js; modified b/c MW saves data in mw.config.get('wgGraphSpecs')
|
||||
$content.find( '.mw-graph' ).each
|
||||
( function ()
|
||||
{
|
||||
var elem = this;
|
||||
var version = $(this).attr('xo-graph-version');
|
||||
var html = $(this).html();
|
||||
var spec = JSON.parse(Html_.decode(html));
|
||||
xolog.info("running graph", "version", version); //, "data", html);
|
||||
|
||||
// blank out html; run graph
|
||||
$(this).html('');
|
||||
xo.xtns.graph.drawVegaGraph
|
||||
( version
|
||||
, this
|
||||
, spec
|
||||
, function (error)
|
||||
{
|
||||
if (error)
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
var replace_at_bgn_or_null = function(str, find, repl) {
|
||||
if (str.startsWith(find))
|
||||
return repl + str.substring(find.length, str.length);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
73
res/bin/any/xowa/xtns/Graph/js/graph.js
Normal file
73
res/bin/any/xowa/xtns/Graph/js/graph.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// NOTE: edited from original to use local json libraries; DATE:2015-09-05
|
||||
(function($){
|
||||
if (window.xtn__graph__exec == null) {
|
||||
window.xtn__graph__exec = graph_exec;
|
||||
}
|
||||
/*
|
||||
function graph_exec() {
|
||||
var $content = $('#content');
|
||||
$content.find( '.mw-graph.mw-graph-always' ).each( function () {
|
||||
console.log('found .mw-graph.mw-graph-always');
|
||||
});
|
||||
$content.find( '.mw-graph' ).each( function () {
|
||||
console.log('found .mw-graph');
|
||||
});
|
||||
$content.find( '.mw-graph' ).each( function () {
|
||||
// $content.find( '.mw-wiki-graph' ).each( function () {
|
||||
// $content.find( '.mw-graph' ).each( function () {
|
||||
var graphId = $( this ).data( 'graph-id' );
|
||||
var elem = this;
|
||||
var spec = JSON.parse(htmlDecode($(this).html()));
|
||||
$(this).html('');
|
||||
vg.parse.spec
|
||||
( spec, function(chart)
|
||||
{
|
||||
if (chart) {
|
||||
chart({ el: elem}).update();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
*/
|
||||
window.drawVegaGraph = function ( elem, data, callback ) {
|
||||
vg.parse.spec( data, function ( error, chart ) {
|
||||
if ( !error ) {
|
||||
chart( { el: elem } ).update();
|
||||
}
|
||||
if ( callback ) {
|
||||
callback( error );
|
||||
}
|
||||
} );
|
||||
};
|
||||
function graph_exec() {
|
||||
var $content = $('#content');
|
||||
|
||||
var specs = {}; // mw.config.get( 'wgGraphSpecs' );
|
||||
if ( !specs ) {
|
||||
return;
|
||||
}
|
||||
$content.find( '.mw-graph,.mw-graph-always' ).each( function () {
|
||||
// var graphId = $( this ).data( 'graph-id' );
|
||||
// if ( !specs.hasOwnProperty( graphId ) ) {
|
||||
// mw.log.warn( graphId );
|
||||
// } else {
|
||||
var elem = this;
|
||||
var spec = JSON.parse(htmlDecode($(this).html()));
|
||||
$(this).html('');
|
||||
window.drawVegaGraph( this, spec, function ( error ) {
|
||||
// window.drawVegaGraph( this, specs[ graphId ], function ( error ) {
|
||||
if ( error ) {
|
||||
// mw.log.warn( error );
|
||||
}
|
||||
} );
|
||||
// }
|
||||
} );
|
||||
}
|
||||
})(jQuery);
|
||||
|
||||
function htmlDecode(input)
|
||||
{
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
3
res/bin/any/xowa/xtns/Graph/lib/d3-global.js
vendored
Normal file
3
res/bin/any/xowa/xtns/Graph/lib/d3-global.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// Back-compat: Export module as global
|
||||
// XOWA: unused b/c module is not available
|
||||
window.d3 = module.exports;
|
||||
9554
res/bin/any/xowa/xtns/Graph/lib/d3.js
vendored
Normal file
9554
res/bin/any/xowa/xtns/Graph/lib/d3.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
505
res/bin/any/xowa/xtns/Graph/lib/d3.layout.cloud.js
Normal file
505
res/bin/any/xowa/xtns/Graph/lib/d3.layout.cloud.js
Normal file
@@ -0,0 +1,505 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
|
||||
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
|
||||
|
||||
var dispatch = require("d3-dispatch").dispatch;
|
||||
|
||||
var cloudRadians = Math.PI / 180,
|
||||
cw = 1 << 11 >> 5,
|
||||
ch = 1 << 11;
|
||||
|
||||
module.exports = function() {
|
||||
var size = [256, 256],
|
||||
text = cloudText,
|
||||
font = cloudFont,
|
||||
fontSize = cloudFontSize,
|
||||
fontStyle = cloudFontNormal,
|
||||
fontWeight = cloudFontNormal,
|
||||
rotate = cloudRotate,
|
||||
padding = cloudPadding,
|
||||
spiral = archimedeanSpiral,
|
||||
words = [],
|
||||
timeInterval = Infinity,
|
||||
event = dispatch("word", "end"),
|
||||
timer = null,
|
||||
random = Math.random,
|
||||
cloud = {},
|
||||
canvas = cloudCanvas;
|
||||
|
||||
cloud.canvas = function(_) {
|
||||
return arguments.length ? (canvas = functor(_), cloud) : canvas;
|
||||
};
|
||||
|
||||
cloud.start = function() {
|
||||
var contextAndRatio = getContext(canvas()),
|
||||
board = zeroArray((size[0] >> 5) * size[1]),
|
||||
bounds = null,
|
||||
n = words.length,
|
||||
i = -1,
|
||||
tags = [],
|
||||
data = words.map(function(d, i) {
|
||||
d.text = text.call(this, d, i);
|
||||
d.font = font.call(this, d, i);
|
||||
d.style = fontStyle.call(this, d, i);
|
||||
d.weight = fontWeight.call(this, d, i);
|
||||
d.rotate = rotate.call(this, d, i);
|
||||
d.size = ~~fontSize.call(this, d, i);
|
||||
d.padding = padding.call(this, d, i);
|
||||
return d;
|
||||
}).sort(function(a, b) { return b.size - a.size; });
|
||||
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(step, 0);
|
||||
step();
|
||||
|
||||
return cloud;
|
||||
|
||||
function step() {
|
||||
var start = Date.now();
|
||||
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
||||
var d = data[i];
|
||||
d.x = (size[0] * (random() + .5)) >> 1;
|
||||
d.y = (size[1] * (random() + .5)) >> 1;
|
||||
cloudSprite(contextAndRatio, d, data, i);
|
||||
if (d.hasText && place(board, d, bounds)) {
|
||||
tags.push(d);
|
||||
event.word(d);
|
||||
if (bounds) cloudBounds(bounds, d);
|
||||
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
|
||||
// Temporary hack
|
||||
d.x -= size[0] >> 1;
|
||||
d.y -= size[1] >> 1;
|
||||
}
|
||||
}
|
||||
if (i >= n) {
|
||||
cloud.stop();
|
||||
event.end(tags, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cloud.stop = function() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
return cloud;
|
||||
};
|
||||
|
||||
function getContext(canvas) {
|
||||
canvas.width = canvas.height = 1;
|
||||
var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
|
||||
canvas.width = (cw << 5) / ratio;
|
||||
canvas.height = ch / ratio;
|
||||
|
||||
var context = canvas.getContext("2d");
|
||||
context.fillStyle = context.strokeStyle = "red";
|
||||
context.textAlign = "center";
|
||||
|
||||
return {context: context, ratio: ratio};
|
||||
}
|
||||
|
||||
function place(board, tag, bounds) {
|
||||
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
|
||||
startX = tag.x,
|
||||
startY = tag.y,
|
||||
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
|
||||
s = spiral(size),
|
||||
dt = random() < .5 ? 1 : -1,
|
||||
t = -dt,
|
||||
dxdy,
|
||||
dx,
|
||||
dy;
|
||||
|
||||
while (dxdy = s(t += dt)) {
|
||||
dx = ~~dxdy[0];
|
||||
dy = ~~dxdy[1];
|
||||
|
||||
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
||||
|
||||
tag.x = startX + dx;
|
||||
tag.y = startY + dy;
|
||||
|
||||
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
||||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
||||
// TODO only check for collisions within current bounds.
|
||||
if (!bounds || !cloudCollide(tag, board, size[0])) {
|
||||
if (!bounds || collideRects(tag, bounds)) {
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
sw = size[0] >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
delete tag.sprite;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cloud.timeInterval = function(_) {
|
||||
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
||||
};
|
||||
|
||||
cloud.words = function(_) {
|
||||
return arguments.length ? (words = _, cloud) : words;
|
||||
};
|
||||
|
||||
cloud.size = function(_) {
|
||||
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
||||
};
|
||||
|
||||
cloud.font = function(_) {
|
||||
return arguments.length ? (font = functor(_), cloud) : font;
|
||||
};
|
||||
|
||||
cloud.fontStyle = function(_) {
|
||||
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
|
||||
};
|
||||
|
||||
cloud.fontWeight = function(_) {
|
||||
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
|
||||
};
|
||||
|
||||
cloud.rotate = function(_) {
|
||||
return arguments.length ? (rotate = functor(_), cloud) : rotate;
|
||||
};
|
||||
|
||||
cloud.text = function(_) {
|
||||
return arguments.length ? (text = functor(_), cloud) : text;
|
||||
};
|
||||
|
||||
cloud.spiral = function(_) {
|
||||
return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
|
||||
};
|
||||
|
||||
cloud.fontSize = function(_) {
|
||||
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
|
||||
};
|
||||
|
||||
cloud.padding = function(_) {
|
||||
return arguments.length ? (padding = functor(_), cloud) : padding;
|
||||
};
|
||||
|
||||
cloud.random = function(_) {
|
||||
return arguments.length ? (random = _, cloud) : random;
|
||||
};
|
||||
|
||||
cloud.on = function() {
|
||||
var value = event.on.apply(event, arguments);
|
||||
return value === event ? cloud : value;
|
||||
};
|
||||
|
||||
return cloud;
|
||||
};
|
||||
|
||||
function cloudText(d) {
|
||||
return d.text;
|
||||
}
|
||||
|
||||
function cloudFont() {
|
||||
return "serif";
|
||||
}
|
||||
|
||||
function cloudFontNormal() {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
function cloudFontSize(d) {
|
||||
return Math.sqrt(d.value);
|
||||
}
|
||||
|
||||
function cloudRotate() {
|
||||
return (~~(Math.random() * 6) - 3) * 30;
|
||||
}
|
||||
|
||||
function cloudPadding() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetches a monochrome sprite bitmap for the specified text.
|
||||
// Load in batches for speed.
|
||||
function cloudSprite(contextAndRatio, d, data, di) {
|
||||
if (d.sprite) return;
|
||||
var c = contextAndRatio.context,
|
||||
ratio = contextAndRatio.ratio;
|
||||
|
||||
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
||||
var x = 0,
|
||||
y = 0,
|
||||
maxh = 0,
|
||||
n = data.length;
|
||||
--di;
|
||||
while (++di < n) {
|
||||
d = data[di];
|
||||
c.save();
|
||||
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
||||
var w = c.measureText(d.text + "m").width * ratio,
|
||||
h = d.size << 1;
|
||||
if (d.rotate) {
|
||||
var sr = Math.sin(d.rotate * cloudRadians),
|
||||
cr = Math.cos(d.rotate * cloudRadians),
|
||||
wcr = w * cr,
|
||||
wsr = w * sr,
|
||||
hcr = h * cr,
|
||||
hsr = h * sr;
|
||||
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
||||
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
||||
} else {
|
||||
w = (w + 0x1f) >> 5 << 5;
|
||||
}
|
||||
if (h > maxh) maxh = h;
|
||||
if (x + w >= (cw << 5)) {
|
||||
x = 0;
|
||||
y += maxh;
|
||||
maxh = 0;
|
||||
}
|
||||
if (y + h >= ch) break;
|
||||
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
||||
if (d.rotate) c.rotate(d.rotate * cloudRadians);
|
||||
c.fillText(d.text, 0, 0);
|
||||
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
|
||||
c.restore();
|
||||
d.width = w;
|
||||
d.height = h;
|
||||
d.xoff = x;
|
||||
d.yoff = y;
|
||||
d.x1 = w >> 1;
|
||||
d.y1 = h >> 1;
|
||||
d.x0 = -d.x1;
|
||||
d.y0 = -d.y1;
|
||||
d.hasText = true;
|
||||
x += w;
|
||||
}
|
||||
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
|
||||
sprite = [];
|
||||
while (--di >= 0) {
|
||||
d = data[di];
|
||||
if (!d.hasText) continue;
|
||||
var w = d.width,
|
||||
w32 = w >> 5,
|
||||
h = d.y1 - d.y0;
|
||||
// Zero the buffer
|
||||
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
|
||||
x = d.xoff;
|
||||
if (x == null) return;
|
||||
y = d.yoff;
|
||||
var seen = 0,
|
||||
seenRow = -1;
|
||||
for (var j = 0; j < h; j++) {
|
||||
for (var i = 0; i < w; i++) {
|
||||
var k = w32 * j + (i >> 5),
|
||||
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
||||
sprite[k] |= m;
|
||||
seen |= m;
|
||||
}
|
||||
if (seen) seenRow = j;
|
||||
else {
|
||||
d.y0++;
|
||||
h--;
|
||||
j--;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
d.y1 = d.y0 + seenRow;
|
||||
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
||||
}
|
||||
}
|
||||
|
||||
// Use mask-based collision detection.
|
||||
function cloudCollide(tag, board, sw) {
|
||||
sw >>= 5;
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
||||
& board[x + i]) return true;
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function cloudBounds(bounds, d) {
|
||||
var b0 = bounds[0],
|
||||
b1 = bounds[1];
|
||||
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
||||
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
||||
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
||||
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
||||
}
|
||||
|
||||
function collideRects(a, b) {
|
||||
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
||||
}
|
||||
|
||||
function archimedeanSpiral(size) {
|
||||
var e = size[0] / size[1];
|
||||
return function(t) {
|
||||
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
||||
};
|
||||
}
|
||||
|
||||
function rectangularSpiral(size) {
|
||||
var dy = 4,
|
||||
dx = dy * size[0] / size[1],
|
||||
x = 0,
|
||||
y = 0;
|
||||
return function(t) {
|
||||
var sign = t < 0 ? -1 : 1;
|
||||
// See triangular numbers: T_n = n * (n + 1) / 2.
|
||||
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
||||
case 0: x += dx; break;
|
||||
case 1: y += dy; break;
|
||||
case 2: x -= dx; break;
|
||||
default: y -= dy; break;
|
||||
}
|
||||
return [x, y];
|
||||
};
|
||||
}
|
||||
|
||||
// TODO reuse arrays?
|
||||
function zeroArray(n) {
|
||||
var a = [],
|
||||
i = -1;
|
||||
while (++i < n) a[i] = 0;
|
||||
return a;
|
||||
}
|
||||
|
||||
function cloudCanvas() {
|
||||
return document.createElement("canvas");
|
||||
}
|
||||
|
||||
function functor(d) {
|
||||
return typeof d === "function" ? d : function() { return d; };
|
||||
}
|
||||
|
||||
var spirals = {
|
||||
archimedean: archimedeanSpiral,
|
||||
rectangular: rectangularSpiral
|
||||
};
|
||||
|
||||
},{"d3-dispatch":2}],2:[function(require,module,exports){
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
factory((global.dispatch = {}));
|
||||
}(this, function (exports) { 'use strict';
|
||||
|
||||
function Dispatch(types) {
|
||||
var i = -1,
|
||||
n = types.length,
|
||||
callbacksByType = {},
|
||||
callbackByName = {},
|
||||
type,
|
||||
that = this;
|
||||
|
||||
that.on = function(type, callback) {
|
||||
type = parseType(type);
|
||||
|
||||
// Return the current callback, if any.
|
||||
if (arguments.length < 2) {
|
||||
return (callback = callbackByName[type.name]) && callback.value;
|
||||
}
|
||||
|
||||
// If a type was specified…
|
||||
if (type.type) {
|
||||
var callbacks = callbacksByType[type.type],
|
||||
callback0 = callbackByName[type.name],
|
||||
i;
|
||||
|
||||
// Remove the current callback, if any, using copy-on-remove.
|
||||
if (callback0) {
|
||||
callback0.value = null;
|
||||
i = callbacks.indexOf(callback0);
|
||||
callbacksByType[type.type] = callbacks = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
|
||||
delete callbackByName[type.name];
|
||||
}
|
||||
|
||||
// Add the new callback, if any.
|
||||
if (callback) {
|
||||
callback = {value: callback};
|
||||
callbackByName[type.name] = callback;
|
||||
callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if a null callback was specified, remove all callbacks with the given name.
|
||||
else if (callback == null) {
|
||||
for (var otherType in callbacksByType) {
|
||||
if (callback = callbackByName[otherType + type.name]) {
|
||||
callback.value = null;
|
||||
var callbacks = callbacksByType[otherType], i = callbacks.indexOf(callback);
|
||||
callbacksByType[otherType] = callbacks.slice(0, i).concat(callbacks.slice(i + 1));
|
||||
delete callbackByName[callback.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return that;
|
||||
};
|
||||
|
||||
while (++i < n) {
|
||||
type = types[i] + "";
|
||||
if (!type || (type in that)) throw new Error("illegal or duplicate type: " + type);
|
||||
callbacksByType[type] = [];
|
||||
that[type] = applier(type);
|
||||
}
|
||||
|
||||
function parseType(type) {
|
||||
var i = (type += "").indexOf("."), name = type;
|
||||
if (i >= 0) type = type.slice(0, i); else name += ".";
|
||||
if (type && !callbacksByType.hasOwnProperty(type)) throw new Error("unknown type: " + type);
|
||||
return {type: type, name: name};
|
||||
}
|
||||
|
||||
function applier(type) {
|
||||
return function() {
|
||||
var callbacks = callbacksByType[type], // Defensive reference; copy-on-remove.
|
||||
callback,
|
||||
callbackValue,
|
||||
i = -1,
|
||||
n = callbacks.length;
|
||||
|
||||
while (++i < n) {
|
||||
if (callbackValue = (callback = callbacks[i]).value) {
|
||||
callbackValue.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
return that;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function dispatch() {
|
||||
return new Dispatch(arguments);
|
||||
}
|
||||
|
||||
dispatch.prototype = Dispatch.prototype; // allow instanceof
|
||||
|
||||
exports.dispatch = dispatch;
|
||||
|
||||
}));
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
697
res/bin/any/xowa/xtns/Graph/lib/graph2.compiled.js
Normal file
697
res/bin/any/xowa/xtns/Graph/lib/graph2.compiled.js
Normal file
@@ -0,0 +1,697 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
( function ( $, mw, vg ) {
|
||||
|
||||
'use strict';
|
||||
/* global require */
|
||||
|
||||
var VegaWrapper = require( 'mw-graph-shared' );
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new VegaWrapper( {
|
||||
datalib: vg.util,
|
||||
useXhr: true,
|
||||
isTrusted: true, // mw.config.get( 'wgGraphIsTrusted' ),
|
||||
domains: '', // mw.config.get( 'wgGraphAllowedDomains' ),
|
||||
domainMap: false,
|
||||
logger: function ( warning ) {
|
||||
mw.log.warn( warning );
|
||||
},
|
||||
parseUrl: function ( opt ) {
|
||||
// Parse URL
|
||||
var uri = {}; // new mw.Uri( opt.url );
|
||||
|
||||
// reduce confusion, only keep expected values
|
||||
if ( uri.port ) {
|
||||
uri.host += ':' + uri.port;
|
||||
delete uri.port;
|
||||
}
|
||||
// If url begins with protocol:///... mark it as having relative host
|
||||
if ( /^[a-z]+:\/\/\//.test( opt.url ) ) {
|
||||
uri.isRelativeHost = true;
|
||||
}
|
||||
if ( uri.protocol ) {
|
||||
// All other libs use trailing colon in the protocol field
|
||||
uri.protocol += ':';
|
||||
}
|
||||
// Node's path includes the query, whereas pathname is without the query
|
||||
// Standardizing on pathname
|
||||
uri.pathname = uri.path;
|
||||
|
||||
delete uri.path;
|
||||
return uri;
|
||||
},
|
||||
formatUrl: function ( uri, opt ) {
|
||||
// Format URL back into a string
|
||||
// Revert path into pathname
|
||||
uri.path = uri.pathname;
|
||||
delete uri.pathname;
|
||||
|
||||
if ( location.host.toLowerCase() === uri.host.toLowerCase() ) {
|
||||
if ( !mw.config.get( 'wgGraphIsTrusted' ) ) {
|
||||
// Only send this header when hostname is the same.
|
||||
// This is broader than the same-origin policy,
|
||||
// but playing on the safer side.
|
||||
opt.headers = { 'Treat-as-Untrusted': 1 };
|
||||
}
|
||||
} else if ( opt.addCorsOrigin ) {
|
||||
// All CORS api calls require origin parameter.
|
||||
// It would be better to use location.origin,
|
||||
// but apparently it's not universal yet.
|
||||
uri.query.origin = location.protocol + '//' + location.host;
|
||||
}
|
||||
|
||||
uri.protocol = VegaWrapper.removeColon( uri.protocol );
|
||||
|
||||
return uri.toString();
|
||||
},
|
||||
languageCode: 'en' // mw.config.get( 'wgUserLanguage' )
|
||||
} );
|
||||
|
||||
/**
|
||||
* Set up drawing canvas inside the given element and draw graph data
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {Object|string} data graph spec
|
||||
* @param {Function} [callback] function(error) called when drawing is done
|
||||
*/
|
||||
window.drawVegaGraph = function ( element, data, callback ) {
|
||||
vg.parse.spec( data, function ( error, chart ) {
|
||||
if ( !error ) {
|
||||
chart( { el: element } ).update();
|
||||
}
|
||||
if ( callback ) {
|
||||
callback( error );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
// mw.hook( 'wikipage.content' ).add( function ( $content ) {
|
||||
var $content = $('#content');
|
||||
|
||||
var specs = {}; // mw.config.get( 'wgGraphSpecs' );
|
||||
if ( !specs ) {
|
||||
return;
|
||||
}
|
||||
$content.find( '.mw-graph.mw-graph-always' ).each( function () {
|
||||
var graphId = $( this ).data( 'graph-id' );
|
||||
if ( !specs.hasOwnProperty( graphId ) ) {
|
||||
// mw.log.warn( graphId );
|
||||
} else {
|
||||
window.drawVegaGraph( this, specs[ graphId ], function ( error ) {
|
||||
if ( error ) {
|
||||
// mw.log.warn( error );
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
// } );
|
||||
|
||||
}( jQuery, null, vg ) );
|
||||
|
||||
},{"mw-graph-shared":3}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Convert a list of domains into an object with a test method.
|
||||
* equivalent regex: (any-subdomain)\.(wikipedia\.org|wikivoyage\.org|...)
|
||||
*
|
||||
* @param domains array of string domains
|
||||
* @param allowSubdomains if true, allows any sub and sub-sub-* domains
|
||||
* @returns {*}
|
||||
*/
|
||||
module.exports = function makeValidator(domains, allowSubdomains) {
|
||||
if (!domains || domains.length === 0) return {
|
||||
// Optimization - always return false
|
||||
test: function () {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return new RegExp(
|
||||
(allowSubdomains ? '^([^@/:]*\\.)?(' : '^(') +
|
||||
domains
|
||||
.map(function (s) {
|
||||
return s.replace('.', '\\.');
|
||||
})
|
||||
.join('|') + ')$', 'i');
|
||||
};
|
||||
|
||||
},{}],3:[function(require,module,exports){
|
||||
'use strict';
|
||||
/* global module */
|
||||
|
||||
var makeValidator = require('domain-validator'),
|
||||
parseWikidataValue = require('wd-type-parser');
|
||||
|
||||
module.exports = VegaWrapper;
|
||||
module.exports.removeColon = removeColon;
|
||||
|
||||
/**
|
||||
* Utility function to remove trailing colon from a protocol
|
||||
* @param {string} protocol
|
||||
* @return {string}
|
||||
*/
|
||||
function removeColon(protocol) {
|
||||
return protocol && protocol.length && protocol[protocol.length - 1] === ':'
|
||||
? protocol.substring(0, protocol.length - 1) : protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared library to wrap around vega code
|
||||
* @param {Object} wrapperOpts Configuration options
|
||||
* @param {Object} wrapperOpts.datalib Vega's datalib object
|
||||
* @param {Object} wrapperOpts.datalib.load Vega's data loader
|
||||
* @param {Function} wrapperOpts.datalib.load.loader Vega's data loader function
|
||||
* @param {Function} wrapperOpts.datalib.extend similar to jquery's extend()
|
||||
* @param {boolean} wrapperOpts.useXhr true if we should use XHR, false for node.js http loading
|
||||
* @param {boolean} wrapperOpts.isTrusted true if the graph spec can be trusted
|
||||
* @param {Object} wrapperOpts.domains allowed protocols and a list of their domains
|
||||
* @param {Object} wrapperOpts.domainMap domain remapping
|
||||
* @param {Function} wrapperOpts.logger
|
||||
* @param {Function} wrapperOpts.parseUrl
|
||||
* @param {Function} wrapperOpts.formatUrl
|
||||
* @param {string} [wrapperOpts.languageCode]
|
||||
* @constructor
|
||||
*/
|
||||
function VegaWrapper(wrapperOpts) {
|
||||
var self = this;
|
||||
// Copy all options into this object
|
||||
self.objExtender = wrapperOpts.datalib.extend;
|
||||
self.objExtender(self, wrapperOpts);
|
||||
self.validators = {};
|
||||
|
||||
self.datalib.load.loader = function (opt, callback) {
|
||||
var error = callback || function (e) { throw e; }, url;
|
||||
|
||||
try {
|
||||
url = self.sanitizeUrl(opt); // enable override
|
||||
} catch (err) {
|
||||
error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process data response
|
||||
var cb = function (error, data) {
|
||||
return self.dataParser(error, data, opt, callback);
|
||||
};
|
||||
|
||||
/*
|
||||
if (self.useXhr) {
|
||||
return self.datalib.load.xhr(url, opt, cb);
|
||||
} else {
|
||||
return self.datalib.load.http(url, opt, cb);
|
||||
}
|
||||
*/
|
||||
return xo.xtns.graph.load_xowa(url, opt, cb);
|
||||
};
|
||||
|
||||
self.datalib.load.sanitizeUrl = self.sanitizeUrl.bind(self);
|
||||
|
||||
// Prevent accidental use
|
||||
self.datalib.load.file = alwaysFail;
|
||||
if (self.useXhr) {
|
||||
self.datalib.load.http = alwaysFail;
|
||||
} else {
|
||||
self.datalib.load.xhr = alwaysFail;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if host was listed in the allowed domains, normalize it, and get correct protocol
|
||||
* @param {string} host
|
||||
* @returns {Object}
|
||||
*/
|
||||
VegaWrapper.prototype.sanitizeHost = function sanitizeHost(host) {
|
||||
// First, map the host
|
||||
host = (this.domainMap && this.domainMap[host]) || host;
|
||||
|
||||
if (this.testHost('https:', host)) {
|
||||
return {host: host, protocol: 'https:'};
|
||||
} else if (this.testHost('http:', host)) {
|
||||
return {host: host, protocol: 'http:'};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test host against the list of allowed domains based on the protocol
|
||||
* @param {string} protocol
|
||||
* @param {string} host
|
||||
* @returns {boolean}
|
||||
*/
|
||||
VegaWrapper.prototype.testHost = function testHost(protocol, host) {
|
||||
if (!this.validators[protocol]) {
|
||||
var domains = this._getProtocolDomains(protocol);
|
||||
if (domains) {
|
||||
this.validators[protocol] = makeValidator(domains, protocol === 'https:' || protocol === 'http:');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.validators[protocol].test(host);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets allowed domains for a given protocol. Assumes protocol ends with a ':'.
|
||||
* Handles if this.domains's keys do not end in the ':'.
|
||||
* @param {string} protocol
|
||||
* @return {[]|false}
|
||||
* @private
|
||||
*/
|
||||
VegaWrapper.prototype._getProtocolDomains = function _getProtocolDomains(protocol) {
|
||||
return this.domains[protocol] || this.domains[removeColon(protocol)];
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and update urlObj to be safe for client-side and server-side usage
|
||||
* @param {Object} opt passed by the vega loader, and will add 'graphProtocol' param
|
||||
* @returns {boolean} true on success
|
||||
*/
|
||||
VegaWrapper.prototype.sanitizeUrl = function sanitizeUrl(opt) {
|
||||
// In some cases we may receive a badly formed URL in a form customprotocol:https://...
|
||||
opt.url = opt.url.replace(/^([a-z]+:)https?:\/\//, '$1//');
|
||||
|
||||
// XOWA: comment out URL parsing code as it relies on various unavailable MediaWiki functions; DATE:2018-03-13
|
||||
// note that URL parsing will be done directly in Xograph.js
|
||||
/*
|
||||
var decodedPathname,
|
||||
isRelativeProtocol = /^\/\//.test(opt.url),
|
||||
urlParts = this.parseUrl(opt),
|
||||
sanitizedHost = this.sanitizeHost(urlParts.host);
|
||||
|
||||
// if (!sanitizedHost) {
|
||||
// throw new Error('URL hostname is not whitelisted: ' + opt.url);
|
||||
// }
|
||||
urlParts.host = sanitizedHost.host;
|
||||
if (!urlParts.protocol) {
|
||||
// node.js mode only - browser's url parser will always set protocol to current one
|
||||
// Update protocol-relative URLs
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
isRelativeProtocol = true;
|
||||
}
|
||||
|
||||
// Save original protocol to post-process the data
|
||||
opt.graphProtocol = urlParts.protocol;
|
||||
|
||||
if (opt.type === 'open') {
|
||||
|
||||
// Trim the value here because mediawiki will do it anyway, so we might as well save on redirect
|
||||
decodedPathname = decodeURIComponent(urlParts.pathname).trim();
|
||||
|
||||
switch (urlParts.protocol) {
|
||||
case 'http:':
|
||||
case 'https:':
|
||||
// The default protocol for the open action is wikititle, so if isRelativeProtocol is set,
|
||||
// we treat the whole pathname as title (without the '/' prefix).
|
||||
if (!isRelativeProtocol) {
|
||||
// If we get http:// and https:// protocol hardcoded, remove the '/wiki/' prefix instead
|
||||
if (!/^\/wiki\/.+$/.test(decodedPathname)) {
|
||||
throw new Error('wikititle: http(s) links must begin with /wiki/ prefix');
|
||||
}
|
||||
decodedPathname = decodedPathname.substring('/wiki'.length);
|
||||
}
|
||||
opt.graphProtocol = 'wikititle';
|
||||
// fall-through
|
||||
|
||||
case 'wikititle:':
|
||||
// wikititle:///My_page or wikititle://en.wikipedia.org/My_page
|
||||
// open() at this point may only be used to link to a Wiki page, as it may be invoked
|
||||
// without a click, thus potentially causing a privacy issue.
|
||||
if (Object.keys(urlParts.query).length !== 0) {
|
||||
throw new Error('wikititle: query parameters are not allowed');
|
||||
}
|
||||
if (!/^\/[^|]+$/.test(decodedPathname)) {
|
||||
throw new Error('wikititle: invalid title');
|
||||
}
|
||||
urlParts.pathname = '/wiki/' + encodeURIComponent(decodedPathname.substring(1).replace(' ', '_'));
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('"open()" action only allows links with wikititle protocol, e.g. wikititle:///My_page');
|
||||
}
|
||||
} else {
|
||||
|
||||
switch (urlParts.protocol) {
|
||||
case 'http:':
|
||||
case 'https:':
|
||||
if (!this.isTrusted) {
|
||||
throw new Error('HTTP and HTTPS protocols are not supported for untrusted graphs.\n' +
|
||||
'Use wikiraw:, wikiapi:, wikirest:, wikirawupload:, and other protocols.\n' +
|
||||
'See https://www.mediawiki.org/wiki/Extension:Graph#External_data');
|
||||
}
|
||||
// keep the original URL
|
||||
break;
|
||||
|
||||
case 'wikiapi:':
|
||||
// wikiapi:///?action=query&list=allpages
|
||||
// Call to api.php - ignores the path parameter, and only uses the query
|
||||
urlParts.query = this.objExtender(urlParts.query, {format: 'json', formatversion: '2'});
|
||||
urlParts.pathname = '/w/api.php';
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
opt.addCorsOrigin = true;
|
||||
break;
|
||||
|
||||
case 'wikirest:':
|
||||
// wikirest:///api/rest_v1/page/...
|
||||
// Call to RESTbase api - requires the path to start with "/api/"
|
||||
// The /api/... path is safe for GET requests
|
||||
if (!/^\/api\//.test(urlParts.pathname)) {
|
||||
throw new Error('wikirest: protocol must begin with the /api/ prefix');
|
||||
}
|
||||
// keep urlParts.query
|
||||
// keep urlParts.pathname
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
break;
|
||||
|
||||
case 'wikiraw:':
|
||||
case 'tabular:':
|
||||
case 'map:':
|
||||
// wikiraw:///MyPage/data
|
||||
// Get content of a wiki page, where the path is the title
|
||||
// of the page with an additional leading '/' which gets removed.
|
||||
// Uses mediawiki api, and extract the content after the request
|
||||
// Query value must be a valid MediaWiki title string, but we only ensure
|
||||
// there is no pipe symbol, the rest is handled by the api.
|
||||
decodedPathname = decodeURIComponent(urlParts.pathname);
|
||||
if (!/^\/[^|]+$/.test(decodedPathname)) {
|
||||
throw new Error(urlParts.protocol + ' invalid title');
|
||||
}
|
||||
if (urlParts.protocol === 'wikiraw:') {
|
||||
urlParts.query = {
|
||||
format: 'json',
|
||||
formatversion: '2',
|
||||
action: 'query',
|
||||
prop: 'revisions',
|
||||
rvprop: 'content',
|
||||
titles: decodedPathname.substring(1)
|
||||
};
|
||||
} else {
|
||||
urlParts.query = {
|
||||
format: 'json',
|
||||
formatversion: '2',
|
||||
action: 'jsondata',
|
||||
title: decodedPathname.substring(1)
|
||||
};
|
||||
if (urlParts.siteLanguage || this.languageCode) {
|
||||
urlParts.query.uselang = urlParts.siteLanguage || this.languageCode;
|
||||
}
|
||||
}
|
||||
|
||||
urlParts.pathname = '/w/api.php';
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
opt.addCorsOrigin = true;
|
||||
break;
|
||||
|
||||
case 'wikifile:':
|
||||
// wikifile:///Einstein_1921.jpg
|
||||
// Get an image for the graph, e.g. from commons, by using Special:Redirect
|
||||
urlParts.pathname = '/wiki/Special:Redirect/file' + urlParts.pathname;
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
// keep urlParts.query
|
||||
break;
|
||||
|
||||
case 'wikirawupload:':
|
||||
// wikirawupload://upload.wikimedia.org/wikipedia/commons/3/3e/Einstein_1921.jpg
|
||||
// Get an image for the graph, e.g. from commons
|
||||
// This tag specifies any content from the uploads.* domain, without query params
|
||||
this._validateExternalService(urlParts, sanitizedHost, opt.url);
|
||||
urlParts.query = {};
|
||||
// keep urlParts.pathname
|
||||
break;
|
||||
|
||||
case 'wikidatasparql:':
|
||||
// wikidatasparql:///?query=<QUERY>
|
||||
// Runs a SPARQL query, converting it to
|
||||
// https://query.wikidata.org/bigdata/namespace/wdq/sparql?format=json&query=...
|
||||
this._validateExternalService(urlParts, sanitizedHost, opt.url);
|
||||
if (!urlParts.query || !urlParts.query.query) {
|
||||
throw new Error('wikidatasparql: missing query parameter in: ' + opt.url);
|
||||
}
|
||||
// Only keep the "query" parameter
|
||||
urlParts.query = {query: urlParts.query.query};
|
||||
urlParts.pathname = '/bigdata/namespace/wdq/sparql';
|
||||
opt.headers = this.objExtender(opt.headers || {}, {'Accept': 'application/sparql-results+json'});
|
||||
break;
|
||||
|
||||
case 'geoshape:':
|
||||
case 'geoline:':
|
||||
// geoshape:///?ids=Q16,Q30 or geoshape:///?query=...
|
||||
// Get geoshapes data from OSM database by supplying Wikidata IDs
|
||||
// https://maps.wikimedia.org/shape?ids=Q16,Q30
|
||||
// 'geoline:' is an identical service, except that it returns lines instead of polygons
|
||||
this._validateExternalService(urlParts, sanitizedHost, opt.url, 'geoshape:');
|
||||
if (!urlParts.query || (!urlParts.query.ids && !urlParts.query.query)) {
|
||||
throw new Error(opt.graphProtocol + ' missing ids or query parameter in: ' + opt.url);
|
||||
}
|
||||
// the query object is not modified
|
||||
urlParts.pathname = '/' + removeColon(opt.graphProtocol);
|
||||
break;
|
||||
|
||||
case 'mapsnapshot:':
|
||||
// mapsnapshot:///?width=__&height=__&zoom=__&lat=__&lon=__ [&style=__]
|
||||
// Converts it into a snapshot image request for Kartotherian:
|
||||
// https://maps.wikimedia.org/img/{style},{zoom},{lat},{lon},{width}x{height}[@{scale}x].{format}
|
||||
// (scale will be set to 2, and format to png)
|
||||
if (!urlParts.query) {
|
||||
throw new Error('mapsnapshot: missing required parameters');
|
||||
}
|
||||
validate(urlParts, 'width', 1, 4096);
|
||||
validate(urlParts, 'height', 1, 4096);
|
||||
validate(urlParts, 'zoom', 0, 22);
|
||||
validate(urlParts, 'lat', -90, 90, true);
|
||||
validate(urlParts, 'lon', -180, 180, true);
|
||||
|
||||
var query = urlParts.query;
|
||||
if (query.style && !/^[-_0-9a-z]+$/.test(query.style)) {
|
||||
throw new Error('mapsnapshot: if style is given, it must be letters/numbers/dash/underscores only');
|
||||
}
|
||||
|
||||
// Uses the same configuration as geoshape service, so reuse settings
|
||||
this._validateExternalService(urlParts, sanitizedHost, opt.url, 'geoshape:');
|
||||
|
||||
urlParts.pathname = '/img/' + (query.style || 'osm-intl') + ',' + query.zoom + ',' +
|
||||
query.lat + ',' + query.lon + ',' + query.width + 'x' + query.height + '@2x.png';
|
||||
urlParts.query = {}; // deleting it would cause errors in mw.Uri()
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unknown protocol ' + opt.url);
|
||||
}
|
||||
}
|
||||
|
||||
return this.formatUrl(urlParts, opt);
|
||||
*/
|
||||
return opt.url;
|
||||
};
|
||||
|
||||
function validate(urlParts, name, min, max, isFloat) {
|
||||
var value = urlParts.query[name];
|
||||
if (value === undefined) {
|
||||
throw new Error(urlParts.protocol + ' parameter ' + name + ' is not set');
|
||||
}
|
||||
if (!(isFloat ? /^-?[0-9]+\.?[0-9]*$/ : /^-?[0-9]+$/).test(value)) {
|
||||
throw new Error(urlParts.protocol + ' parameter ' + name + ' is not a number');
|
||||
}
|
||||
value = isFloat ? parseFloat(value) : parseInt(value);
|
||||
if (value < min || value > max) {
|
||||
throw new Error(urlParts.protocol + ' parameter ' + name + ' is not valid');
|
||||
}
|
||||
}
|
||||
|
||||
VegaWrapper.prototype._validateExternalService = function _validateExternalService(urlParts, sanitizedHost, url, protocolOverride) {
|
||||
var protocol = protocolOverride || urlParts.protocol,
|
||||
domains = this._getProtocolDomains(protocol);
|
||||
if (!domains) {
|
||||
throw new Error(protocol + ': protocol is disabled: ' + url);
|
||||
}
|
||||
if (urlParts.isRelativeHost) {
|
||||
urlParts.host = domains[0];
|
||||
urlParts.protocol = this.sanitizeHost(urlParts.host).protocol;
|
||||
} else {
|
||||
urlParts.protocol = sanitizedHost.protocol;
|
||||
}
|
||||
if (!this.testHost(protocol, urlParts.host)) {
|
||||
throw new Error(protocol + ': URL must either be relative (' + protocol + '///...), or use one of the allowed hosts: ' + url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs post-processing of the data requested by the graph's spec
|
||||
*/
|
||||
VegaWrapper.prototype.dataParser = function dataParser(error, data, opt, callback) {
|
||||
if (!error) {
|
||||
try {
|
||||
data = this.parseDataOrThrow(data, opt);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
if (error) data = undefined;
|
||||
callback(error, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the response from MW Api, throwing an error or logging warnings
|
||||
*/
|
||||
VegaWrapper.prototype.parseMWApiResponse = function parseMWApiResponse(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data.error) {
|
||||
throw new Error('API error: ' + JSON.stringify(data.error));
|
||||
}
|
||||
if (data.warnings) {
|
||||
this.logger('API warnings: ' + JSON.stringify(data.warnings));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs post-processing of the data requested by the graph's spec, and throw on error
|
||||
*/
|
||||
VegaWrapper.prototype.parseDataOrThrow = function parseDataOrThrow(data, opt) {
|
||||
switch (opt.xowa_protocol) { // XOWA: use opt.xowa_protocol
|
||||
case 'wikiapi:':
|
||||
data = this.parseMWApiResponse(data);
|
||||
break;
|
||||
case 'wikiraw:':
|
||||
// XOWA: comment out call to MW API; Xograph will make call to xo_server; DATE:2018-03-13
|
||||
/*
|
||||
data = this.parseMWApiResponse(data);
|
||||
try {
|
||||
data = data.query.pages[0].revisions[0].content;
|
||||
} catch (e) {
|
||||
throw new Error('Page content not available ' + opt.url);
|
||||
}
|
||||
*/
|
||||
break;
|
||||
case null:
|
||||
case 'tabular:':
|
||||
case 'map:':
|
||||
data = this.parseMWApiResponse(data).jsondata;
|
||||
var metadata = [{
|
||||
description: data.description,
|
||||
license_code: data.license.code,
|
||||
license_text: data.license.text,
|
||||
license_url: data.license.url,
|
||||
sources: data.sources
|
||||
}];
|
||||
if (opt.xowa_protocol === 'tabular:') {
|
||||
var fields = data.schema.fields.map(function (v) {
|
||||
return v.name;
|
||||
});
|
||||
data = {
|
||||
meta: metadata,
|
||||
fields: data.schema.fields,
|
||||
data: data.data.map(function (v) {
|
||||
var row = {}, i;
|
||||
for (i = 0; i < fields.length; i++) {
|
||||
// Need to copy nulls too -- Vega has no easy way to test for undefined
|
||||
row[fields[i]] = v[i];
|
||||
}
|
||||
return row;
|
||||
})
|
||||
}
|
||||
} else {
|
||||
metadata[0].zoom = data.zoom;
|
||||
metadata[0].latitude = data.latitude;
|
||||
metadata[0].longitude = data.longitude;
|
||||
data = {
|
||||
meta: metadata,
|
||||
data: data.data
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'wikidatasparql:':
|
||||
data = JSON.parse(data);
|
||||
if (!data.results || !Array.isArray(data.results.bindings)) {
|
||||
throw new Error('SPARQL query result does not have "results.bindings"');
|
||||
}
|
||||
data = data.results.bindings.map(function (row) {
|
||||
var key, result = {};
|
||||
for (key in row) {
|
||||
if (row.hasOwnProperty(key)) {
|
||||
result[key] = parseWikidataValue(row[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Throw an error when called
|
||||
*/
|
||||
function alwaysFail() {
|
||||
throw new Error('Disabled');
|
||||
}
|
||||
|
||||
},{"domain-validator":2,"wd-type-parser":4}],4:[function(require,module,exports){
|
||||
'use strict';
|
||||
/* global module */
|
||||
|
||||
module.exports = parseWikidataValue;
|
||||
|
||||
/**
|
||||
* Given a value object as returned from Wikidata Query Service, returns a simplified value
|
||||
* @param {object} value Original object as sent by the Wikidata query service
|
||||
* @param {string} value.type SPARQL data type (literal, uri)
|
||||
* @param {string} value.datatype XMLSchema data type
|
||||
* @param {*} value.value The actual value sent by the Wikidata query service
|
||||
* @param {boolean=} ignoreUnknown if false, will return value.value even if it cannot be recognized
|
||||
* @return {*}
|
||||
*/
|
||||
function parseWikidataValue(value, ignoreUnknown) {
|
||||
var temp;
|
||||
|
||||
if (!value || !value.type || value.value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (value.type) {
|
||||
case 'literal':
|
||||
switch (value.datatype) {
|
||||
case 'http://www.w3.org/2001/XMLSchema#double':
|
||||
case 'http://www.w3.org/2001/XMLSchema#float':
|
||||
case 'http://www.w3.org/2001/XMLSchema#decimal':
|
||||
case 'http://www.w3.org/2001/XMLSchema#integer':
|
||||
case 'http://www.w3.org/2001/XMLSchema#long':
|
||||
case 'http://www.w3.org/2001/XMLSchema#int':
|
||||
case 'http://www.w3.org/2001/XMLSchema#short':
|
||||
case 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger':
|
||||
case 'http://www.w3.org/2001/XMLSchema#positiveInteger':
|
||||
case 'http://www.w3.org/2001/XMLSchema#unsignedLong':
|
||||
case 'http://www.w3.org/2001/XMLSchema#unsignedInt':
|
||||
case 'http://www.w3.org/2001/XMLSchema#unsignedShort':
|
||||
case 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger':
|
||||
case 'http://www.w3.org/2001/XMLSchema#negativeInteger':
|
||||
temp = parseFloat(value.value);
|
||||
if (temp.toString() === value.value) {
|
||||
// use number only if it is fully round-tripable back to string
|
||||
// TBD: this might be overcautios, and would cause more problems than solve
|
||||
return temp;
|
||||
}
|
||||
break;
|
||||
case 'http://www.opengis.net/ont/geosparql#wktLiteral':
|
||||
// Point(-64.2 -36.62) -- (longitude latitude)
|
||||
temp = /^Point\(([-0-9.]+) ([-0-9.]+)\)$/.exec(value.value);
|
||||
if (temp) {
|
||||
return [parseFloat(temp[1]), parseFloat(temp[2])];
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'uri':
|
||||
// "http://www.wikidata.org/entity/Q12345" -> "Q12345"
|
||||
temp = /^http:\/\/www\.wikidata\.org\/entity\/(Q[1-9][0-9]*)$/.exec(value.value);
|
||||
if (temp) {
|
||||
return temp[1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
return ignoreUnknown ? undefined : value.value;
|
||||
}
|
||||
|
||||
|
||||
},{}]},{},[1]);
|
||||
@@ -0,0 +1,57 @@
|
||||
/*!
|
||||
* StyleSheet for JQuery splitter Plugin
|
||||
* Copyright (C) 2010 Jakub Jankiewicz <http://jcubic.pl>
|
||||
*
|
||||
* Same license as plugin
|
||||
*/
|
||||
.splitter_panel {
|
||||
position: relative;
|
||||
}
|
||||
.splitter_panel .vsplitter {
|
||||
background-color: grey;
|
||||
cursor: col-resize;
|
||||
z-index:900;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.splitter_panel .hsplitter {
|
||||
background-color: #5F5F5F;
|
||||
cursor: row-resize;
|
||||
z-index: 800;
|
||||
height: 7px;
|
||||
}
|
||||
.splitter_panel .vsplitter.splitter-invisible,
|
||||
.splitter_panel .hsplitter.splitter-invisible {
|
||||
background: none;
|
||||
}
|
||||
.splitter_panel .vsplitter, .splitter_panel .left_panel, .splitter_panel .right_panel,
|
||||
.splitter_panel .hsplitter, .splitter_panel .top_panel, .splitter_panel .bottom_panel {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
}
|
||||
.splitter_panel .vsplitter, .splitter_panel .left_panel, .splitter_panel .right_panel {
|
||||
height: 100%;
|
||||
}
|
||||
.splitter_panel .hsplitter, .splitter_panel .top_panel, .splitter_panel .bottom_panel {
|
||||
width: 100%;
|
||||
}
|
||||
.splitter_panel .top_panel, .splitter_panel .left_panel, .splitter_panel .vsplitter {
|
||||
top: 0;
|
||||
}
|
||||
.splitter_panel .top_panel, .splitter_panel .bottom_panel, .splitter_panel .left_panel, .splitter_panel .hsplitter {
|
||||
left: 0;
|
||||
}
|
||||
.splitter_panel .bottom_panel {
|
||||
bottom: 0;
|
||||
}
|
||||
.splitter_panel .right_panel {
|
||||
right: 0;
|
||||
}
|
||||
.splitterMask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*!
|
||||
* JQuery Spliter Plugin version 0.20.1
|
||||
* Copyright (C) 2010-2016 Jakub Jankiewicz <http://jcubic.pl>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function($, undefined) {
|
||||
var count = 0;
|
||||
var splitter_id = null;
|
||||
var splitters = [];
|
||||
var current_splitter = null;
|
||||
$.fn.split = function(options) {
|
||||
var data = this.data('splitter');
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
var panel_1;
|
||||
var panel_2;
|
||||
var settings = $.extend({
|
||||
limit: 100,
|
||||
orientation: 'horizontal',
|
||||
position: '50%',
|
||||
invisible: false,
|
||||
onDragStart: $.noop,
|
||||
onDragEnd: $.noop,
|
||||
onDrag: $.noop
|
||||
}, options || {});
|
||||
this.settings = settings;
|
||||
var cls;
|
||||
var children = this.children();
|
||||
if (settings.orientation == 'vertical') {
|
||||
panel_1 = children.first().addClass('left_panel');
|
||||
panel_2 = panel_1.next().addClass('right_panel');
|
||||
cls = 'vsplitter';
|
||||
} else if (settings.orientation == 'horizontal') {
|
||||
panel_1 = children.first().addClass('top_panel');
|
||||
panel_2 = panel_1.next().addClass('bottom_panel');
|
||||
cls = 'hsplitter';
|
||||
}
|
||||
if (settings.invisible) {
|
||||
cls += ' splitter-invisible';
|
||||
}
|
||||
var width = this.width();
|
||||
var height = this.height();
|
||||
var id = count++;
|
||||
this.addClass('splitter_panel');
|
||||
var splitter = $('<div/>').addClass(cls).bind('mouseenter touchstart', function() {
|
||||
splitter_id = id;
|
||||
}).bind('mouseleave touchend', function() {
|
||||
splitter_id = null;
|
||||
}).insertAfter(panel_1);
|
||||
var position;
|
||||
|
||||
function get_position(position) {
|
||||
if (typeof position === 'number') {
|
||||
return position;
|
||||
} else if (typeof position === 'string') {
|
||||
var match = position.match(/^([0-9\.]+)(px|%)$/);
|
||||
if (match) {
|
||||
if (match[2] == 'px') {
|
||||
return +match[1];
|
||||
} else {
|
||||
if (settings.orientation == 'vertical') {
|
||||
return (width * +match[1]) / 100;
|
||||
} else if (settings.orientation == 'horizontal') {
|
||||
return (height * +match[1]) / 100;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//throw position + ' is invalid value';
|
||||
}
|
||||
} else {
|
||||
//throw 'position have invalid type';
|
||||
}
|
||||
}
|
||||
|
||||
var self = $.extend(this, {
|
||||
refresh: function() {
|
||||
var new_width = this.width();
|
||||
var new_height = this.height();
|
||||
if (width != new_width || height != new_height) {
|
||||
width = this.width();
|
||||
height = this.height();
|
||||
self.position(position);
|
||||
}
|
||||
},
|
||||
position: (function() {
|
||||
if (settings.orientation == 'vertical') {
|
||||
return function(n, silent) {
|
||||
if (n === undefined) {
|
||||
return position;
|
||||
} else {
|
||||
position = get_position(n);
|
||||
var sw = splitter.width();
|
||||
var sw2 = sw/2, pw;
|
||||
if (settings.invisible) {
|
||||
pw = panel_1.width(position).outerWidth();
|
||||
panel_2.width(self.width()-pw);
|
||||
splitter.css('left', pw-sw2);
|
||||
} else {
|
||||
pw = panel_1.width(position-sw2).outerWidth();
|
||||
panel_2.width(self.width()-pw-sw);
|
||||
splitter.css('left', pw);
|
||||
}
|
||||
}
|
||||
if (!silent) {
|
||||
self.trigger('splitter.resize');
|
||||
self.find('.splitter_panel').trigger('splitter.resize');
|
||||
}
|
||||
return self;
|
||||
};
|
||||
} else if (settings.orientation == 'horizontal') {
|
||||
return function(n, silent) {
|
||||
if (n === undefined) {
|
||||
return position;
|
||||
} else {
|
||||
position = get_position(n);
|
||||
var sw = splitter.height();
|
||||
var sw2 = sw/2, pw;
|
||||
if (settings.invisible) {
|
||||
pw = panel_1.height(position).outerHeight();
|
||||
panel_2.height(self.height()-pw);
|
||||
splitter.css('top', pw-sw2);
|
||||
} else {
|
||||
pw = panel_1.height(position-sw2).outerHeight();
|
||||
panel_2.height(self.height()-pw-sw);
|
||||
splitter.css('top', pw);
|
||||
}
|
||||
}
|
||||
if (!silent) {
|
||||
self.trigger('splitter.resize');
|
||||
self.find('.splitter_panel').trigger('splitter.resize');
|
||||
}
|
||||
return self;
|
||||
};
|
||||
} else {
|
||||
return $.noop;
|
||||
}
|
||||
})(),
|
||||
orientation: settings.orientation,
|
||||
limit: settings.limit,
|
||||
isActive: function() {
|
||||
return splitter_id === id;
|
||||
},
|
||||
destroy: function() {
|
||||
self.removeClass('splitter_panel');
|
||||
splitter.unbind('mouseenter');
|
||||
splitter.unbind('mouseleave');
|
||||
splitter.unbind('touchstart');
|
||||
splitter.unbind('touchmove');
|
||||
splitter.unbind('touchend');
|
||||
splitter.unbind('touchleave');
|
||||
splitter.unbind('touchcancel');
|
||||
if (settings.orientation == 'vertical') {
|
||||
panel_1.removeClass('left_panel');
|
||||
panel_2.removeClass('right_panel');
|
||||
} else if (settings.orientation == 'horizontal') {
|
||||
panel_1.removeClass('top_panel');
|
||||
panel_2.removeClass('bottom_panel');
|
||||
}
|
||||
self.unbind('splitter.resize');
|
||||
self.trigger('splitter.resize');
|
||||
self.find('.splitter_panel').trigger('splitter.resize');
|
||||
splitters[i] = null;
|
||||
count--;
|
||||
splitter.remove();
|
||||
self.removeData('splitter');
|
||||
var not_null = false;
|
||||
for (var i=splitters.length; i--;) {
|
||||
if (splitters[i] !== null) {
|
||||
not_null = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//remove document events when no splitters
|
||||
if (!not_null) {
|
||||
$(document.documentElement).unbind('.splitter');
|
||||
$(window).unbind('resize.splitter');
|
||||
splitters = [];
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
self.bind('splitter.resize', function(e) {
|
||||
var pos = self.position();
|
||||
if (self.orientation == 'vertical' &&
|
||||
pos > self.width()) {
|
||||
pos = self.width() - self.limit-1;
|
||||
} else if (self.orientation == 'horizontal' &&
|
||||
pos > self.height()) {
|
||||
pos = self.height() - self.limit-1;
|
||||
}
|
||||
if (pos < self.limit) {
|
||||
pos = self.limit + 1;
|
||||
}
|
||||
e.stopPropagation();
|
||||
self.position(pos, true);
|
||||
});
|
||||
//inital position of splitter
|
||||
var pos;
|
||||
if (settings.orientation == 'vertical') {
|
||||
if (pos > width-settings.limit) {
|
||||
pos = width-settings.limit;
|
||||
} else {
|
||||
pos = get_position(settings.position);
|
||||
}
|
||||
} else if (settings.orientation == 'horizontal') {
|
||||
//position = height/2;
|
||||
if (pos > height-settings.limit) {
|
||||
pos = height-settings.limit;
|
||||
} else {
|
||||
pos = get_position(settings.position);
|
||||
}
|
||||
}
|
||||
if (pos < settings.limit) {
|
||||
pos = settings.limit;
|
||||
}
|
||||
self.position(pos, true);
|
||||
if (splitters.length === 0) { // first time bind events to document
|
||||
$(window).bind('resize.splitter', function() {
|
||||
$.each(splitters, function(i, splitter) {
|
||||
if (splitter) {
|
||||
splitter.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
$(document.documentElement).on('mousedown.splitter touchstart.splitter', function(e) {
|
||||
if (splitter_id !== null) {
|
||||
current_splitter = splitters[splitter_id];
|
||||
setTimeout(function() {
|
||||
$('<div class="splitterMask"></div>').
|
||||
css('cursor', current_splitter.children().eq(1).css('cursor')).
|
||||
insertAfter(current_splitter);
|
||||
});
|
||||
current_splitter.settings.onDragStart(e);
|
||||
}
|
||||
}).bind('mouseup.splitter touchend.splitter touchleave.splitter touchcancel.splitter', function(e) {
|
||||
if (current_splitter) {
|
||||
setTimeout(function() {
|
||||
$('.splitterMask').remove();
|
||||
});
|
||||
current_splitter.settings.onDragEnd(e);
|
||||
current_splitter = null;
|
||||
}
|
||||
}).bind('mousemove.splitter touchmove.splitter', function(e) {
|
||||
if (current_splitter !== null) {
|
||||
var limit = current_splitter.limit;
|
||||
var offset = current_splitter.offset();
|
||||
if (current_splitter.orientation == 'vertical') {
|
||||
var pageX = e.pageX;
|
||||
if(e.originalEvent && e.originalEvent.changedTouches){
|
||||
pageX = e.originalEvent.changedTouches[0].pageX;
|
||||
}
|
||||
var x = pageX - offset.left;
|
||||
if (x <= current_splitter.limit) {
|
||||
x = current_splitter.limit + 1;
|
||||
} else if (x >= current_splitter.width() - limit) {
|
||||
x = current_splitter.width() - limit - 1;
|
||||
}
|
||||
if (x > current_splitter.limit &&
|
||||
x < current_splitter.width()-limit) {
|
||||
current_splitter.position(x, true);
|
||||
current_splitter.trigger('splitter.resize');
|
||||
current_splitter.find('.splitter_panel').
|
||||
trigger('splitter.resize');
|
||||
//e.preventDefault();
|
||||
}
|
||||
} else if (current_splitter.orientation == 'horizontal') {
|
||||
var pageY = e.pageY;
|
||||
if(e.originalEvent && e.originalEvent.changedTouches){
|
||||
pageY = e.originalEvent.changedTouches[0].pageY;
|
||||
}
|
||||
var y = pageY-offset.top;
|
||||
if (y <= current_splitter.limit) {
|
||||
y = current_splitter.limit + 1;
|
||||
} else if (y >= current_splitter.height() - limit) {
|
||||
y = current_splitter.height() - limit - 1;
|
||||
}
|
||||
if (y > current_splitter.limit &&
|
||||
y < current_splitter.height()-limit) {
|
||||
current_splitter.position(y, true);
|
||||
current_splitter.trigger('splitter.resize');
|
||||
current_splitter.find('.splitter_panel').
|
||||
trigger('splitter.resize');
|
||||
//e.preventDefault();
|
||||
}
|
||||
}
|
||||
current_splitter.settings.onDrag(e);
|
||||
}
|
||||
});//*/
|
||||
}
|
||||
splitters[id] = self;
|
||||
self.data('splitter', self);
|
||||
return self;
|
||||
};
|
||||
})(jQuery);
|
||||
11003
res/bin/any/xowa/xtns/Graph/lib/vega1/vega.js
Normal file
11003
res/bin/any/xowa/xtns/Graph/lib/vega1/vega.js
Normal file
File diff suppressed because one or more lines are too long
22092
res/bin/any/xowa/xtns/Graph/lib/vega2/vega.js
Normal file
22092
res/bin/any/xowa/xtns/Graph/lib/vega2/vega.js
Normal file
File diff suppressed because one or more lines are too long
55
res/bin/any/xowa/xtns/Graph/modules/graph-loader.js
Normal file
55
res/bin/any/xowa/xtns/Graph/modules/graph-loader.js
Normal file
@@ -0,0 +1,55 @@
|
||||
( function ( $, mw ) {
|
||||
|
||||
mw.hook( 'wikipage.content' ).add( function ( $content ) {
|
||||
|
||||
/**
|
||||
* Replace a graph image by the vega graph.
|
||||
*
|
||||
* If dependencies aren't loaded yet, they are loaded first
|
||||
* before rendering the graph.
|
||||
*
|
||||
* @param {jQuery} $el Graph container.
|
||||
*/
|
||||
function loadAndReplaceWithGraph( $el ) {
|
||||
// TODO, Performance BUG: loading vega and calling api should happen in parallel
|
||||
// Lazy loading dependencies
|
||||
mw.loader.using( 'ext.graph.vega2', function () {
|
||||
new mw.Api().get( {
|
||||
formatversion: 2,
|
||||
action: 'graph',
|
||||
title: mw.config.get( 'wgPageName' ),
|
||||
hash: $el.data( 'graphId' )
|
||||
} ).done( function ( data ) {
|
||||
mw.drawVegaGraph( $el[ 0 ], data.graph, function ( error ) {
|
||||
var $layover = $el.find( '.mw-graph-layover' );
|
||||
if ( !error ) {
|
||||
$el.find( 'img' ).remove();
|
||||
$layover.remove();
|
||||
} else {
|
||||
mw.log.warn( error );
|
||||
}
|
||||
$el.removeClass( 'mw-graph-interactable' );
|
||||
// TODO: handle error by showing some message
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
// Make graph containers clickable
|
||||
$content.find( '.mw-graph.mw-graph-interactable' ).on( 'click', function () {
|
||||
var $this = $( this ),
|
||||
$button = $this.find( '.mw-graph-switch' );
|
||||
|
||||
// Prevent multiple clicks
|
||||
$this.off( 'click' );
|
||||
|
||||
// Add a class to decorate loading
|
||||
$button.addClass( 'mw-graph-loading' );
|
||||
|
||||
// Replace the image with the graph
|
||||
loadAndReplaceWithGraph( $this );
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
}( jQuery, mediaWiki ) );
|
||||
76
res/bin/any/xowa/xtns/Graph/modules/graph.sandbox.js
Normal file
76
res/bin/any/xowa/xtns/Graph/modules/graph.sandbox.js
Normal file
@@ -0,0 +1,76 @@
|
||||
( function ( $, mw ) {
|
||||
var oldContent, ccw,
|
||||
resizeCodeEditor = $.noop;
|
||||
|
||||
$( function () {
|
||||
var viewportHeight = $( window ).height(),
|
||||
sandboxHeight = viewportHeight - 150,
|
||||
initialPosition = sandboxHeight - 100;
|
||||
$( '#mw-graph-sandbox' ).width( '100%' ).height( sandboxHeight ).split( {
|
||||
orientation: 'vertical',
|
||||
limit: 100,
|
||||
position: '40%'
|
||||
} );
|
||||
$( '#mw-graph-left' ).split( {
|
||||
orientation: 'horizontal',
|
||||
limit: 100,
|
||||
position: initialPosition
|
||||
} );
|
||||
} );
|
||||
|
||||
mw.hook( 'codeEditor.configure' ).add( function ( session ) {
|
||||
var $json = $( '#mw-graph-json' )[ 0 ],
|
||||
$graph = $( '.mw-graph' ),
|
||||
$graphEl = $graph[ 0 ],
|
||||
$rightPanel = $( '#mw-graph-right' ),
|
||||
$editor = $( '.editor' );
|
||||
|
||||
if ( ccw ) {
|
||||
ccw.release();
|
||||
}
|
||||
ccw = mw.confirmCloseWindow( {
|
||||
test: function () {
|
||||
return session.getValue().length > 0;
|
||||
},
|
||||
message: mw.msg( 'editwarning-warning' )
|
||||
} );
|
||||
|
||||
resizeCodeEditor = function () {
|
||||
$editor.parent().height( $rightPanel.height() - 57 );
|
||||
$.wikiEditor.instances[ 0 ].data( 'wikiEditor-context' ).codeEditor.resize();
|
||||
};
|
||||
|
||||
// I tried to resize on $( window ).resize(), but that didn't work right
|
||||
resizeCodeEditor();
|
||||
|
||||
session.on( 'change', $.debounce( 300, function () {
|
||||
var content = session.getValue();
|
||||
|
||||
if ( oldContent === content ) {
|
||||
return;
|
||||
}
|
||||
oldContent = content;
|
||||
$graph.empty();
|
||||
|
||||
new mw.Api().post( {
|
||||
formatversion: 2,
|
||||
action: 'graph',
|
||||
text: content
|
||||
} ).done( function ( data ) {
|
||||
if ( session.getValue() !== content ) {
|
||||
// Just in case the content has changed since we made the api call
|
||||
return;
|
||||
}
|
||||
$json.textContent = JSON.stringify( data.graph, null, 2 );
|
||||
mw.drawVegaGraph( $graphEl, data.graph, function ( error ) {
|
||||
if ( error ) {
|
||||
$graphEl.textContent = ( error.exception || error ).toString();
|
||||
}
|
||||
} );
|
||||
} ).fail( function ( errCode, error ) {
|
||||
$graphEl.textContent = errCode.toString() + ':' + ( error.exception || error ).toString();
|
||||
} );
|
||||
} ) );
|
||||
} );
|
||||
|
||||
}( jQuery, mediaWiki ) );
|
||||
27
res/bin/any/xowa/xtns/Graph/modules/graph1.js
Normal file
27
res/bin/any/xowa/xtns/Graph/modules/graph1.js
Normal file
@@ -0,0 +1,27 @@
|
||||
( function ( $, mw ) {
|
||||
// mw.hook( 'wikipage.content' ).add( function ( $content ) {
|
||||
var specs = {} // mw.config.get( 'wgGraphSpecs' );
|
||||
|
||||
vg.data.load.sanitizeUrl = function () {
|
||||
// mw.log.warn( 'Vega 1.x does not allow external URLs. Switch to Vega 2.' );
|
||||
return false;
|
||||
};
|
||||
|
||||
if ( specs ) {
|
||||
var $content = $('#content'); // XOWA
|
||||
$content.find( '.mw-graph.mw-graph-always' ).each( function () {
|
||||
var graphId = $( this ).data( 'graph-id' ),
|
||||
el = this;
|
||||
if ( !specs[ graphId ] ) {
|
||||
// mw.log.warn( graphId );
|
||||
} else {
|
||||
vg.parse.spec( specs[ graphId ], function ( chart ) {
|
||||
if ( chart ) {
|
||||
chart( { el: el } ).update();
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
// } );
|
||||
}( jQuery, null ) );
|
||||
107
res/bin/any/xowa/xtns/Graph/modules/graph2.js
Normal file
107
res/bin/any/xowa/xtns/Graph/modules/graph2.js
Normal file
@@ -0,0 +1,107 @@
|
||||
( function ( $, mw, vg ) {
|
||||
|
||||
'use strict';
|
||||
/* global require */
|
||||
|
||||
/*
|
||||
var VegaWrapper = require( 'mw-graph-shared' );
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new VegaWrapper( {
|
||||
datalib: vg.util,
|
||||
useXhr: true,
|
||||
isTrusted: true, //mw.config.get( 'wgGraphIsTrusted' ),
|
||||
domains: '', // mw.config.get( 'wgGraphAllowedDomains' ),
|
||||
domainMap: false,
|
||||
logger: function ( warning ) {
|
||||
// mw.log.warn( warning );
|
||||
},
|
||||
parseUrl: function ( opt ) {
|
||||
// Parse URL
|
||||
var uri = new mw.Uri( opt.url );
|
||||
// reduce confusion, only keep expected values
|
||||
if ( uri.port ) {
|
||||
uri.host += ':' + uri.port;
|
||||
delete uri.port;
|
||||
}
|
||||
// If url begins with protocol:///... mark it as having relative host
|
||||
if ( /^[a-z]+:\/\/\//.test( opt.url ) ) {
|
||||
uri.isRelativeHost = true;
|
||||
}
|
||||
if ( uri.protocol ) {
|
||||
// All other libs use trailing colon in the protocol field
|
||||
uri.protocol += ':';
|
||||
}
|
||||
// Node's path includes the query, whereas pathname is without the query
|
||||
// Standardizing on pathname
|
||||
uri.pathname = uri.path;
|
||||
delete uri.path;
|
||||
return uri;
|
||||
},
|
||||
formatUrl: function ( uri, opt ) {
|
||||
// Format URL back into a string
|
||||
// Revert path into pathname
|
||||
uri.path = uri.pathname;
|
||||
delete uri.pathname;
|
||||
|
||||
if ( location.host.toLowerCase() === uri.host.toLowerCase() ) {
|
||||
// if ( !mw.config.get( 'wgGraphIsTrusted' ) ) {
|
||||
// Only send this header when hostname is the same.
|
||||
// This is broader than the same-origin policy,
|
||||
// but playing on the safer side.
|
||||
// opt.headers = { 'Treat-as-Untrusted': 1 };
|
||||
// }
|
||||
} else if ( opt.addCorsOrigin ) {
|
||||
// All CORS api calls require origin parameter.
|
||||
// It would be better to use location.origin,
|
||||
// but apparently it's not universal yet.
|
||||
uri.query.origin = location.protocol + '//' + location.host;
|
||||
}
|
||||
|
||||
uri.protocol = VegaWrapper.removeColon( uri.protocol );
|
||||
|
||||
return uri.toString();
|
||||
},
|
||||
// languageCode: mw.config.get( 'wgUserLanguage' )
|
||||
} );
|
||||
*/
|
||||
/**
|
||||
* Set up drawing canvas inside the given element and draw graph data
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {Object|string} data graph spec
|
||||
* @param {Function} [callback] function(error) called when drawing is done
|
||||
*/
|
||||
window.drawVegaGraph = function ( element, data, callback ) {
|
||||
vg.parse.spec( data, function ( error, chart ) {
|
||||
if ( !error ) {
|
||||
chart( { el: element } ).update();
|
||||
}
|
||||
if ( callback ) {
|
||||
callback( error );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
// mw.hook( 'wikipage.content' ).add( function ( $content ) {
|
||||
var $content = $('#content');
|
||||
|
||||
var specs = {} // mw.config.get( 'wgGraphSpecs' );
|
||||
if ( !specs ) {
|
||||
return;
|
||||
}
|
||||
$content.find( '.mw-graph.mw-graph-always' ).each( function () {
|
||||
var graphId = $( this ).data( 'graph-id' );
|
||||
if ( !specs.hasOwnProperty( graphId ) ) {
|
||||
// mw.log.warn( graphId );
|
||||
} else {
|
||||
window.drawVegaGraph( this, specs[ graphId ], function ( error ) {
|
||||
if ( error ) {
|
||||
// mw.log.warn( error );
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
// } );
|
||||
|
||||
}( jQuery, null, vg ) );
|
||||
7
res/bin/any/xowa/xtns/Graph/modules/ve-graph/graph.svg
Normal file
7
res/bin/any/xowa/xtns/Graph/modules/ve-graph/graph.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g id="graph">
|
||||
<path id="axes" d="M5 6v12h14v-1H6V6H5z"/>
|
||||
<path id="data" d="M7 16v-3l4-3 3 2 4-4v8H7"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
@@ -0,0 +1,607 @@
|
||||
/*!
|
||||
* VisualEditor MWGraphNode tests.
|
||||
*/
|
||||
|
||||
QUnit.module( 'ext.graph.visualEditor' );
|
||||
|
||||
( function () {
|
||||
'use strict';
|
||||
|
||||
/* Sample specs */
|
||||
|
||||
var sampleSpecs = {
|
||||
areaGraph: {
|
||||
version: 2,
|
||||
width: 500,
|
||||
height: 200,
|
||||
padding: {
|
||||
top: 10,
|
||||
left: 30,
|
||||
bottom: 30,
|
||||
right: 10
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: 'table',
|
||||
values: [
|
||||
{ x: 0, y: 28 },
|
||||
{ x: 1, y: 43 },
|
||||
{ x: 2, y: 81 },
|
||||
{ x: 3, y: 19 }
|
||||
]
|
||||
}
|
||||
],
|
||||
scales: [
|
||||
{
|
||||
name: 'x',
|
||||
type: 'linear',
|
||||
range: 'width',
|
||||
zero: false,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'x'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: 'linear',
|
||||
range: 'height',
|
||||
nice: true,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'y'
|
||||
}
|
||||
}
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
type: 'x',
|
||||
scale: 'x'
|
||||
},
|
||||
{
|
||||
type: 'y',
|
||||
scale: 'y'
|
||||
}
|
||||
],
|
||||
marks: [
|
||||
{
|
||||
type: 'area',
|
||||
from: {
|
||||
data: 'table'
|
||||
},
|
||||
properties: {
|
||||
enter: {
|
||||
interpolate: {
|
||||
value: 'monotone'
|
||||
},
|
||||
x: {
|
||||
scale: 'x',
|
||||
field: 'x'
|
||||
},
|
||||
y: {
|
||||
scale: 'y',
|
||||
field: 'y'
|
||||
},
|
||||
y2: {
|
||||
scale: 'y',
|
||||
value: 0
|
||||
},
|
||||
fill: {
|
||||
value: 'steelblue'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
stackedAreaGraph: {
|
||||
version: 2,
|
||||
width: 500,
|
||||
height: 200,
|
||||
padding: {
|
||||
top: 10,
|
||||
left: 30,
|
||||
bottom: 30,
|
||||
right: 10
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: 'table',
|
||||
values: [
|
||||
{ x: 0, y: 28, c: 0 },
|
||||
{ x: 0, y: 55, c: 1 },
|
||||
{ x: 1, y: 43, c: 0 },
|
||||
{ x: 1, y: 91, c: 1 },
|
||||
{ x: 2, y: 81, c: 0 },
|
||||
{ x: 2, y: 53, c: 1 },
|
||||
{ x: 3, y: 19, c: 0 },
|
||||
{ x: 3, y: 87, c: 1 },
|
||||
{ x: 4, y: 52, c: 0 },
|
||||
{ x: 4, y: 48, c: 1 },
|
||||
{ x: 5, y: 24, c: 0 },
|
||||
{ x: 5, y: 49, c: 1 },
|
||||
{ x: 6, y: 87, c: 0 },
|
||||
{ x: 6, y: 66, c: 1 },
|
||||
{ x: 7, y: 17, c: 0 },
|
||||
{ x: 7, y: 27, c: 1 },
|
||||
{ x: 8, y: 68, c: 0 },
|
||||
{ x: 8, y: 16, c: 1 },
|
||||
{ x: 9, y: 49, c: 0 },
|
||||
{ x: 9, y: 15, c: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
source: 'table',
|
||||
transform: [
|
||||
{
|
||||
type: 'facet',
|
||||
keys: [
|
||||
'x'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'stats',
|
||||
value: 'y'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
scales: [
|
||||
{
|
||||
name: 'x',
|
||||
type: 'linear',
|
||||
range: 'width',
|
||||
zero: false,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'x'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: 'linear',
|
||||
range: 'height',
|
||||
nice: true,
|
||||
domain: {
|
||||
data: 'stats',
|
||||
field: 'sum'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
type: 'ordinal',
|
||||
range: 'category10'
|
||||
}
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
type: 'x',
|
||||
scale: 'x'
|
||||
},
|
||||
{
|
||||
type: 'y',
|
||||
scale: 'y'
|
||||
}
|
||||
],
|
||||
marks: [
|
||||
{
|
||||
type: 'group',
|
||||
from: {
|
||||
data: 'table',
|
||||
transform: [
|
||||
{
|
||||
type: 'facet',
|
||||
keys: [
|
||||
'c'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'stack',
|
||||
point: 'x',
|
||||
height: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
marks: [
|
||||
{
|
||||
type: 'area',
|
||||
properties: {
|
||||
enter: {
|
||||
interpolate: {
|
||||
value: 'monotone'
|
||||
},
|
||||
x: {
|
||||
scale: 'x',
|
||||
field: 'x'
|
||||
},
|
||||
y: {
|
||||
scale: 'y',
|
||||
field: 'y'
|
||||
},
|
||||
y2: {
|
||||
scale: 'y',
|
||||
field: 'y2'
|
||||
},
|
||||
fill: {
|
||||
scale: 'color',
|
||||
field: 'c'
|
||||
}
|
||||
},
|
||||
update: {
|
||||
fillOpacity: {
|
||||
value: 1
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
fillOpacity: {
|
||||
value: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
invalidAxesBarGraph: {
|
||||
version: 2,
|
||||
width: 500,
|
||||
height: 200,
|
||||
padding: {
|
||||
top: 10,
|
||||
left: 30,
|
||||
bottom: 30,
|
||||
right: 10
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: 'table',
|
||||
values: [
|
||||
{ x: 0, y: 28 },
|
||||
{ x: 1, y: 43 },
|
||||
{ x: 2, y: 81 },
|
||||
{ x: 3, y: 19 }
|
||||
]
|
||||
}
|
||||
],
|
||||
scales: [
|
||||
{
|
||||
name: 'x',
|
||||
type: 'linear',
|
||||
range: 'width',
|
||||
zero: false,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'x'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: 'linear',
|
||||
range: 'height',
|
||||
nice: true,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'y'
|
||||
}
|
||||
}
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
type: 'x',
|
||||
scale: 'z'
|
||||
},
|
||||
{
|
||||
type: 'y',
|
||||
scale: 'y'
|
||||
}
|
||||
],
|
||||
marks: [
|
||||
{
|
||||
type: 'area',
|
||||
from: {
|
||||
data: 'table'
|
||||
},
|
||||
properties: {
|
||||
enter: {
|
||||
interpolate: {
|
||||
value: 'monotone'
|
||||
},
|
||||
x: {
|
||||
scale: 'x',
|
||||
field: 'x'
|
||||
},
|
||||
y: {
|
||||
scale: 'y',
|
||||
field: 'y'
|
||||
},
|
||||
y2: {
|
||||
scale: 'y',
|
||||
value: 0
|
||||
},
|
||||
fill: {
|
||||
value: 'steelblue'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/* Tests */
|
||||
|
||||
QUnit.test( 've.dm.MWGraphNode', function ( assert ) {
|
||||
var node = new ve.dm.MWGraphNode(),
|
||||
specString = JSON.stringify( sampleSpecs.areaGraph );
|
||||
|
||||
assert.deepEqual( node.getSpec(), ve.dm.MWGraphNode.static.defaultSpec, 'MWGraphNode spec is initialized to the default spec' );
|
||||
|
||||
node.setSpecFromString( specString );
|
||||
assert.deepEqual( node.getSpec(), sampleSpecs.areaGraph, 'Basic valid spec is parsed' );
|
||||
|
||||
node.setSpecFromString( 'invalid JSON string' );
|
||||
assert.deepEqual( node.getSpec(), {}, 'Setting an invalid JSON resets the spec to an empty object' );
|
||||
|
||||
node.setSpec( sampleSpecs.stackedAreaGraph );
|
||||
assert.deepEqual( node.getSpec(), sampleSpecs.stackedAreaGraph, 'Setting the spec by object' );
|
||||
|
||||
node.setSpec( null );
|
||||
assert.deepEqual( node.getSpec(), {}, 'Setting a null spec resets the spec to an empty object' );
|
||||
} );
|
||||
|
||||
QUnit.test( 've.ce.MWGraphNode', function ( assert ) {
|
||||
var view = ve.test.utils.createSurfaceViewFromHtml(
|
||||
'<div typeof="mw:Extension/graph"></div>'
|
||||
),
|
||||
documentNode = view.getDocument().getDocumentNode(),
|
||||
node = documentNode.children[ 0 ];
|
||||
|
||||
assert.equal( node.type, 'mwGraph', 'Parsoid HTML graphs are properly recognized as graph nodes' );
|
||||
} );
|
||||
|
||||
QUnit.test( 've.ce.MWGraphNode.static', function ( assert ) {
|
||||
var testElement = document.createElement( 'div' ),
|
||||
renderValidTest = assert.async(),
|
||||
renderInvalidTest = assert.async();
|
||||
|
||||
$( '#qunit-fixture' ).append( testElement );
|
||||
|
||||
ve.ce.MWGraphNode.static.vegaParseSpec( sampleSpecs.areaGraph, testElement ).always(
|
||||
function () {
|
||||
assert.ok( this.state() === 'resolved', 'Simple graph gets rendered correctly' );
|
||||
renderValidTest();
|
||||
}
|
||||
);
|
||||
|
||||
ve.ce.MWGraphNode.static.vegaParseSpec( sampleSpecs.invalidAxesBarGraph, testElement ).always(
|
||||
function ( failMessageKey ) {
|
||||
assert.ok( failMessageKey === 'graph-ve-vega-error', 'Invalid graph triggers an error at rendering' );
|
||||
renderInvalidTest();
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 've.dm.MWGraphModel', function ( assert ) {
|
||||
var model = new ve.dm.MWGraphModel( sampleSpecs.areaGraph ),
|
||||
updateSpecRemoval = {
|
||||
marks: undefined,
|
||||
scales: undefined,
|
||||
padding: { top: 50 },
|
||||
axes: [
|
||||
{ type: 'z' }
|
||||
]
|
||||
},
|
||||
areaGraphRemovalExpected = {
|
||||
version: 2,
|
||||
width: 500,
|
||||
height: 200,
|
||||
padding: {
|
||||
top: 50,
|
||||
left: 30,
|
||||
bottom: 30,
|
||||
right: 10
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: 'table',
|
||||
values: [
|
||||
{ x: 0, y: 28 },
|
||||
{ x: 1, y: 43 },
|
||||
{ x: 2, y: 81 },
|
||||
{ x: 3, y: 19 }
|
||||
]
|
||||
}
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
type: 'z',
|
||||
scale: 'x'
|
||||
},
|
||||
{
|
||||
type: 'y',
|
||||
scale: 'y'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
assert.equal( model.hasBeenChanged(), false, 'Model changes are correctly initialized' );
|
||||
|
||||
model.setSpecFromString( 'invalid json string' );
|
||||
assert.equal( model.hasBeenChanged(), true, 'Model spec resets to an empty object when fed invalid data' );
|
||||
|
||||
model.setSpecFromString( JSON.stringify( sampleSpecs.areaGraph, null, '\t' ) );
|
||||
assert.equal( model.hasBeenChanged(), false, 'Model doesn\'t throw false positives after applying no changes' );
|
||||
|
||||
model.setSpecFromString( JSON.stringify( sampleSpecs.stackedAreaGraph ) );
|
||||
assert.equal( model.hasBeenChanged(), true, 'Model recognizes valid changes to spec' );
|
||||
|
||||
model.setSpecFromString( JSON.stringify( sampleSpecs.areaGraph ) );
|
||||
model.updateSpec( updateSpecRemoval );
|
||||
assert.deepEqual( model.getSpec(), areaGraphRemovalExpected, 'Updating the spec and removing properties' );
|
||||
} );
|
||||
|
||||
QUnit.test( 've.dm.MWGraphModel.static', function ( assert ) {
|
||||
var result,
|
||||
basicTestObj = {
|
||||
a: 3,
|
||||
b: undefined,
|
||||
c: {
|
||||
ca: undefined,
|
||||
cb: 'undefined'
|
||||
}
|
||||
},
|
||||
complexTestObj = {
|
||||
a: {
|
||||
aa: undefined,
|
||||
ab: 3,
|
||||
ac: [
|
||||
{
|
||||
ac0a: undefined,
|
||||
ac0b: 4
|
||||
},
|
||||
{
|
||||
ac1a: 'ac1a',
|
||||
ac1b: 5,
|
||||
ac1c: undefined
|
||||
}
|
||||
]
|
||||
},
|
||||
b: {
|
||||
a: undefined,
|
||||
b: undefined,
|
||||
c: 2
|
||||
},
|
||||
c: 3,
|
||||
d: undefined
|
||||
},
|
||||
undefinedPropertiesBasicExpected = [ 'b', 'c.ca' ],
|
||||
undefinedPropertiesComplexExpected = [ 'a.aa', 'a.ac.0.ac0a', 'a.ac.1.ac1c', 'b.a', 'b.b', 'd' ],
|
||||
removePropBasicExpected = {
|
||||
a: 3,
|
||||
b: undefined,
|
||||
c: {
|
||||
cb: 'undefined'
|
||||
}
|
||||
},
|
||||
removePropComplexExpected = {
|
||||
a: {
|
||||
aa: undefined,
|
||||
ab: 3,
|
||||
ac: [ {
|
||||
ac1b: 5,
|
||||
ac1c: undefined
|
||||
} ]
|
||||
},
|
||||
c: 3,
|
||||
d: undefined
|
||||
};
|
||||
|
||||
result = ve.dm.MWGraphModel.static.getUndefinedProperties( basicTestObj );
|
||||
assert.deepEqual( result, undefinedPropertiesBasicExpected, 'Basic deep undefined property scan is successful' );
|
||||
|
||||
result = ve.dm.MWGraphModel.static.getUndefinedProperties( complexTestObj );
|
||||
assert.deepEqual( result, undefinedPropertiesComplexExpected, 'Complex deep undefined property scan is successful' );
|
||||
|
||||
result = ve.dm.MWGraphModel.static.removeProperty( basicTestObj, [ 'c', 'ca' ] );
|
||||
assert.deepEqual( basicTestObj, removePropBasicExpected, 'Basic nested property removal is successful' );
|
||||
|
||||
ve.dm.MWGraphModel.static.removeProperty( complexTestObj, [ 'a', 'ac', '0' ] );
|
||||
ve.dm.MWGraphModel.static.removeProperty( complexTestObj, [ 'a', 'ac', '0', 'ac1a' ] );
|
||||
ve.dm.MWGraphModel.static.removeProperty( complexTestObj, [ 'b' ] );
|
||||
assert.deepEqual( complexTestObj, removePropComplexExpected, 'Complex nested property removal is successful' );
|
||||
|
||||
assert.throws(
|
||||
ve.dm.MWGraphModel.static.removeProperty( complexTestObj, [ 'b' ] ),
|
||||
'Trying to delete an invalid property throws an error'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 've.ui.TableWidget', function ( assert ) {
|
||||
var widgetA = new ve.ui.TableWidget( {
|
||||
rows: [
|
||||
{
|
||||
key: 'foo',
|
||||
label: 'Foo'
|
||||
},
|
||||
{
|
||||
key: 'bar',
|
||||
label: 'Bar'
|
||||
}
|
||||
],
|
||||
cols: [
|
||||
{
|
||||
label: 'A'
|
||||
},
|
||||
{
|
||||
label: 'B'
|
||||
}
|
||||
]
|
||||
} ),
|
||||
widgetB = new ve.ui.TableWidget( {
|
||||
rows: [
|
||||
{
|
||||
key: 'foo'
|
||||
},
|
||||
{
|
||||
key: 'bar'
|
||||
}
|
||||
],
|
||||
cols: [
|
||||
{}, {}, {}
|
||||
],
|
||||
data: [
|
||||
[ '11', '12', '13' ],
|
||||
[ '21', '22', '23' ]
|
||||
]
|
||||
} ),
|
||||
widgetAexpectedRowProps = {
|
||||
index: 1,
|
||||
key: 'bar',
|
||||
label: 'Bar'
|
||||
},
|
||||
widgetAexpectedInitialData = [
|
||||
[ '', '' ],
|
||||
[ '', '' ]
|
||||
],
|
||||
widgetAexpectedDataAfterValue = [
|
||||
[ '', '' ],
|
||||
[ '3', '' ]
|
||||
],
|
||||
widgetAexpectedDataAfterRowColumnInsertions = [
|
||||
[ '', '', '' ],
|
||||
[ 'a', 'b', 'c' ],
|
||||
[ '3', '', '' ]
|
||||
],
|
||||
widgetAexpectedDataAfterColumnRemoval = [
|
||||
[ '', '' ],
|
||||
[ 'b', 'c' ],
|
||||
[ '', '' ]
|
||||
];
|
||||
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedInitialData, 'Table data is initialized properly' );
|
||||
|
||||
assert.deepEqual( widgetA.model.getRowProperties( 'bar' ), widgetAexpectedRowProps, 'Row props are returned successfully' );
|
||||
|
||||
widgetA.setValue( 'bar', 0, '3' );
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterValue, 'Table data is modified successfully' );
|
||||
|
||||
widgetA.insertColumn();
|
||||
widgetA.insertRow( [ 'a', 'b', 'c' ], 1, 'baz', 'Baz' );
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterRowColumnInsertions, 'Row and column are added successfully' );
|
||||
|
||||
widgetA.removeColumn( 0 );
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Columns are removed successfully' );
|
||||
|
||||
widgetA.removeRow( -1 );
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Invalid row removal by index does not change table data' );
|
||||
|
||||
widgetA.removeRow( 'qux' );
|
||||
assert.deepEqual( widgetA.model.data, widgetAexpectedDataAfterColumnRemoval, 'Invalid row removal by key does not change table data' );
|
||||
|
||||
assert.deepEqual( widgetB.getItems()[ 0 ].getItems()[ 2 ].getValue(), '13', 'Initial data is populated in text inputs properly' );
|
||||
} );
|
||||
}() );
|
||||
@@ -0,0 +1,9 @@
|
||||
.mw-graph {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ve-ce-mwGraphNode-plot {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*!
|
||||
* VisualEditor ContentEditable MWGraphNode class.
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* ContentEditable MediaWiki graph node.
|
||||
*
|
||||
* @class
|
||||
* @extends ve.ce.MWBlockExtensionNode
|
||||
* @mixins ve.ce.MWResizableNode
|
||||
*
|
||||
* @constructor
|
||||
* @param {ve.dm.MWGraphNode} model Model to observe
|
||||
* @param {Object} [config] Configuration options
|
||||
*/
|
||||
ve.ce.MWGraphNode = function VeCeMWGraphNode( model, config ) {
|
||||
this.$graph = $( '<div>' ).addClass( 'mw-graph' );
|
||||
this.$plot = $( '<div>' ).addClass( 've-ce-mwGraphNode-plot' );
|
||||
|
||||
// Parent constructor
|
||||
ve.ce.MWGraphNode.super.apply( this, arguments );
|
||||
|
||||
// Mixin constructors
|
||||
ve.ce.MWResizableNode.call( this, this.$plot, config );
|
||||
|
||||
this.$element
|
||||
.addClass( 'mw-graph-container' )
|
||||
.append( this.$graph );
|
||||
|
||||
this.showHandles( [ 'se' ] );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ce.MWGraphNode, ve.ce.MWBlockExtensionNode );
|
||||
|
||||
// Need to mix in the base class as well
|
||||
OO.mixinClass( ve.ce.MWGraphNode, ve.ce.ResizableNode );
|
||||
|
||||
OO.mixinClass( ve.ce.MWGraphNode, ve.ce.MWResizableNode );
|
||||
|
||||
/* Static Properties */
|
||||
|
||||
ve.ce.MWGraphNode.static.name = 'mwGraph';
|
||||
|
||||
ve.ce.MWGraphNode.static.primaryCommandName = 'graph';
|
||||
|
||||
ve.ce.MWGraphNode.static.tagName = 'div';
|
||||
|
||||
/* Static Methods */
|
||||
|
||||
/**
|
||||
* Attempt to render the graph through Vega.
|
||||
*
|
||||
* @param {Object} spec The graph spec
|
||||
* @param {HTMLElement} element Element to render the graph in
|
||||
* @return {jQuery.Promise} Promise that resolves when the graph is rendered.
|
||||
* Promise is rejected with an error message key if there was a problem rendering the graph.
|
||||
*/
|
||||
ve.ce.MWGraphNode.static.vegaParseSpec = function ( spec, element ) {
|
||||
var deferred = $.Deferred(),
|
||||
node = this,
|
||||
canvasNode, view;
|
||||
|
||||
// Check if the spec is currently valid
|
||||
if ( ve.isEmptyObject( spec ) ) {
|
||||
deferred.reject( 'graph-ve-no-spec' );
|
||||
} else if ( !ve.dm.MWGraphModel.static.specHasData( spec ) ) {
|
||||
deferred.reject( 'graph-ve-empty-graph' );
|
||||
} else {
|
||||
vg.parse.spec( spec, function ( chart ) {
|
||||
try {
|
||||
view = chart( { el: element } ).update();
|
||||
|
||||
// HACK: If canvas is blank, this means Vega didn't render properly.
|
||||
// Once Vega allows for proper rendering validation, this should be
|
||||
// swapped for a validation check.
|
||||
canvasNode = element.children[ 0 ].children[ 0 ];
|
||||
if ( node.isCanvasBlank( canvasNode ) ) {
|
||||
deferred.reject( 'graph-ve-vega-error-no-render' );
|
||||
} else {
|
||||
deferred.resolve( view );
|
||||
}
|
||||
|
||||
} catch ( err ) {
|
||||
deferred.reject( 'graph-ve-vega-error' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a canvas is blank
|
||||
*
|
||||
* @author Austin Brunkhorst http://stackoverflow.com/a/17386803/2055594
|
||||
* @param {HTMLElement} canvas The canvas to Check
|
||||
* @return {boolean} The canvas is blank
|
||||
*/
|
||||
ve.ce.MWGraphNode.static.isCanvasBlank = function ( canvas ) {
|
||||
var blank = document.createElement( 'canvas' );
|
||||
|
||||
blank.width = canvas.width;
|
||||
blank.height = canvas.height;
|
||||
|
||||
return canvas.toDataURL() === blank.toDataURL();
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Render a Vega graph inside the node
|
||||
*/
|
||||
ve.ce.MWGraphNode.prototype.update = function () {
|
||||
var node = this;
|
||||
|
||||
// Clear element
|
||||
this.$graph.empty();
|
||||
|
||||
this.$element.toggleClass( 'mw-graph-vega1', this.getModel().isGraphLegacy() );
|
||||
|
||||
mw.loader.using( 'ext.graph.vega2' ).done( function () {
|
||||
node.$plot.detach();
|
||||
|
||||
node.constructor.static.vegaParseSpec( node.getModel().getSpec(), node.$graph[ 0 ] ).then(
|
||||
function ( view ) {
|
||||
// HACK: We need to know which padding values Vega computes in case
|
||||
// of automatic padding, but it isn't properly exposed in the view
|
||||
node.$graph.append( node.$plot );
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
node.$plot.css( view._padding );
|
||||
|
||||
node.calculateHighlights();
|
||||
},
|
||||
function ( failMessageKey ) {
|
||||
node.$graph.text( ve.msg( failMessageKey ) );
|
||||
}
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ce.MWGraphNode.prototype.getAttributeChanges = function ( width, height ) {
|
||||
var attrChanges = {},
|
||||
newSpec = ve.dm.MWGraphModel.static.updateSpec( this.getModel().getSpec(), {
|
||||
width: width,
|
||||
height: height
|
||||
} );
|
||||
|
||||
ve.setProp( attrChanges, 'mw', 'body', 'extsrc', JSON.stringify( newSpec ) );
|
||||
|
||||
return attrChanges;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ce.MWGraphNode.prototype.getFocusableElement = function () {
|
||||
return this.$graph;
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.ce.nodeFactory.register( ve.ce.MWGraphNode );
|
||||
@@ -0,0 +1,526 @@
|
||||
/*!
|
||||
* VisualEditor DataModel MWGraphModel class.
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* MediaWiki graph model.
|
||||
*
|
||||
* @class
|
||||
* @mixins OO.EventEmitter
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [spec] The Vega specification as a JSON object
|
||||
*/
|
||||
ve.dm.MWGraphModel = function VeDmMWGraphModel( spec ) {
|
||||
// Mixin constructors
|
||||
OO.EventEmitter.call( this );
|
||||
|
||||
// Properties
|
||||
this.spec = spec || {};
|
||||
this.originalSpec = ve.copy( this.spec );
|
||||
|
||||
this.cachedPadding = ve.copy( this.spec.padding ) || this.getDefaultPaddingObject();
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.mixinClass( ve.dm.MWGraphModel, OO.EventEmitter );
|
||||
|
||||
/* Static Members */
|
||||
|
||||
ve.dm.MWGraphModel.static.defaultPadding = 30;
|
||||
|
||||
ve.dm.MWGraphModel.static.minDimensions = {
|
||||
width: 60,
|
||||
height: 60
|
||||
};
|
||||
|
||||
ve.dm.MWGraphModel.static.graphConfigs = {
|
||||
area: {
|
||||
mark: {
|
||||
type: 'area',
|
||||
properties: {
|
||||
enter: {
|
||||
fill: { value: 'steelblue' },
|
||||
interpolate: { value: 'monotone' },
|
||||
stroke: undefined,
|
||||
strokeWidth: undefined,
|
||||
width: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
scale: {
|
||||
name: 'x',
|
||||
type: 'linear'
|
||||
},
|
||||
fields: [
|
||||
'x',
|
||||
'y'
|
||||
]
|
||||
},
|
||||
|
||||
bar: {
|
||||
mark: {
|
||||
type: 'rect',
|
||||
properties: {
|
||||
enter: {
|
||||
fill: { value: 'steelblue' },
|
||||
interpolate: undefined,
|
||||
stroke: undefined,
|
||||
strokeWidth: undefined,
|
||||
// HACK: Boolean values set to true need to be wrapped
|
||||
// in strings until T118883 is resolved
|
||||
width: { scale: 'x', band: 'true', offset: -1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
scale: {
|
||||
name: 'x',
|
||||
type: 'ordinal'
|
||||
},
|
||||
fields: [
|
||||
'x',
|
||||
'y'
|
||||
]
|
||||
},
|
||||
|
||||
line: {
|
||||
mark: {
|
||||
type: 'line',
|
||||
properties: {
|
||||
enter: {
|
||||
fill: undefined,
|
||||
interpolate: { value: 'monotone' },
|
||||
stroke: { value: 'steelblue' },
|
||||
strokeWidth: { value: 3 },
|
||||
width: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
scale: {
|
||||
name: 'x',
|
||||
type: 'linear'
|
||||
},
|
||||
fields: [
|
||||
'x',
|
||||
'y'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/* Events */
|
||||
|
||||
/**
|
||||
* @event specChange
|
||||
*
|
||||
* Change when the JSON specification is updated
|
||||
*
|
||||
* @param {Object} The new specification
|
||||
*/
|
||||
|
||||
/* Static Methods */
|
||||
|
||||
/**
|
||||
* Updates a spec with new parameters.
|
||||
*
|
||||
* @param {Object} spec The spec to update
|
||||
* @param {Object} params The new params to update. Properties set to undefined will be removed from the spec.
|
||||
* @return {Object} The new spec
|
||||
*/
|
||||
ve.dm.MWGraphModel.static.updateSpec = function ( spec, params ) {
|
||||
var undefinedProperty,
|
||||
undefinedProperties = ve.dm.MWGraphModel.static.getUndefinedProperties( params ),
|
||||
i;
|
||||
|
||||
// Remove undefined properties from spec
|
||||
for ( i = 0; i < undefinedProperties.length; i++ ) {
|
||||
undefinedProperty = undefinedProperties[ i ].split( '.' );
|
||||
ve.dm.MWGraphModel.static.removeProperty( spec, $.extend( [], undefinedProperty ) );
|
||||
ve.dm.MWGraphModel.static.removeProperty( params, $.extend( [], undefinedProperty ) );
|
||||
}
|
||||
|
||||
// Extend remaining properties
|
||||
spec = $.extend( true, {}, spec, params );
|
||||
|
||||
return spec;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively gets all the keys to properties set to undefined in a JSON object
|
||||
*
|
||||
* @author Based on the work on Artyom Neustroev at http://stackoverflow.com/a/15690816/2055594
|
||||
* @private
|
||||
* @param {Object} obj The object to iterate
|
||||
* @param {string} [stack] The parent property of the root property of obj. Used internally for recursion.
|
||||
* @param {string[]} [list] The list of properties to return. Used internally for recursion.
|
||||
* @return {string[]} The list of properties to return.
|
||||
*/
|
||||
ve.dm.MWGraphModel.static.getUndefinedProperties = function ( obj, stack, list ) {
|
||||
var property;
|
||||
|
||||
list = list || [];
|
||||
|
||||
// Append . to the stack if it's defined
|
||||
stack = ( stack === undefined ) ? '' : stack + '.';
|
||||
|
||||
for ( property in obj ) {
|
||||
if ( obj.hasOwnProperty( property ) ) {
|
||||
if ( $.type( obj[ property ] ) === 'object' || $.type( obj[ property ] ) === 'array' ) {
|
||||
ve.dm.MWGraphModel.static.getUndefinedProperties( obj[ property ], stack + property, list );
|
||||
} else if ( obj[ property ] === undefined ) {
|
||||
list.push( stack + property );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a nested property from an object
|
||||
*
|
||||
* @param {Object} obj The object
|
||||
* @param {Array} prop The path of the property to remove
|
||||
*/
|
||||
ve.dm.MWGraphModel.static.removeProperty = function ( obj, prop ) {
|
||||
var firstProp = prop.shift();
|
||||
|
||||
try {
|
||||
if ( prop.length > 0 ) {
|
||||
ve.dm.MWGraphModel.static.removeProperty( obj[ firstProp ], prop );
|
||||
} else {
|
||||
if ( $.type( obj ) === 'array' ) {
|
||||
obj.splice( parseInt( firstProp ), 1 );
|
||||
} else {
|
||||
delete obj[ firstProp ];
|
||||
}
|
||||
}
|
||||
} catch ( err ) {
|
||||
// We don't need to bubble errors here since hitting a missing property
|
||||
// will not exist anyway in the object anyway
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a spec currently has something in its dataset
|
||||
*
|
||||
* @param {Object} spec The spec
|
||||
* @return {boolean} The spec has some data in its dataset
|
||||
*/
|
||||
ve.dm.MWGraphModel.static.specHasData = function ( spec ) {
|
||||
// FIXME: Support multiple pipelines
|
||||
return !!spec.data[ 0 ].values.length;
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Switch the graph to a different type
|
||||
*
|
||||
* @param {string} type Desired graph type. Can be either area, line or bar.
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.switchGraphType = function ( type ) {
|
||||
var params = {
|
||||
scales: [ ve.copy( this.constructor.static.graphConfigs[ type ].scale ) ],
|
||||
marks: [ ve.copy( this.constructor.static.graphConfigs[ type ].mark ) ]
|
||||
};
|
||||
|
||||
this.updateSpec( params );
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply changes to the node
|
||||
*
|
||||
* @param {ve.dm.MWGraphNode} node The node to be modified
|
||||
* @param {ve.dm.Surface} surfaceModel The surface model for the document
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.applyChanges = function ( node, surfaceModel ) {
|
||||
var mwData = ve.copy( node.getAttribute( 'mw' ) );
|
||||
|
||||
// Send transaction
|
||||
mwData.body.extsrc = this.getSpecString();
|
||||
surfaceModel.change(
|
||||
ve.dm.TransactionBuilder.static.newFromAttributeChanges(
|
||||
surfaceModel.getDocument(),
|
||||
node.getOffset(),
|
||||
{ mw: mwData }
|
||||
)
|
||||
);
|
||||
surfaceModel.applyStaging();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the spec with new parameters
|
||||
*
|
||||
* @param {Object} params The new parameters to be updated in the spec
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.updateSpec = function ( params ) {
|
||||
var updatedSpec = ve.dm.MWGraphModel.static.updateSpec( $.extend( true, {}, this.spec ), params );
|
||||
|
||||
// Only emit a change event if the spec really changed
|
||||
if ( !OO.compare( this.spec, updatedSpec ) ) {
|
||||
this.spec = updatedSpec;
|
||||
this.emit( 'specChange', this.spec );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets and validates the specification from a stringified version
|
||||
*
|
||||
* @param {string} str The new specification string
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setSpecFromString = function ( str ) {
|
||||
var newSpec = ve.dm.MWGraphNode.static.parseSpecString( str );
|
||||
|
||||
// Only apply changes if the new spec is valid JSON and if the
|
||||
// spec truly was modified
|
||||
if ( !OO.compare( this.spec, newSpec ) ) {
|
||||
this.spec = newSpec;
|
||||
this.emit( 'specChange', this.spec );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the specification
|
||||
*
|
||||
* @return {Object} The specification
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getSpec = function () {
|
||||
return this.spec;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the stringified specification
|
||||
*
|
||||
* @return {string} The specification string
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getSpecString = function () {
|
||||
return ve.dm.MWGraphNode.static.stringifySpec( this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the original stringified specificiation
|
||||
*
|
||||
* @return {string} The original JSON string specification
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getOriginalSpecString = function () {
|
||||
return ve.dm.MWGraphNode.static.stringifySpec( this.originalSpec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the graph type
|
||||
*
|
||||
* @return {string} The graph type
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getGraphType = function () {
|
||||
var markType = this.spec.marks[ 0 ].type;
|
||||
|
||||
switch ( markType ) {
|
||||
case 'area':
|
||||
return 'area';
|
||||
case 'rect':
|
||||
return 'bar';
|
||||
case 'line':
|
||||
return 'line';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get graph size
|
||||
*
|
||||
* @return {Object} The graph width and height
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getSize = function () {
|
||||
return {
|
||||
width: this.spec.width,
|
||||
height: this.spec.height
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the graph width
|
||||
*
|
||||
* @param {number} value The new width
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setWidth = function ( value ) {
|
||||
this.spec.width = value;
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the graph height
|
||||
*
|
||||
* @param {number} value The new height
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setHeight = function ( value ) {
|
||||
this.spec.height = value;
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the padding values of the graph
|
||||
*
|
||||
* @return {Object} The paddings
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getPaddingObject = function () {
|
||||
return this.spec.padding;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the default padding
|
||||
*
|
||||
* @return {Object} The default padding values
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getDefaultPaddingObject = function () {
|
||||
var i,
|
||||
indexes = [ 'top', 'bottom', 'left', 'right' ],
|
||||
paddingObj = {};
|
||||
|
||||
for ( i = 0; i < indexes.length; i++ ) {
|
||||
paddingObj[ indexes[ i ] ] = ve.dm.MWGraphModel.static.defaultPadding;
|
||||
}
|
||||
|
||||
return paddingObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a padding value
|
||||
*
|
||||
* @param {string} index The index to change. Can be either top, right, bottom or right
|
||||
* @param {number} value The new value
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setPadding = function ( index, value ) {
|
||||
if ( this.isPaddingAutomatic() ) {
|
||||
this.spec.padding = this.getDefaultPaddingObject();
|
||||
}
|
||||
|
||||
this.spec.padding[ index ] = value;
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles automatic and manual padding modes
|
||||
*
|
||||
* @param {boolean} auto Padding is now automatic
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setPaddingAuto = function ( auto ) {
|
||||
if ( auto ) {
|
||||
this.cachedPadding = ve.copy( this.spec.padding ) || this.getDefaultPaddingObject();
|
||||
ve.dm.MWGraphModel.static.removeProperty( this.spec, [ 'padding' ] );
|
||||
} else {
|
||||
this.spec.padding = ve.copy( this.cachedPadding );
|
||||
}
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the fields for a data pipeline
|
||||
*
|
||||
* @param {number} [id] The pipeline's id
|
||||
* @return {string[]} The fields for the pipeline
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getPipelineFields = function ( id ) {
|
||||
var firstEntry = ve.getProp( this.spec, 'data', id, 'values', 0 );
|
||||
|
||||
// Get the fields directly from the pipeline data if the pipeline exists and
|
||||
// has data, otherwise default back on the fields intended for this graph type
|
||||
if ( firstEntry ) {
|
||||
return Object.keys( firstEntry );
|
||||
} else {
|
||||
return ve.dm.MWGraphModel.static.graphConfigs[ this.getGraphType() ].fields;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a data pipeline
|
||||
*
|
||||
* @param {number} [id] The pipeline's id
|
||||
* @return {Object} The data pipeline within the spec
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.getPipeline = function ( id ) {
|
||||
return this.spec.data[ id ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the field value of an entry in a pipeline
|
||||
*
|
||||
* @param {number} [entry] ID of the entry
|
||||
* @param {string} [field] The field to change
|
||||
* @param {number} [value] The new value
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.setEntryField = function ( entry, field, value ) {
|
||||
if ( this.spec.data[ 0 ].values[ entry ] === undefined ) {
|
||||
this.spec.data[ 0 ].values[ entry ] = this.buildNewEntry( 0 );
|
||||
}
|
||||
this.spec.data[ 0 ].values[ entry ][ field ] = value;
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds and returns a new entry for a pipeline
|
||||
*
|
||||
* @private
|
||||
* @param {number} [pipelineId] The ID of the pipeline the entry is intended for
|
||||
* @return {Object} The new entry
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.buildNewEntry = function ( pipelineId ) {
|
||||
var fields = this.getPipelineFields( pipelineId ),
|
||||
newEntry = {},
|
||||
i;
|
||||
|
||||
for ( i = 0; i < fields.length; i++ ) {
|
||||
newEntry[ fields[ i ] ] = '';
|
||||
}
|
||||
|
||||
return newEntry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an entry from a pipeline
|
||||
*
|
||||
* @param {number} [index] The index of the entry to delete
|
||||
* @fires specChange
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.removeEntry = function ( index ) {
|
||||
// FIXME: Support multiple pipelines
|
||||
this.spec.data[ 0 ].values.splice( index, 1 );
|
||||
|
||||
this.emit( 'specChange', this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the current spec has been modified since the dialog was opened
|
||||
*
|
||||
* @return {boolean} The spec was changed
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.hasBeenChanged = function () {
|
||||
return !OO.compare( this.spec, this.originalSpec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the padding is set to be automatic or not
|
||||
*
|
||||
* @return {boolean} The padding is automatic
|
||||
*/
|
||||
ve.dm.MWGraphModel.prototype.isPaddingAutomatic = function () {
|
||||
return OO.compare( this.spec.padding, undefined );
|
||||
};
|
||||
@@ -0,0 +1,264 @@
|
||||
/*!
|
||||
* VisualEditor DataModel MWGraphNode class.
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* DataModel MediaWiki graph node.
|
||||
*
|
||||
* @class
|
||||
* @extends ve.dm.MWBlockExtensionNode
|
||||
* @mixins ve.dm.ResizableNode
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [element]
|
||||
*/
|
||||
ve.dm.MWGraphNode = function VeDmMWGraphNode() {
|
||||
var mw, extsrc;
|
||||
|
||||
// Parent constructor
|
||||
ve.dm.MWGraphNode.super.apply( this, arguments );
|
||||
|
||||
// Mixin constructors
|
||||
ve.dm.ResizableNode.call( this );
|
||||
|
||||
// Properties
|
||||
this.spec = null;
|
||||
|
||||
// Events
|
||||
this.connect( this, {
|
||||
attributeChange: 'onAttributeChange'
|
||||
} );
|
||||
|
||||
// Initialize specificiation
|
||||
mw = this.getAttribute( 'mw' );
|
||||
extsrc = ve.getProp( mw, 'body', 'extsrc' );
|
||||
|
||||
if ( extsrc ) {
|
||||
this.setSpecFromString( extsrc );
|
||||
} else {
|
||||
this.setSpec( ve.dm.MWGraphNode.static.defaultSpec );
|
||||
}
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.dm.MWGraphNode, ve.dm.MWBlockExtensionNode );
|
||||
OO.mixinClass( ve.dm.MWGraphNode, ve.dm.ResizableNode );
|
||||
|
||||
/* Static Members */
|
||||
|
||||
ve.dm.MWGraphNode.static.name = 'mwGraph';
|
||||
|
||||
ve.dm.MWGraphNode.static.extensionName = 'graph';
|
||||
|
||||
ve.dm.MWGraphNode.static.defaultSpec = {
|
||||
version: 2,
|
||||
width: 400,
|
||||
height: 200,
|
||||
data: [
|
||||
{
|
||||
name: 'table',
|
||||
values: [
|
||||
{
|
||||
x: 0,
|
||||
y: 1
|
||||
},
|
||||
{
|
||||
x: 1,
|
||||
y: 3
|
||||
},
|
||||
{
|
||||
x: 2,
|
||||
y: 2
|
||||
},
|
||||
{
|
||||
x: 3,
|
||||
y: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
scales: [
|
||||
{
|
||||
name: 'x',
|
||||
type: 'linear',
|
||||
range: 'width',
|
||||
zero: false,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'x'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: 'linear',
|
||||
range: 'height',
|
||||
nice: true,
|
||||
domain: {
|
||||
data: 'table',
|
||||
field: 'y'
|
||||
}
|
||||
}
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
type: 'x',
|
||||
scale: 'x'
|
||||
},
|
||||
{
|
||||
type: 'y',
|
||||
scale: 'y'
|
||||
}
|
||||
],
|
||||
marks: [
|
||||
{
|
||||
type: 'area',
|
||||
from: {
|
||||
data: 'table'
|
||||
},
|
||||
properties: {
|
||||
enter: {
|
||||
x: {
|
||||
scale: 'x',
|
||||
field: 'x'
|
||||
},
|
||||
y: {
|
||||
scale: 'y',
|
||||
field: 'y'
|
||||
},
|
||||
y2: {
|
||||
scale: 'y',
|
||||
value: 0
|
||||
},
|
||||
fill: {
|
||||
value: 'steelblue'
|
||||
},
|
||||
interpolate: {
|
||||
value: 'monotone'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/* Static Methods */
|
||||
|
||||
/**
|
||||
* Parses a spec string and returns its object representation.
|
||||
*
|
||||
* @param {string} str The spec string to validate. If the string is null or represents an empty object, the spec will be null.
|
||||
* @return {Object} The object specification. On a failed parsing, the object will be returned empty.
|
||||
*/
|
||||
ve.dm.MWGraphNode.static.parseSpecString = function ( str ) {
|
||||
var result;
|
||||
|
||||
try {
|
||||
result = JSON.parse( str );
|
||||
|
||||
// JSON.parse can return other types than Object, we don't want that
|
||||
// The error will be caught just below as this counts as a failed process
|
||||
if ( $.type( result ) !== 'object' ) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch ( err ) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the indented string representation of a spec.
|
||||
*
|
||||
* @param {Object} spec The object specificiation.
|
||||
* @return {string} The stringified version of the spec.
|
||||
*/
|
||||
ve.dm.MWGraphNode.static.stringifySpec = function ( spec ) {
|
||||
var result = JSON.stringify( spec, null, '\t' );
|
||||
return result || '';
|
||||
};
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.createScalable = function () {
|
||||
var width = ve.getProp( this.spec, 'width' ),
|
||||
height = ve.getProp( this.spec, 'height' );
|
||||
|
||||
return new ve.dm.Scalable( {
|
||||
currentDimensions: {
|
||||
width: width,
|
||||
height: height
|
||||
},
|
||||
minDimensions: ve.dm.MWGraphModel.static.minDimensions,
|
||||
fixedRatio: false
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the specification string
|
||||
*
|
||||
* @return {string} The specification JSON string
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.getSpecString = function () {
|
||||
return this.constructor.static.stringifySpec( this.spec );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the parsed JSON specification
|
||||
*
|
||||
* @return {Object} The specification object
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.getSpec = function () {
|
||||
return this.spec;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the specificiation
|
||||
*
|
||||
* @param {Object} spec The new spec
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.setSpec = function ( spec ) {
|
||||
// Consolidate all falsy values to an empty object for consistency
|
||||
this.spec = spec || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the specification from a stringified version
|
||||
*
|
||||
* @param {string} str The new specification JSON string
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.setSpecFromString = function ( str ) {
|
||||
this.setSpec( this.constructor.static.parseSpecString( str ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* React to node attribute changes
|
||||
*
|
||||
* @param {string} attributeName The attribute being updated
|
||||
* @param {Object} from The old value of the attribute
|
||||
* @param {Object} to The new value of the attribute
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.onAttributeChange = function ( attributeName, from, to ) {
|
||||
if ( attributeName === 'mw' ) {
|
||||
this.setSpecFromString( to.body.extsrc );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Is this graph using a legacy version of Vega?
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
ve.dm.MWGraphNode.prototype.isGraphLegacy = function () {
|
||||
return !!( this.spec && this.spec.hasOwnProperty( 'version' ) && this.spec.version < 2 );
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.dm.modelRegistry.register( ve.dm.MWGraphNode );
|
||||
@@ -0,0 +1,609 @@
|
||||
/*!
|
||||
* VisualEditor UserInterface MWGraphDialog class.
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* MediaWiki graph dialog.
|
||||
*
|
||||
* @class
|
||||
* @extends ve.ui.MWExtensionDialog
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [element]
|
||||
*/
|
||||
ve.ui.MWGraphDialog = function VeUiMWGraphDialog() {
|
||||
// Parent constructor
|
||||
ve.ui.MWGraphDialog.super.apply( this, arguments );
|
||||
|
||||
// Properties
|
||||
this.graphModel = null;
|
||||
this.mode = '';
|
||||
this.cachedRawData = null;
|
||||
this.listeningToInputChanges = true;
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.MWGraphDialog, ve.ui.MWExtensionDialog );
|
||||
|
||||
/* Static properties */
|
||||
|
||||
ve.ui.MWGraphDialog.static.name = 'graph';
|
||||
|
||||
ve.ui.MWGraphDialog.static.title = OO.ui.deferMsg( 'graph-ve-dialog-edit-title' );
|
||||
|
||||
ve.ui.MWGraphDialog.static.size = 'large';
|
||||
|
||||
ve.ui.MWGraphDialog.static.actions = [
|
||||
{
|
||||
action: 'done',
|
||||
label: OO.ui.deferMsg( 'visualeditor-dialog-action-done' ),
|
||||
flags: [ 'progressive', 'primary' ],
|
||||
modes: 'edit'
|
||||
},
|
||||
{
|
||||
action: 'done',
|
||||
label: OO.ui.deferMsg( 'visualeditor-dialog-action-insert' ),
|
||||
flags: [ 'constructive', 'primary' ],
|
||||
modes: 'insert'
|
||||
},
|
||||
{
|
||||
label: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
|
||||
flags: 'safe',
|
||||
modes: [ 'edit', 'insert' ]
|
||||
}
|
||||
];
|
||||
|
||||
ve.ui.MWGraphDialog.static.modelClasses = [ ve.dm.MWGraphNode ];
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.getBodyHeight = function () {
|
||||
// FIXME: This should depend on the dialog's content.
|
||||
return 500;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.initialize = function () {
|
||||
var graphTypeField,
|
||||
sizeFieldset,
|
||||
paddingAutoField, paddingFieldset,
|
||||
jsonTextField;
|
||||
|
||||
// Parent method
|
||||
ve.ui.MWGraphDialog.super.prototype.initialize.call( this );
|
||||
|
||||
/* Root layout */
|
||||
this.rootLayout = new OO.ui.BookletLayout( {
|
||||
classes: [ 've-ui-mwGraphDialog-panel-root' ],
|
||||
outlined: true
|
||||
} );
|
||||
|
||||
this.generalPage = new OO.ui.PageLayout( 'general' );
|
||||
this.dataPage = new OO.ui.PageLayout( 'data' );
|
||||
this.rawPage = new OO.ui.PageLayout( 'raw' );
|
||||
|
||||
this.rootLayout.addPages( [
|
||||
this.generalPage, this.dataPage, this.rawPage
|
||||
] );
|
||||
|
||||
/* General page */
|
||||
this.generalPage.getOutlineItem()
|
||||
.setIcon( 'parameter' )
|
||||
.setLabel( ve.msg( 'graph-ve-dialog-edit-page-general' ) );
|
||||
|
||||
this.graphTypeDropdownInput = new OO.ui.DropdownInputWidget();
|
||||
|
||||
graphTypeField = new OO.ui.FieldLayout( this.graphTypeDropdownInput, {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-field-graph-type' )
|
||||
} );
|
||||
|
||||
this.unknownGraphTypeWarningLabel = new OO.ui.LabelWidget( {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-unknown-graph-type-warning' )
|
||||
} );
|
||||
|
||||
this.sizeWidget = new ve.ui.MediaSizeWidget( null, {
|
||||
noDefaultDimensions: true,
|
||||
noOriginalDimensions: true
|
||||
} );
|
||||
|
||||
sizeFieldset = new OO.ui.FieldsetLayout( {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-size-fieldset' )
|
||||
} );
|
||||
|
||||
sizeFieldset.addItems( [ this.sizeWidget ] );
|
||||
|
||||
this.paddingAutoCheckbox = new OO.ui.CheckboxInputWidget( {
|
||||
value: 'paddingAuto'
|
||||
} );
|
||||
|
||||
paddingAutoField = new OO.ui.FieldLayout( this.paddingAutoCheckbox, {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-auto' )
|
||||
} );
|
||||
|
||||
this.paddingTable = new ve.ui.TableWidget( {
|
||||
rows: [
|
||||
{
|
||||
key: 'top',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-table-top' )
|
||||
},
|
||||
{
|
||||
key: 'bottom',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-table-bottom' )
|
||||
},
|
||||
{
|
||||
key: 'left',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-table-left' )
|
||||
},
|
||||
{
|
||||
key: 'right',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-table-right' )
|
||||
}
|
||||
],
|
||||
cols: [
|
||||
{
|
||||
key: 'value'
|
||||
}
|
||||
],
|
||||
validate: /^[0-9]+$/,
|
||||
showHeaders: false,
|
||||
allowRowInsertion: false,
|
||||
allowRowDeletion: false
|
||||
} );
|
||||
|
||||
paddingFieldset = new OO.ui.FieldsetLayout( {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-padding-fieldset' )
|
||||
} );
|
||||
|
||||
paddingFieldset.addItems( [
|
||||
paddingAutoField,
|
||||
this.paddingTable
|
||||
] );
|
||||
|
||||
this.generalPage.$element.append(
|
||||
graphTypeField.$element,
|
||||
this.unknownGraphTypeWarningLabel.$element,
|
||||
sizeFieldset.$element,
|
||||
paddingFieldset.$element
|
||||
);
|
||||
|
||||
/* Data page */
|
||||
this.dataPage.getOutlineItem()
|
||||
.setIcon( 'parameter' )
|
||||
.setLabel( ve.msg( 'graph-ve-dialog-edit-page-data' ) );
|
||||
|
||||
this.dataTable = new ve.ui.TableWidget( {
|
||||
validate: /^[0-9]+$/,
|
||||
showRowLabels: false
|
||||
} );
|
||||
|
||||
this.dataPage.$element.append( this.dataTable.$element );
|
||||
|
||||
/* Raw JSON page */
|
||||
this.rawPage.getOutlineItem()
|
||||
.setIcon( 'code' )
|
||||
.setLabel( ve.msg( 'graph-ve-dialog-edit-page-raw' ) );
|
||||
|
||||
this.jsonTextInput = new ve.ui.MWAceEditorWidget( {
|
||||
autosize: true,
|
||||
classes: [ 've-ui-mwGraphDialog-json' ],
|
||||
maxRows: 22,
|
||||
validate: this.validateRawData
|
||||
} );
|
||||
|
||||
// Make sure JSON is LTR
|
||||
this.jsonTextInput
|
||||
.setLanguage( 'json' )
|
||||
.toggleLineNumbers( false )
|
||||
.setDir( 'ltr' );
|
||||
|
||||
jsonTextField = new OO.ui.FieldLayout( this.jsonTextInput, {
|
||||
label: ve.msg( 'graph-ve-dialog-edit-field-raw-json' ),
|
||||
align: 'top'
|
||||
} );
|
||||
|
||||
this.rawPage.$element.append( jsonTextField.$element );
|
||||
|
||||
// Events
|
||||
this.rootLayout.connect( this, { set: 'onRootLayoutSet' } );
|
||||
|
||||
this.graphTypeDropdownInput.connect( this, { change: 'onGraphTypeInputChange' } );
|
||||
this.sizeWidget.connect( this, { change: 'onSizeWidgetChange' } );
|
||||
this.paddingAutoCheckbox.connect( this, { change: 'onPaddingAutoCheckboxChange' } );
|
||||
this.paddingTable.connect( this, {
|
||||
change: 'onPaddingTableChange'
|
||||
} );
|
||||
|
||||
this.dataTable.connect( this, {
|
||||
change: 'onDataInputChange',
|
||||
removeRow: 'onDataInputRowDelete'
|
||||
} );
|
||||
|
||||
this.jsonTextInput.connect( this, { change: 'onSpecStringInputChange' } );
|
||||
|
||||
// Initialization
|
||||
this.$body.append( this.rootLayout.$element );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.getSetupProcess = function ( data ) {
|
||||
return ve.ui.MWGraphDialog.super.prototype.getSetupProcess.call( this, data )
|
||||
.next( function () {
|
||||
var spec, newElement;
|
||||
|
||||
this.getFragment().getSurface().pushStaging();
|
||||
|
||||
// Create new graph node if not present (insert mode)
|
||||
if ( !this.selectedNode ) {
|
||||
this.setMode( 'insert' );
|
||||
|
||||
newElement = this.getNewElement();
|
||||
this.fragment = this.getFragment().insertContent( [
|
||||
newElement,
|
||||
{ type: '/' + newElement.type }
|
||||
] );
|
||||
this.getFragment().select();
|
||||
this.selectedNode = this.getFragment().getSelectedNode();
|
||||
} else {
|
||||
this.setMode( 'edit' );
|
||||
}
|
||||
|
||||
// Set up model
|
||||
spec = ve.copy( this.selectedNode.getSpec() );
|
||||
|
||||
this.graphModel = new ve.dm.MWGraphModel( spec );
|
||||
this.graphModel.connect( this, {
|
||||
specChange: 'onSpecChange'
|
||||
} );
|
||||
|
||||
// Set up default values
|
||||
this.setupFormValues();
|
||||
|
||||
// If parsing fails here, cached raw data can simply remain null
|
||||
try {
|
||||
this.cachedRawData = JSON.parse( this.jsonTextInput.getValue() );
|
||||
} catch ( err ) {}
|
||||
|
||||
this.checkChanges();
|
||||
}, this );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.getTeardownProcess = function ( data ) {
|
||||
return ve.ui.MWGraphDialog.super.prototype.getTeardownProcess.call( this, data )
|
||||
.first( function () {
|
||||
// Kill model
|
||||
this.graphModel.disconnect( this );
|
||||
|
||||
this.graphModel = null;
|
||||
|
||||
// Clear data page
|
||||
this.dataTable.clearWithProperties();
|
||||
|
||||
// Kill staging
|
||||
if ( data === undefined ) {
|
||||
this.getFragment().getSurface().popStaging();
|
||||
this.getFragment().update( this.getFragment().getSurface().getSelection() );
|
||||
}
|
||||
}, this );
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.getActionProcess = function ( action ) {
|
||||
switch ( action ) {
|
||||
case 'done':
|
||||
return new OO.ui.Process( function () {
|
||||
this.graphModel.applyChanges( this.selectedNode, this.getFragment().getSurface() );
|
||||
this.close( { action: action } );
|
||||
}, this );
|
||||
|
||||
default:
|
||||
return ve.ui.MWGraphDialog.super.prototype.getActionProcess.call( this, action );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup initial values in the dialog
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.setupFormValues = function () {
|
||||
var graphType = this.graphModel.getGraphType(),
|
||||
graphSize = this.graphModel.getSize(),
|
||||
paddings = this.graphModel.getPaddingObject(),
|
||||
options = [
|
||||
{
|
||||
data: 'bar',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-type-bar' )
|
||||
},
|
||||
{
|
||||
data: 'area',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-type-area' )
|
||||
},
|
||||
{
|
||||
data: 'line',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-type-line' )
|
||||
}
|
||||
],
|
||||
unknownGraphTypeOption = {
|
||||
data: 'unknown',
|
||||
label: ve.msg( 'graph-ve-dialog-edit-type-unknown' )
|
||||
},
|
||||
dataFields = this.graphModel.getPipelineFields( 0 ),
|
||||
padding, i;
|
||||
|
||||
// Graph type
|
||||
if ( graphType === 'unknown' ) {
|
||||
options.push( unknownGraphTypeOption );
|
||||
}
|
||||
|
||||
this.graphTypeDropdownInput
|
||||
.setOptions( options )
|
||||
.setValue( graphType );
|
||||
|
||||
// Size
|
||||
this.sizeWidget.setScalable( new ve.dm.Scalable( {
|
||||
currentDimensions: {
|
||||
width: graphSize.width,
|
||||
height: graphSize.height
|
||||
},
|
||||
minDimensions: ve.dm.MWGraphModel.static.minDimensions,
|
||||
fixedRatio: false
|
||||
} ) );
|
||||
|
||||
// Padding
|
||||
this.paddingAutoCheckbox.setSelected( this.graphModel.isPaddingAutomatic() );
|
||||
for ( padding in paddings ) {
|
||||
if ( paddings.hasOwnProperty( padding ) ) {
|
||||
this.paddingTable.setValue( padding, 0, paddings[ padding ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
for ( i = 0; i < dataFields.length; i++ ) {
|
||||
this.dataTable.insertColumn( null, null, dataFields[ i ], dataFields[ i ] );
|
||||
}
|
||||
|
||||
this.updateDataPage();
|
||||
|
||||
// JSON text input
|
||||
this.jsonTextInput.setValue( this.graphModel.getSpecString() ).clearUndoStack();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update data page widgets based on the current spec
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.updateDataPage = function () {
|
||||
var pipeline = this.graphModel.getPipeline( 0 ),
|
||||
i, row, field;
|
||||
|
||||
for ( i = 0; i < pipeline.values.length; i++ ) {
|
||||
row = [];
|
||||
|
||||
for ( field in pipeline.values[ i ] ) {
|
||||
if ( pipeline.values[ i ].hasOwnProperty( field ) ) {
|
||||
row.push( pipeline.values[ i ][ field ] );
|
||||
}
|
||||
}
|
||||
|
||||
this.dataTable.insertRow( row );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate raw data input
|
||||
*
|
||||
* @private
|
||||
* @param {string} value The new input value
|
||||
* @return {boolean} Data is valid
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.validateRawData = function ( value ) {
|
||||
var isValid = !$.isEmptyObject( ve.dm.MWGraphNode.static.parseSpecString( value ) ),
|
||||
label = ( isValid ) ? '' : ve.msg( 'graph-ve-dialog-edit-json-invalid' );
|
||||
|
||||
this.setLabel( label );
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle spec string input change
|
||||
*
|
||||
* @private
|
||||
* @param {string} value The text input value
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onSpecStringInputChange = function ( value ) {
|
||||
var newRawData;
|
||||
|
||||
try {
|
||||
// If parsing fails here, nothing more needs to happen
|
||||
newRawData = JSON.parse( value );
|
||||
|
||||
// Only pass changes to model if there was anything worthwhile to change
|
||||
if ( !OO.compare( this.cachedRawData, newRawData ) ) {
|
||||
this.cachedRawData = newRawData;
|
||||
this.graphModel.setSpecFromString( value );
|
||||
}
|
||||
} catch ( err ) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle graph type changes
|
||||
*
|
||||
* @param {string} value The new graph type
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onGraphTypeInputChange = function ( value ) {
|
||||
this.unknownGraphTypeWarningLabel.toggle( value === 'unknown' );
|
||||
|
||||
if ( value !== 'unknown' ) {
|
||||
this.graphModel.switchGraphType( value );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle data input changes
|
||||
*
|
||||
* @private
|
||||
* @param {number} rowIndex The index of the row that changed
|
||||
* @param {string} rowKey The key of the row that changed, or `undefined` if it doesn't exist
|
||||
* @param {number} colIndex The index of the column that changed
|
||||
* @param {string} colKey The key of the column that changed, or `undefined` if it doesn't exist
|
||||
* @param {string} value The new value
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onDataInputChange = function ( rowIndex, rowKey, colIndex, colKey, value ) {
|
||||
if ( !isNaN( value ) ) {
|
||||
this.graphModel.setEntryField( rowIndex, colKey, +value );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle data input row deletions
|
||||
*
|
||||
* @param {number} [rowIndex] The index of the row deleted
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onDataInputRowDelete = function ( rowIndex ) {
|
||||
this.graphModel.removeEntry( rowIndex );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle page set events on the root layout
|
||||
*
|
||||
* @param {OO.ui.PageLayout} page Set page
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onRootLayoutSet = function ( page ) {
|
||||
if ( page.getName() === 'raw' ) {
|
||||
// The raw data may have been changed while not visible,
|
||||
// so recalculate height now it is visible.
|
||||
// HACK: Invalidate value cache
|
||||
this.jsonTextInput.valCache = null;
|
||||
this.jsonTextInput.adjustSize( true );
|
||||
this.setSize( 'larger' );
|
||||
} else {
|
||||
this.setSize( 'large' );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle auto padding mode changes
|
||||
*
|
||||
* @param {boolean} value New mode value
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onPaddingAutoCheckboxChange = function ( value ) {
|
||||
this.graphModel.setPaddingAuto( value );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle size widget changes
|
||||
*
|
||||
* @param {Object} dimensions New dimensions
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onSizeWidgetChange = function ( dimensions ) {
|
||||
if ( this.sizeWidget.isValid() ) {
|
||||
this.graphModel.setWidth( dimensions.width );
|
||||
this.graphModel.setHeight( dimensions.height );
|
||||
}
|
||||
this.checkChanges();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle padding table data changes
|
||||
*
|
||||
* @param {number} rowIndex The index of the row that changed
|
||||
* @param {string} rowKey The key of the row that changed, or `undefined` if it doesn't exist
|
||||
* @param {number} colIndex The index of the column that changed
|
||||
* @param {string} colKey The key of the column that changed, or `undefined` if it doesn't exist
|
||||
* @param {string} value The new value
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onPaddingTableChange = function ( rowIndex, rowKey, colIndex, colKey, value ) {
|
||||
this.graphModel.setPadding( rowKey, parseInt( value ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model spec change events
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.onSpecChange = function () {
|
||||
var padding,
|
||||
paddingAuto = this.graphModel.isPaddingAutomatic(),
|
||||
paddingObj = this.graphModel.getPaddingObject();
|
||||
|
||||
if ( this.listeningToInputChanges ) {
|
||||
this.listeningToInputChanges = false;
|
||||
|
||||
this.jsonTextInput.setValue( this.graphModel.getSpecString() );
|
||||
|
||||
if ( paddingAuto ) {
|
||||
// Clear padding table if set to automatic
|
||||
this.paddingTable.clear();
|
||||
} else {
|
||||
// Fill padding table with model values if set to manual
|
||||
for ( padding in paddingObj ) {
|
||||
if ( paddingObj.hasOwnProperty( padding ) ) {
|
||||
this.paddingTable.setValue( padding, 0, paddingObj[ padding ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.paddingTable.setDisabled( paddingAuto );
|
||||
|
||||
this.listeningToInputChanges = true;
|
||||
|
||||
this.checkChanges();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for overall validity and enables/disables action abilities accordingly
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.checkChanges = function () {
|
||||
var dialog = this;
|
||||
|
||||
// Synchronous validation
|
||||
if ( !this.sizeWidget.isValid() ) {
|
||||
this.actions.setAbilities( { done: false } );
|
||||
return;
|
||||
}
|
||||
|
||||
// Asynchronous validation
|
||||
this.jsonTextInput.getValidity().then(
|
||||
function () {
|
||||
dialog.actions.setAbilities( {
|
||||
done: ( dialog.mode === 'insert' ) || dialog.graphModel.hasBeenChanged()
|
||||
} );
|
||||
},
|
||||
function () {
|
||||
dialog.actions.setAbilities( { done: false } );
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets and caches the mode of the dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} mode The new mode, either `edit` or `insert`
|
||||
*/
|
||||
ve.ui.MWGraphDialog.prototype.setMode = function ( mode ) {
|
||||
this.actions.setMode( mode );
|
||||
this.mode = mode;
|
||||
};
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.ui.windowFactory.register( ve.ui.MWGraphDialog );
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* MediaWiki UserInterface graph tool.
|
||||
*
|
||||
* @class
|
||||
* @extends ve.ui.FragmentWindowTool
|
||||
* @constructor
|
||||
* @param {OO.ui.ToolGroup} toolGroup
|
||||
* @param {Object} [config] Configuration options
|
||||
*/
|
||||
ve.ui.MWGraphDialogTool = function VeUiMWGraphDialogTool() {
|
||||
ve.ui.MWGraphDialogTool.super.apply( this, arguments );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.MWGraphDialogTool, ve.ui.FragmentWindowTool );
|
||||
|
||||
/* Static properties */
|
||||
|
||||
ve.ui.MWGraphDialogTool.static.name = 'graph';
|
||||
ve.ui.MWGraphDialogTool.static.group = 'object';
|
||||
ve.ui.MWGraphDialogTool.static.icon = 'graph';
|
||||
ve.ui.MWGraphDialogTool.static.title =
|
||||
OO.ui.deferMsg( 'graph-ve-dialog-button-tooltip' );
|
||||
ve.ui.MWGraphDialogTool.static.modelClasses = [ ve.dm.MWGraphNode ];
|
||||
ve.ui.MWGraphDialogTool.static.commandName = 'graph';
|
||||
|
||||
/* Registration */
|
||||
|
||||
ve.ui.toolFactory.register( ve.ui.MWGraphDialogTool );
|
||||
|
||||
/* Commands */
|
||||
|
||||
ve.ui.commandRegistry.register(
|
||||
new ve.ui.Command(
|
||||
'graph', 'window', 'open',
|
||||
{ args: [ 'graph' ], supportedSelections: [ 'linear' ] }
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* VisualEditor UserInterface Graph icon styles.
|
||||
*
|
||||
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
.oo-ui-icon-graph {
|
||||
/* @embed */
|
||||
background-image: url( graph.svg );
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*!
|
||||
* VisualEditor DataModel RowWidgetModel class
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* RowWidget model.
|
||||
*
|
||||
* @class
|
||||
* @mixins OO.EventEmitter
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [config] Configuration options
|
||||
* @cfg {Array} [data] An array containing all values of the row
|
||||
* @cfg {Array} [keys] An array of keys for easy cell selection
|
||||
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
|
||||
* @cfg {string} [label=''] Row label. Defaults to empty string.
|
||||
* @cfg {boolean} [showLabel=true] Show row label. Defaults to true.
|
||||
* @cfg {boolean} [deletable=true] Allow row to be deleted. Defaults to true.
|
||||
*/
|
||||
ve.dm.RowWidgetModel = function VeDmRowWidgetModel( config ) {
|
||||
config = config || {};
|
||||
|
||||
// Mixin constructors
|
||||
OO.EventEmitter.call( this, config );
|
||||
|
||||
this.data = config.data || [];
|
||||
this.validate = config.validate;
|
||||
this.index = ( config.index !== undefined ) ? config.index : -1;
|
||||
this.label = ( config.label !== undefined ) ? config.label : '';
|
||||
this.showLabel = ( config.showLabel !== undefined ) ? !!config.showLabel : true;
|
||||
this.isDeletable = ( config.deletable !== undefined ) ? !!config.deletable : true;
|
||||
|
||||
this.initializeProps( config.keys );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.mixinClass( ve.dm.RowWidgetModel, OO.EventEmitter );
|
||||
|
||||
/* Events */
|
||||
|
||||
/**
|
||||
* @event valueChange
|
||||
*
|
||||
* Fired when a value inside the row has changed.
|
||||
*
|
||||
* @param {number} The column index of the updated cell
|
||||
* @param {number} The new value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event insertCell
|
||||
*
|
||||
* Fired when a new cell is inserted into the row.
|
||||
*
|
||||
* @param {Array} The initial data
|
||||
* @param {number} The index in which to insert the new cell
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event removeCell
|
||||
*
|
||||
* Fired when a cell is removed from the row.
|
||||
*
|
||||
* @param {number} The removed cell index
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event clear
|
||||
*
|
||||
* Fired when the row is cleared
|
||||
*
|
||||
* @param {boolean} Clear cell properties
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event labelUpdate
|
||||
*
|
||||
* Fired when the row label might need to be updated
|
||||
*/
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Initializes and ensures the proper creation of the cell property array.
|
||||
* If data exceeds the number of cells given, new ones will be created.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} props The initial cell props
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.initializeProps = function ( props ) {
|
||||
var i, len;
|
||||
|
||||
this.cells = [];
|
||||
|
||||
if ( Array.isArray( props ) ) {
|
||||
for ( i = 0, len = props.length; i < len; i++ ) {
|
||||
this.cells.push( {
|
||||
index: i,
|
||||
key: props[ i ]
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers the initialization process and builds the initial row.
|
||||
*
|
||||
* @fires insertCell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.setupRow = function () {
|
||||
this.verifyData();
|
||||
this.buildRow();
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies if the table data is complete and synced with
|
||||
* cell properties, and adds empty strings as cell data if
|
||||
* cells are missing
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.verifyData = function () {
|
||||
var i, len;
|
||||
|
||||
for ( i = 0, len = this.cells.length; i < len; i++ ) {
|
||||
if ( this.data[ i ] === undefined ) {
|
||||
this.data.push( '' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build initial row
|
||||
*
|
||||
* @private
|
||||
* @fires insertCell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.buildRow = function () {
|
||||
var i, len;
|
||||
|
||||
for ( i = 0, len = this.cells.length; i < len; i++ ) {
|
||||
this.emit( 'insertCell', this.data[ i ], i );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the entire row with new data
|
||||
*
|
||||
* @private
|
||||
* @fires insertCell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.refreshRow = function () {
|
||||
// TODO: Clear existing table
|
||||
|
||||
this.buildRow();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a particular cell
|
||||
*
|
||||
* @param {number|string} handle The index or key of the cell
|
||||
* @param {mixed} value The new value
|
||||
* @fires valueChange
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.setValue = function ( handle, value ) {
|
||||
var index;
|
||||
|
||||
if ( typeof handle === 'number' ) {
|
||||
index = handle;
|
||||
} else if ( typeof handle === 'string' ) {
|
||||
index = this.getCellProperties( handle ).index;
|
||||
}
|
||||
|
||||
if ( typeof index === 'number' && this.data[ index ] !== undefined &&
|
||||
this.data[ index ] !== value ) {
|
||||
|
||||
this.data[ index ] = value;
|
||||
this.emit( 'valueChange', index, value );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the row data
|
||||
*
|
||||
* @param {Array} data The new row data
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.setData = function ( data ) {
|
||||
if ( Array.isArray( data ) ) {
|
||||
this.data = data;
|
||||
|
||||
this.verifyData();
|
||||
this.refreshRow();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the row index
|
||||
*
|
||||
* @param {number} index The new row index
|
||||
* @fires labelUpdate
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.setIndex = function ( index ) {
|
||||
this.index = index;
|
||||
this.emit( 'labelChange' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the row label
|
||||
*
|
||||
* @param {number} label The new row label
|
||||
* @fires labelUpdate
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.setLabel = function ( label ) {
|
||||
this.label = label;
|
||||
this.emit( 'labelChange' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a row into the table. If the row isn't added at the end of the table,
|
||||
* all the following data will be shifted back one row.
|
||||
*
|
||||
* @param {number|string} [data] The data to insert to the cell.
|
||||
* @param {number} [index] The index in which to insert the new cell.
|
||||
* If unset or set to null, the cell will be added at the end of the row.
|
||||
* @param {string} [key] A key to quickly select this cell.
|
||||
* If unset or set to null, no key will be set.
|
||||
* @fires insertCell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.insertCell = function ( data, index, key ) {
|
||||
var insertIndex = ( typeof index === 'number' ) ? index : this.cells.length,
|
||||
insertData, i, len;
|
||||
|
||||
// Add the new cell metadata
|
||||
this.cells.splice( insertIndex, 0, {
|
||||
index: insertIndex,
|
||||
key: key || undefined
|
||||
} );
|
||||
|
||||
// Add the new row data
|
||||
insertData = ( $.type( data ) === 'string' || $.type( data ) === 'number' ) ? data : '';
|
||||
this.data.splice( insertIndex, 0, insertData );
|
||||
|
||||
// Update all indexes in following cells
|
||||
for ( i = insertIndex + 1, len = this.cells.length; i < len; i++ ) {
|
||||
this.cells[ i ].index++;
|
||||
}
|
||||
|
||||
this.emit( 'insertCell', data, insertIndex );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a cell from the table. If the cell removed isn't at the end of the table,
|
||||
* all the following cells will be shifted back one cell.
|
||||
*
|
||||
* @param {number|string} handle The key or numerical index of the cell to remove
|
||||
* @fires removeCell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.removeCell = function ( handle ) {
|
||||
var cellProps = this.getCellProperties( handle ),
|
||||
i, len;
|
||||
|
||||
// Exit early if the row couldn't be found
|
||||
if ( cellProps === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cells.splice( cellProps.index, 1 );
|
||||
this.data.splice( cellProps.index, 1 );
|
||||
|
||||
// Update all indexes in following cells
|
||||
for ( i = cellProps.index, len = this.cells.length; i < len; i++ ) {
|
||||
this.cells[ i ].index--;
|
||||
}
|
||||
|
||||
this.emit( 'removeCell', cellProps.index );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the row data
|
||||
*
|
||||
* @fires clear
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.clear = function () {
|
||||
this.data = [];
|
||||
this.verifyData();
|
||||
|
||||
this.emit( 'clear', false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the row data, as well as all cell properties
|
||||
*
|
||||
* @fires clear
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.clearWithProperties = function () {
|
||||
this.data = [];
|
||||
this.cells = [];
|
||||
|
||||
this.emit( 'clear', true );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the validation pattern to test cells against
|
||||
*
|
||||
* @return {RegExp|Function|string}
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.getValidationPattern = function () {
|
||||
return this.validate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all row properties
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.getRowProperties = function () {
|
||||
return {
|
||||
index: this.index,
|
||||
label: this.label,
|
||||
showLabel: this.showLabel,
|
||||
isDeletable: this.isDeletable
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of a given cell
|
||||
*
|
||||
* @param {string|number} handle The key (or numeric index) of the cell
|
||||
* @return {Object|null} An object containing the `key` and `index` properties of the cell.
|
||||
* Returns `null` if the cell can't be found.
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.getCellProperties = function ( handle ) {
|
||||
var cell = null,
|
||||
i, len;
|
||||
|
||||
if ( typeof handle === 'string' ) {
|
||||
for ( i = 0, len = this.cells.length; i < len; i++ ) {
|
||||
if ( this.cells[ i ].key === handle ) {
|
||||
cell = this.cells[ i ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ( typeof handle === 'number' ) {
|
||||
if ( handle < this.cells.length ) {
|
||||
cell = this.cells[ handle ];
|
||||
}
|
||||
}
|
||||
|
||||
return cell;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of all cells
|
||||
*
|
||||
* @return {Array} An array of objects containing `key` and `index` properties for each cell
|
||||
*/
|
||||
ve.dm.RowWidgetModel.prototype.getAllCellProperties = function () {
|
||||
return this.cells.slice();
|
||||
};
|
||||
@@ -0,0 +1,515 @@
|
||||
/*!
|
||||
* VisualEditor DataModel TableWidgetModel class.
|
||||
*
|
||||
* @license The MIT License (MIT); see LICENSE.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
* TableWidget model.
|
||||
*
|
||||
* @class
|
||||
* @mixins OO.EventEmitter
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [config] Configuration options
|
||||
* @cfg {Array} [rows] An array of objects containing `key` and `label` properties for every row
|
||||
* @cfg {Array} [cols] An array of objects containing `key` and `label` properties for every column
|
||||
* @cfg {Array} [data] An array containing all values of the table
|
||||
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
|
||||
* @cfg {boolean} [showHeaders=true] Show table header row. Defaults to true.
|
||||
* @cfg {boolean} [showRowLabels=true] Show row labels. Defaults to true.
|
||||
* @cfg {boolean} [allowRowInsertion=true] Allow row insertion. Defaults to true.
|
||||
* @cfg {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
|
||||
*/
|
||||
ve.dm.TableWidgetModel = function VeDmTableWidgetModel( config ) {
|
||||
config = config || {};
|
||||
|
||||
// Mixin constructors
|
||||
OO.EventEmitter.call( this, config );
|
||||
|
||||
this.data = config.data || [];
|
||||
this.validate = config.validate;
|
||||
this.showHeaders = ( config.showHeaders !== undefined ) ? !!config.showHeaders : true;
|
||||
this.showRowLabels = ( config.showRowLabels !== undefined ) ? !!config.showRowLabels : true;
|
||||
this.allowRowInsertion = ( config.allowRowInsertion !== undefined ) ? !!config.allowRowInsertion : true;
|
||||
this.allowRowDeletion = ( config.allowRowDeletion !== undefined ) ? !!config.allowRowDeletion : true;
|
||||
|
||||
this.initializeProps( config.rows, config.cols );
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.mixinClass( ve.dm.TableWidgetModel, OO.EventEmitter );
|
||||
|
||||
/* Static Methods */
|
||||
|
||||
/**
|
||||
* Get an entry from a props table
|
||||
*
|
||||
* @static
|
||||
* @private
|
||||
* @param {string|number} handle The key (or numeric index) of the row/column
|
||||
* @param {Array} table Props table
|
||||
* @return {Object|null} An object containing the `key`, `index` and `label`
|
||||
* properties of the row/column. Returns `null` if the row/column can't be found.
|
||||
*/
|
||||
ve.dm.TableWidgetModel.static.getEntryFromPropsTable = function ( handle, table ) {
|
||||
var row = null,
|
||||
i, len;
|
||||
|
||||
if ( typeof handle === 'string' ) {
|
||||
for ( i = 0, len = table.length; i < len; i++ ) {
|
||||
if ( table[ i ].key === handle ) {
|
||||
row = table[ i ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ( typeof handle === 'number' ) {
|
||||
if ( handle < table.length ) {
|
||||
row = table[ handle ];
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
/* Events */
|
||||
|
||||
/**
|
||||
* @event valueChange
|
||||
*
|
||||
* Fired when a value inside the table has changed.
|
||||
*
|
||||
* @param {number} The row index of the updated cell
|
||||
* @param {number} The column index of the updated cell
|
||||
* @param {mixed} The new value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event insertRow
|
||||
*
|
||||
* Fired when a new row is inserted into the table.
|
||||
*
|
||||
* @param {Array} The initial data
|
||||
* @param {number} The index in which to insert the new row
|
||||
* @param {string} The row key
|
||||
* @param {string} The row label
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event insertColumn
|
||||
*
|
||||
* Fired when a new row is inserted into the table.
|
||||
*
|
||||
* @param {Array} The initial data
|
||||
* @param {number} The index in which to insert the new column
|
||||
* @param {string} The column key
|
||||
* @param {string} The column label
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event removeRow
|
||||
*
|
||||
* Fired when a row is removed from the table.
|
||||
*
|
||||
* @param {number} The removed row index
|
||||
* @param {string} The removed row key
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event removeColumn
|
||||
*
|
||||
* Fired when a column is removed from the table.
|
||||
*
|
||||
* @param {number} The removed column index
|
||||
* @param {string} The removed column key
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event clear
|
||||
*
|
||||
* Fired when the table data is wiped.
|
||||
*
|
||||
* @param {boolean} Clear row/column properties
|
||||
*/
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Initializes and ensures the proper creation of the rows and cols property arrays.
|
||||
* If data exceeds the number of rows and cols given, new ones will be created.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} rowProps The initial row props
|
||||
* @param {Array} colProps The initial column props
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.initializeProps = function ( rowProps, colProps ) {
|
||||
// FIXME: Account for extra data with missing row/col metadata
|
||||
|
||||
var i, len;
|
||||
|
||||
this.rows = [];
|
||||
this.cols = [];
|
||||
|
||||
if ( Array.isArray( rowProps ) ) {
|
||||
for ( i = 0, len = rowProps.length; i < len; i++ ) {
|
||||
this.rows.push( {
|
||||
index: i,
|
||||
key: rowProps[ i ].key,
|
||||
label: rowProps[ i ].label
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
if ( Array.isArray( colProps ) ) {
|
||||
for ( i = 0, len = colProps.length; i < len; i++ ) {
|
||||
this.cols.push( {
|
||||
index: i,
|
||||
key: colProps[ i ].key,
|
||||
label: colProps[ i ].label
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers the initialization process and builds the initial table.
|
||||
*
|
||||
* @fires insertRow
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.setupTable = function () {
|
||||
this.verifyData();
|
||||
this.buildTable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies if the table data is complete and synced with
|
||||
* row and column properties, and adds empty strings as
|
||||
* cell data if cells are missing
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.verifyData = function () {
|
||||
var i, j, rowLen, colLen;
|
||||
|
||||
for ( i = 0, rowLen = this.rows.length; i < rowLen; i++ ) {
|
||||
if ( this.data[ i ] === undefined ) {
|
||||
this.data.push( [] );
|
||||
}
|
||||
|
||||
for ( j = 0, colLen = this.cols.length; j < colLen; j++ ) {
|
||||
if ( this.data[ i ][ j ] === undefined ) {
|
||||
this.data[ i ].push( '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build initial table
|
||||
*
|
||||
* @private
|
||||
* @fires insertRow
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.buildTable = function () {
|
||||
var i, len;
|
||||
|
||||
for ( i = 0, len = this.rows.length; i < len; i++ ) {
|
||||
this.emit( 'insertRow', this.data[ i ], i, this.rows[ i ].key, this.rows[ i ].label );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the entire table with new data
|
||||
*
|
||||
* @private
|
||||
* @fires insertRow
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.refreshTable = function () {
|
||||
// TODO: Clear existing table
|
||||
|
||||
this.buildTable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a particular cell
|
||||
*
|
||||
* @param {number|string} row The index or key of the row
|
||||
* @param {number|string} col The index or key of the column
|
||||
* @param {mixed} value The new value
|
||||
* @fires valueChange
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.setValue = function ( row, col, value ) {
|
||||
var rowIndex, colIndex;
|
||||
|
||||
if ( typeof row === 'number' ) {
|
||||
rowIndex = row;
|
||||
} else if ( typeof row === 'string' ) {
|
||||
rowIndex = this.getRowProperties( row ).index;
|
||||
}
|
||||
|
||||
if ( typeof col === 'number' ) {
|
||||
colIndex = col;
|
||||
} else if ( typeof col === 'string' ) {
|
||||
colIndex = this.getColumnProperties( col ).index;
|
||||
}
|
||||
|
||||
if ( typeof rowIndex === 'number' && typeof colIndex === 'number' &&
|
||||
this.data[ rowIndex ] !== undefined && this.data[ rowIndex ][ colIndex ] !== undefined &&
|
||||
this.data[ rowIndex ][ colIndex ] !== value ) {
|
||||
|
||||
this.data[ rowIndex ][ colIndex ] = value;
|
||||
this.emit( 'valueChange', rowIndex, colIndex, value );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the table data
|
||||
*
|
||||
* @param {Array} data The new table data
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.setData = function ( data ) {
|
||||
if ( Array.isArray( data ) ) {
|
||||
this.data = data;
|
||||
|
||||
this.verifyData();
|
||||
this.refreshTable();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a row into the table. If the row isn't added at the end of the table,
|
||||
* all the following data will be shifted back one row.
|
||||
*
|
||||
* @param {Array} [data] The data to insert to the row.
|
||||
* @param {number} [index] The index in which to insert the new row.
|
||||
* If unset or set to null, the row will be added at the end of the table.
|
||||
* @param {string} [key] A key to quickly select this row.
|
||||
* If unset or set to null, no key will be set.
|
||||
* @param {string} [label] A label to display next to the row.
|
||||
* If unset or set to null, the key will be used if there is one.
|
||||
* @fires insertRow
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.insertRow = function ( data, index, key, label ) {
|
||||
var insertIndex = ( typeof index === 'number' ) ? index : this.rows.length,
|
||||
newRowData = [],
|
||||
insertData, insertDataCell, i, len;
|
||||
|
||||
// Add the new row metadata
|
||||
this.rows.splice( insertIndex, 0, {
|
||||
index: insertIndex,
|
||||
key: key || undefined,
|
||||
label: label || undefined
|
||||
} );
|
||||
|
||||
// Add the new row data
|
||||
insertData = ( Array.isArray( data ) ) ? data : [];
|
||||
// Ensure that all columns of data for this row have been supplied,
|
||||
// otherwise fill the remaining data with empty strings
|
||||
for ( i = 0, len = this.cols.length; i < len; i++ ) {
|
||||
insertDataCell = '';
|
||||
if ( $.type( insertData[ i ] ) === 'string' || $.type( insertData[ i ] ) === 'number' ) {
|
||||
insertDataCell = insertData[ i ];
|
||||
}
|
||||
|
||||
newRowData.push( insertDataCell );
|
||||
}
|
||||
this.data.splice( insertIndex, 0, newRowData );
|
||||
|
||||
// Update all indexes in following rows
|
||||
for ( i = insertIndex + 1, len = this.rows.length; i < len; i++ ) {
|
||||
this.rows[ i ].index++;
|
||||
}
|
||||
|
||||
this.emit( 'insertRow', data, insertIndex, key, label );
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a column into the table. If the column isn't added at the end of the table,
|
||||
* all the following data will be shifted back one column.
|
||||
*
|
||||
* @param {Array} [data] The data to insert to the column.
|
||||
* @param {number} [index] The index in which to insert the new column.
|
||||
* If unset or set to null, the column will be added at the end of the table.
|
||||
* @param {string} [key] A key to quickly select this column.
|
||||
* If unset or set to null, no key will be set.
|
||||
* @param {string} [label] A label to display next to the column.
|
||||
* If unset or set to null, the key will be used if there is one.
|
||||
* @fires insertColumn
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.insertColumn = function ( data, index, key, label ) {
|
||||
var insertIndex = ( typeof index === 'number' ) ? index : this.cols.length,
|
||||
insertDataCell, insertData, i, len;
|
||||
|
||||
// Add the new column metadata
|
||||
this.cols.splice( insertIndex, 0, {
|
||||
index: insertIndex,
|
||||
key: key || undefined,
|
||||
label: label || undefined
|
||||
} );
|
||||
|
||||
// Add the new column data
|
||||
insertData = ( Array.isArray( data ) ) ? data : [];
|
||||
// Ensure that all rows of data for this column have been supplied,
|
||||
// otherwise fill the remaining data with empty strings
|
||||
for ( i = 0, len = this.rows.length; i < len; i++ ) {
|
||||
insertDataCell = '';
|
||||
if ( $.type( insertData[ i ] ) === 'string' || $.type( insertData[ i ] ) === 'number' ) {
|
||||
insertDataCell = insertData[ i ];
|
||||
}
|
||||
|
||||
this.data[ i ].splice( insertIndex, 0, insertDataCell );
|
||||
}
|
||||
|
||||
// Update all indexes in following cols
|
||||
for ( i = insertIndex + 1, len = this.cols.length; i < len; i++ ) {
|
||||
this.cols[ i ].index++;
|
||||
}
|
||||
|
||||
this.emit( 'insertColumn', data, index, key, label );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a row from the table. If the row removed isn't at the end of the table,
|
||||
* all the following rows will be shifted back one row.
|
||||
*
|
||||
* @param {number|string} handle The key or numerical index of the row to remove
|
||||
* @fires removeRow
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.removeRow = function ( handle ) {
|
||||
var rowProps = this.getRowProperties( handle ),
|
||||
i, len;
|
||||
|
||||
// Exit early if the row couldn't be found
|
||||
if ( rowProps === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rows.splice( rowProps.index, 1 );
|
||||
this.data.splice( rowProps.index, 1 );
|
||||
|
||||
// Update all indexes in following rows
|
||||
for ( i = rowProps.index, len = this.rows.length; i < len; i++ ) {
|
||||
this.rows[ i ].index--;
|
||||
}
|
||||
|
||||
this.emit( 'removeRow', rowProps.index, rowProps.key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a column from the table. If the column removed isn't at the end of the table,
|
||||
* all the following columns will be shifted back one column.
|
||||
*
|
||||
* @param {number|string} handle The key or numerical index of the column to remove
|
||||
* @fires removeColumn
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.removeColumn = function ( handle ) {
|
||||
var colProps = this.getColumnProperties( handle ),
|
||||
i, len;
|
||||
|
||||
// Exit early if the column couldn't be found
|
||||
if ( colProps === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cols.splice( colProps.index, 1 );
|
||||
|
||||
for ( i = 0, len = this.data.length; i < len; i++ ) {
|
||||
this.data[ i ].splice( colProps.index, 1 );
|
||||
}
|
||||
|
||||
// Update all indexes in following columns
|
||||
for ( i = colProps.index, len = this.cols.length; i < len; i++ ) {
|
||||
this.cols[ i ].index--;
|
||||
}
|
||||
|
||||
this.emit( 'removeColumn', colProps.index, colProps.key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the table data
|
||||
*
|
||||
* @fires clear
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.clear = function () {
|
||||
this.data = [];
|
||||
this.verifyData();
|
||||
|
||||
this.emit( 'clear', false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the table data, as well as all row and column properties
|
||||
*
|
||||
* @fires clear
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.clearWithProperties = function () {
|
||||
this.data = [];
|
||||
this.rows = [];
|
||||
this.cols = [];
|
||||
|
||||
this.emit( 'clear', true );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all table properties
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getTableProperties = function () {
|
||||
return {
|
||||
showHeaders: this.showHeaders,
|
||||
showRowLabels: this.showRowLabels,
|
||||
allowRowInsertion: this.allowRowInsertion,
|
||||
allowRowDeletion: this.allowRowDeletion
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the validation pattern to test cells against
|
||||
*
|
||||
* @return {RegExp|Function|string}
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getValidationPattern = function () {
|
||||
return this.validate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of a given row
|
||||
*
|
||||
* @param {string|number} handle The key (or numeric index) of the row
|
||||
* @return {Object|null} An object containing the `key`, `index` and `label` properties of the row.
|
||||
* Returns `null` if the row can't be found.
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getRowProperties = function ( handle ) {
|
||||
return ve.dm.TableWidgetModel.static.getEntryFromPropsTable( handle, this.rows );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of all rows
|
||||
*
|
||||
* @return {Array} An array of objects containing `key`, `index` and `label` properties for each row
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getAllRowProperties = function () {
|
||||
return this.rows.slice();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of a given column
|
||||
*
|
||||
* @param {string|number} handle The key (or numeric index) of the column
|
||||
* @return {Object|null} An object containing the `key`, `index` and `label` properties of the column.
|
||||
* Returns `null` if the column can't be found.
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getColumnProperties = function ( handle ) {
|
||||
return ve.dm.TableWidgetModel.static.getEntryFromPropsTable( handle, this.cols );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get properties of all columns
|
||||
*
|
||||
* @return {Array} An array of objects containing `key`, `index` and `label` properties for each column
|
||||
*/
|
||||
ve.dm.TableWidgetModel.prototype.getAllColumnProperties = function () {
|
||||
return this.cols.slice();
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
.ve-ui-rowWidget {
|
||||
clear: left;
|
||||
float: left;
|
||||
margin-bottom: -1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ve-ui-rowWidget-label {
|
||||
display: block;
|
||||
margin-right: 5%;
|
||||
padding-top: 0.5em;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.ve-ui-rowWidget > .ve-ui-rowWidget-label {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ve-ui-rowWidget > .ve-ui-rowWidget-cells {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ve-ui-rowWidget > .ve-ui-rowWidget-cells > .oo-ui-inputWidget {
|
||||
float: left;
|
||||
margin-right: -1px;
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.ve-ui-rowWidget > .ve-ui-rowWidget-cells > .oo-ui-inputWidget > input,
|
||||
.ve-ui-rowWidget > .ve-ui-rowWidget-delete-button > .oo-ui-buttonElement-button {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* A RowWidget is used in conjunction with {@link ve.ui.TableWidget table widgets}
|
||||
* and should not be instantiated by themselves. They group together
|
||||
* {@link OO.ui.TextInputWidget text input widgets} to form a unified row of
|
||||
* editable data.
|
||||
*
|
||||
* @class
|
||||
* @extends OO.ui.Widget
|
||||
* @mixins OO.ui.mixin.GroupElement
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [config] Configuration options
|
||||
* @cfg {Array} [data] The data of the cells
|
||||
* @cfg {Array} [keys] An array of keys for easy cell selection
|
||||
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
|
||||
* @cfg {number} [index] The row index.
|
||||
* @cfg {string} [label] The row label to display. If not provided, the row index will
|
||||
* be used be default. If set to null, no label will be displayed.
|
||||
* @cfg {boolean} [showLabel=true] Show row label. Defaults to true.
|
||||
* @cfg {boolean} [deletable=true] Whether the table should provide deletion UI tools
|
||||
* for this row or not. Defaults to true.
|
||||
*/
|
||||
ve.ui.RowWidget = function VeUiRowWidget( config ) {
|
||||
config = config || {};
|
||||
|
||||
// Parent constructor
|
||||
ve.ui.RowWidget.super.call( this, config );
|
||||
|
||||
// Mixin constructor
|
||||
OO.ui.mixin.GroupElement.call( this, config );
|
||||
|
||||
// Set up model
|
||||
this.model = new ve.dm.RowWidgetModel( config );
|
||||
|
||||
// Set up group element
|
||||
this.setGroupElement(
|
||||
$( '<div>' )
|
||||
.addClass( 've-ui-rowWidget-cells' )
|
||||
);
|
||||
|
||||
// Set up label
|
||||
this.labelCell = new OO.ui.LabelWidget( {
|
||||
classes: [ 've-ui-rowWidget-label' ]
|
||||
} );
|
||||
|
||||
// Set up delete button
|
||||
if ( this.model.getRowProperties().isDeletable ) {
|
||||
this.deleteButton = new OO.ui.ButtonWidget( {
|
||||
icon: { 'default': 'remove' },
|
||||
classes: [ 've-ui-rowWidget-delete-button' ],
|
||||
flags: 'destructive',
|
||||
title: ve.msg( 'graph-ve-dialog-edit-table-row-delete' )
|
||||
} );
|
||||
}
|
||||
|
||||
// Events
|
||||
this.model.connect( this, {
|
||||
valueChange: 'onValueChange',
|
||||
insertCell: 'onInsertCell',
|
||||
removeCell: 'onRemoveCell',
|
||||
clear: 'onClear',
|
||||
labelUpdate: 'onLabelUpdate'
|
||||
} );
|
||||
|
||||
this.aggregate( {
|
||||
change: 'cellChange'
|
||||
} );
|
||||
|
||||
this.connect( this, {
|
||||
cellChange: 'onCellChange',
|
||||
disable: 'onDisable'
|
||||
} );
|
||||
|
||||
if ( this.model.getRowProperties().isDeletable ) {
|
||||
this.deleteButton.connect( this, {
|
||||
click: 'onDeleteButtonClick'
|
||||
} );
|
||||
}
|
||||
|
||||
// Initialization
|
||||
this.$element.addClass( 've-ui-rowWidget' );
|
||||
|
||||
this.$element.append(
|
||||
this.labelCell.$element,
|
||||
this.$group
|
||||
);
|
||||
|
||||
if ( this.model.getRowProperties().isDeletable ) {
|
||||
this.$element.append( this.deleteButton.$element );
|
||||
}
|
||||
|
||||
this.setLabel( this.model.getRowProperties().label );
|
||||
|
||||
this.model.setupRow();
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.RowWidget, OO.ui.Widget );
|
||||
OO.mixinClass( ve.ui.RowWidget, OO.ui.mixin.GroupElement );
|
||||
|
||||
/* Events */
|
||||
|
||||
/**
|
||||
* @event inputChange
|
||||
*
|
||||
* Change when an input contained within the row is updated
|
||||
*
|
||||
* @param {number} The index of the cell that changed
|
||||
* @param {string} The new value of the cell
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event deleteButtonClick
|
||||
*
|
||||
* Fired when the delete button for the row is pressed
|
||||
*/
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.addItems = function ( items, index ) {
|
||||
var i, len;
|
||||
|
||||
OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
|
||||
|
||||
for ( i = index, len = items.length; i < len; i++ ) {
|
||||
items[ i ].setData( i );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.removeItems = function ( items ) {
|
||||
var i, len, cells;
|
||||
|
||||
OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
|
||||
|
||||
cells = this.getItems();
|
||||
for ( i = 0, len = cells.length; i < len; i++ ) {
|
||||
cells[ i ].setData( i );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the row index
|
||||
*
|
||||
* @return {number} The row index
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.getIndex = function () {
|
||||
return this.model.getRowProperties().index;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the row index
|
||||
*
|
||||
* @param {number} index The new index
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.setIndex = function ( index ) {
|
||||
this.model.setIndex( index );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the label displayed on the row. If no custom label is set, the
|
||||
* row index is used instead.
|
||||
*
|
||||
* @return {string} The row label
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.getLabel = function () {
|
||||
var props = this.model.getRowProperties();
|
||||
|
||||
if ( props.label === null ) {
|
||||
return '';
|
||||
} else if ( !props.label ) {
|
||||
return props.index.toString();
|
||||
} else {
|
||||
return props.label;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the label to be displayed on the widget.
|
||||
*
|
||||
* @param {string} label The new label
|
||||
* @fires labelUpdate
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.setLabel = function ( label ) {
|
||||
this.model.setLabel( label );
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a particular cell
|
||||
*
|
||||
* @param {number} index The cell index
|
||||
* @param {string} value The new value
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.setValue = function ( index, value ) {
|
||||
this.model.setValue( index, value );
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert a cell at a specified index
|
||||
*
|
||||
* @param {string} data The cell data
|
||||
* @param {index} index The index to insert the cell at
|
||||
* @param {string} key A key for easy cell selection
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.insertCell = function ( data, index, key ) {
|
||||
this.model.insertCell( data, index, key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a column at a specified index
|
||||
*
|
||||
* @param {number} index The index to removeColumn
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.removeCell = function ( index ) {
|
||||
this.model.removeCell( index );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the field values
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.clear = function () {
|
||||
this.model.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model value changes
|
||||
*
|
||||
* @param {number} index The column index of the updated cell
|
||||
* @param {number} value The new value
|
||||
*
|
||||
* @fires inputChange
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onValueChange = function ( index, value ) {
|
||||
this.getItems()[ index ].setValue( value );
|
||||
this.emit( 'inputChange', index, value );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model cell insertions
|
||||
*
|
||||
* @param {string} data The initial data
|
||||
* @param {number} index The index in which to insert the new cell
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onInsertCell = function ( data, index ) {
|
||||
this.addItems( [
|
||||
new OO.ui.TextInputWidget( {
|
||||
data: index,
|
||||
value: data,
|
||||
validate: this.model.getValidationPattern()
|
||||
} )
|
||||
], index );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model cell removals
|
||||
*
|
||||
* @param {number} index The removed cell index
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onRemoveCell = function ( index ) {
|
||||
this.removeItems( [ index ] );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle clear requests
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onClear = function () {
|
||||
var i, len,
|
||||
cells = this.getItems();
|
||||
|
||||
for ( i = 0, len = cells.length; i < len; i++ ) {
|
||||
cells[ i ].setValue( '' );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update model label changes
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onLabelUpdate = function () {
|
||||
this.labelCell.setLabel( this.getLabel() );
|
||||
};
|
||||
|
||||
/**
|
||||
* React to cell input change
|
||||
*
|
||||
* @private
|
||||
* @param {OO.ui.TextInputWidget} input The input that fired the event
|
||||
* @param {string} value The value of the input
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onCellChange = function ( input, value ) {
|
||||
// FIXME: The table itself should know if it contains invalid data
|
||||
// in order to pass form state to the dialog when it asks if the Apply
|
||||
// button should be enabled or not. This probably requires the table
|
||||
// and each individual row to handle validation through an array of promises
|
||||
// fed from the cells within.
|
||||
// Right now, the table can't know if it's valid or not because the events
|
||||
// don't get passed through.
|
||||
var self = this;
|
||||
input.getValidity().done( function () {
|
||||
self.model.setValue( input.getData(), value );
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle delete button clicks
|
||||
*
|
||||
* @private
|
||||
* @fires deleteButtonClick
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onDeleteButtonClick = function () {
|
||||
this.emit( 'deleteButtonClick' );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle disabled state changes
|
||||
*
|
||||
* @param {boolean} disabled The new disabled state
|
||||
*/
|
||||
ve.ui.RowWidget.prototype.onDisable = function ( disabled ) {
|
||||
var i,
|
||||
cells = this.getItems();
|
||||
|
||||
for ( i = 0; i < cells.length; i++ ) {
|
||||
cells[ i ].setDisabled( disabled );
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
.ve-ui-tableWidget > .ve-ui-tableWidget-rows {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ve-ui-tableWidget.ve-ui-tableWidget-no-labels .ve-ui-rowWidget-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/**
|
||||
* A TableWidget groups {@link ve.ui.RowWidget row widgets} together to form a bidimensional
|
||||
* grid of text inputs.
|
||||
*
|
||||
* @class
|
||||
* @extends OO.ui.Widget
|
||||
* @mixins OO.ui.mixin.GroupElement
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [config] Configuration options
|
||||
* @cfg {Array} [rows] An array of objects containing `key` and `label` properties for every row
|
||||
* @cfg {Array} [cols] An array of objects containing `key` and `label` properties for every column
|
||||
* @cfg {Array} [data] An array containing all values of the table
|
||||
* @cfg {RegExp|Function|string} [validate] Validation pattern to apply on every cell
|
||||
* @cfg {boolean} [showHeaders=true] Whether or not to show table headers. Defaults to true.
|
||||
* @cfg {boolean} [showRowLabels=true] Whether or not to show row labels. Defaults to true.
|
||||
* @cfg {boolean} [allowRowInsertion=true] Whether or not to enable row insertion. Defaults to true.
|
||||
* @cfg {boolean} [allowRowDeletion=true] Allow row deletion. Defaults to true.
|
||||
*/
|
||||
ve.ui.TableWidget = function VeUiTableWidget( config ) {
|
||||
var headerRowItems = [],
|
||||
insertionRowItems = [],
|
||||
columnProps, prop, i, len;
|
||||
|
||||
// Configuration initialization
|
||||
config = config || {};
|
||||
|
||||
// Parent constructor
|
||||
ve.ui.TableWidget.super.call( this, config );
|
||||
|
||||
// Mixin constructors
|
||||
OO.ui.mixin.GroupElement.call( this, config );
|
||||
|
||||
// Set up model
|
||||
this.model = new ve.dm.TableWidgetModel( config );
|
||||
|
||||
// Properties
|
||||
this.listeningToInsertionRowChanges = true;
|
||||
|
||||
// Set up group element
|
||||
this.setGroupElement(
|
||||
$( '<div>' )
|
||||
.addClass( 've-ui-tableWidget-rows' )
|
||||
);
|
||||
|
||||
// Set up static rows
|
||||
columnProps = this.model.getAllColumnProperties();
|
||||
|
||||
if ( this.model.getTableProperties().showHeaders ) {
|
||||
this.headerRow = new ve.ui.RowWidget( {
|
||||
deletable: false,
|
||||
label: null
|
||||
} );
|
||||
|
||||
for ( i = 0, len = columnProps.length; i < len; i++ ) {
|
||||
prop = columnProps[ i ];
|
||||
headerRowItems.push( new OO.ui.TextInputWidget( {
|
||||
value: prop.label ? prop.label : ( prop.key ? prop.key : prop.index ),
|
||||
// TODO: Allow editing of fields
|
||||
disabled: true
|
||||
} ) );
|
||||
}
|
||||
|
||||
this.headerRow.addItems( headerRowItems );
|
||||
}
|
||||
|
||||
if ( this.model.getTableProperties().allowRowInsertion ) {
|
||||
this.insertionRow = new ve.ui.RowWidget( {
|
||||
classes: 've-ui-rowWidget-insertionRow',
|
||||
deletable: false,
|
||||
label: null
|
||||
} );
|
||||
|
||||
for ( i = 0, len = columnProps.length; i < len; i++ ) {
|
||||
insertionRowItems.push( new OO.ui.TextInputWidget( {
|
||||
data: columnProps[ i ].key ? columnProps[ i ].key : columnProps[ i ].index
|
||||
} ) );
|
||||
}
|
||||
|
||||
this.insertionRow.addItems( insertionRowItems );
|
||||
}
|
||||
|
||||
// Set up initial rows
|
||||
if ( Array.isArray( config.items ) ) {
|
||||
this.addItems( config.items );
|
||||
}
|
||||
|
||||
// Events
|
||||
this.model.connect( this, {
|
||||
valueChange: 'onValueChange',
|
||||
insertRow: 'onInsertRow',
|
||||
insertColumn: 'onInsertColumn',
|
||||
removeRow: 'onRemoveRow',
|
||||
removeColumn: 'onRemoveColumn',
|
||||
clear: 'onClear'
|
||||
} );
|
||||
|
||||
this.aggregate( {
|
||||
inputChange: 'rowInputChange',
|
||||
deleteButtonClick: 'rowDeleteButtonClick'
|
||||
} );
|
||||
|
||||
this.connect( this, {
|
||||
disable: 'onDisabledChange',
|
||||
rowInputChange: 'onRowInputChange',
|
||||
rowDeleteButtonClick: 'onRowDeleteButtonClick'
|
||||
} );
|
||||
|
||||
if ( this.model.getTableProperties().allowRowInsertion ) {
|
||||
this.insertionRow.connect( this, {
|
||||
inputChange: 'onInsertionRowInputChange'
|
||||
} );
|
||||
}
|
||||
|
||||
// Initialization
|
||||
this.$element.addClass( 've-ui-tableWidget' );
|
||||
|
||||
if ( this.model.getTableProperties().showHeaders ) {
|
||||
this.$element.append( this.headerRow.$element );
|
||||
}
|
||||
this.$element.append( this.$group );
|
||||
if ( this.model.getTableProperties().allowRowInsertion ) {
|
||||
this.$element.append( this.insertionRow.$element );
|
||||
}
|
||||
|
||||
this.$element.toggleClass(
|
||||
've-ui-tableWidget-no-labels',
|
||||
!this.model.getTableProperties().showRowLabels
|
||||
);
|
||||
|
||||
this.model.setupTable();
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
OO.inheritClass( ve.ui.TableWidget, OO.ui.Widget );
|
||||
OO.mixinClass( ve.ui.TableWidget, OO.ui.mixin.GroupElement );
|
||||
|
||||
/* Static Properties */
|
||||
ve.ui.TableWidget.static.patterns = {
|
||||
validate: /^[0-9]+(\.[0-9]+)?$/,
|
||||
filter: /[0-9]+(\.[0-9]+)?/
|
||||
};
|
||||
|
||||
/* Events */
|
||||
|
||||
/**
|
||||
* @event change
|
||||
*
|
||||
* Change when the data within the table has been updated.
|
||||
*
|
||||
* @param {number} The index of the row that changed
|
||||
* @param {string} The key of the row that changed, or `undefined` if it doesn't exist
|
||||
* @param {number} The index of the column that changed
|
||||
* @param {string} The key of the column that changed, or `undefined` if it doesn't exist
|
||||
* @param {string} The new value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @event removeRow
|
||||
*
|
||||
* Fires when a row is removed from the table
|
||||
*
|
||||
* @param {number} The index of the row being deleted
|
||||
* @param {string} The key of the row being deleted
|
||||
*/
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Set the value of a particular cell
|
||||
*
|
||||
* @param {number|string} row The row containing the cell to edit. Can be either
|
||||
* the row index or string key if one has been set for the row.
|
||||
* @param {number|string} col The column containing the cell to edit. Can be either
|
||||
* the column index or string key if one has been set for the column.
|
||||
* @param {mixed} value The new value
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.setValue = function ( row, col, value ) {
|
||||
this.model.setValue( row, col, value );
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the table data
|
||||
*
|
||||
* @param {Array} data The new table data
|
||||
* @return {boolean} The data has been successfully changed
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.setData = function ( data ) {
|
||||
if ( !Array.isArray( data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.model.setData( data );
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a row into the table. If the row isn't added at the end of the table,
|
||||
* all the following data will be shifted back one row.
|
||||
*
|
||||
* @param {Array} [data] The data to insert to the row.
|
||||
* @param {number} [index] The index in which to insert the new row.
|
||||
* If unset or set to null, the row will be added at the end of the table.
|
||||
* @param {string} [key] A key to quickly select this row.
|
||||
* If unset or set to null, no key will be set.
|
||||
* @param {string} [label] A label to display next to the row.
|
||||
* If unset or set to null, the key will be used if there is one.
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.insertRow = function ( data, index, key, label ) {
|
||||
this.model.insertRow( data, index, key, label );
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a column into the table. If the column isn't added at the end of the table,
|
||||
* all the following data will be shifted back one column.
|
||||
*
|
||||
* @param {Array} [data] The data to insert to the column.
|
||||
* @param {number} [index] The index in which to insert the new column.
|
||||
* If unset or set to null, the column will be added at the end of the table.
|
||||
* @param {string} [key] A key to quickly select this column.
|
||||
* If unset or set to null, no key will be set.
|
||||
* @param {string} [label] A label to display next to the column.
|
||||
* If unset or set to null, the key will be used if there is one.
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.insertColumn = function ( data, index, key, label ) {
|
||||
this.model.insertColumn( data, index, key, label );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a row from the table. If the row removed isn't at the end of the table,
|
||||
* all the following rows will be shifted back one row.
|
||||
*
|
||||
* @param {number|string} key The key or numerical index of the row to remove.
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.removeRow = function ( key ) {
|
||||
this.model.removeRow( key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a column from the table. If the column removed isn't at the end of the table,
|
||||
* all the following columns will be shifted back one column.
|
||||
*
|
||||
* @param {number|string} key The key or numerical index of the column to remove.
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.removeColumn = function ( key ) {
|
||||
this.model.removeColumn( key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears all values from the table, without wiping any row or column properties.
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.clear = function () {
|
||||
this.model.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the table data, as well as all row and column properties
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.clearWithProperties = function () {
|
||||
this.model.clearWithProperties();
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter cell input once it is changed
|
||||
*
|
||||
* @param {string} value The input value
|
||||
* @return {string} The filtered input
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.filterCellInput = function ( value ) {
|
||||
var matches = value.match( ve.ui.TableWidget.static.patterns.filter );
|
||||
return ( Array.isArray( matches ) ) ? matches[ 0 ] : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.addItems = function ( items, index ) {
|
||||
var i, len;
|
||||
|
||||
OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
|
||||
|
||||
for ( i = index, len = items.length; i < len; i++ ) {
|
||||
items[ i ].setIndex( i );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.removeItems = function ( items ) {
|
||||
var i, len, rows;
|
||||
|
||||
OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
|
||||
|
||||
rows = this.getItems();
|
||||
for ( i = 0, len = rows.length; i < len; i++ ) {
|
||||
rows[ i ].setIndex( i );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model value changes
|
||||
*
|
||||
* @private
|
||||
* @param {number} row The row index of the changed cell
|
||||
* @param {number} col The column index of the changed cell
|
||||
* @param {mixed} value The new value
|
||||
* @fires change
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onValueChange = function ( row, col, value ) {
|
||||
var rowProps = this.model.getRowProperties( row ),
|
||||
colProps = this.model.getColumnProperties( col );
|
||||
|
||||
this.getItems()[ row ].setValue( col, value );
|
||||
|
||||
this.emit( 'change', row, rowProps.key, col, colProps.key, value );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model row insertions
|
||||
*
|
||||
* @private
|
||||
* @param {Array} data The initial data
|
||||
* @param {number} index The index in which the new row was inserted
|
||||
* @param {string} key The row key
|
||||
* @param {string} label The row label
|
||||
* @fires change
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onInsertRow = function ( data, index, key, label ) {
|
||||
var colProps = this.model.getAllColumnProperties(),
|
||||
keys = [],
|
||||
newRow, i, len;
|
||||
|
||||
for ( i = 0, len = colProps.length; i < len; i++ ) {
|
||||
keys.push( ( colProps[ i ].key ) ? colProps[ i ].key : i );
|
||||
}
|
||||
|
||||
newRow = new ve.ui.RowWidget( {
|
||||
data: data,
|
||||
keys: keys,
|
||||
validate: this.model.getValidationPattern(),
|
||||
label: label,
|
||||
showLabel: this.model.getTableProperties().showRowLabels,
|
||||
deletable: this.model.getTableProperties().allowRowDeletion
|
||||
} );
|
||||
|
||||
// TODO: Handle index parameter. Right now all new rows are inserted at the end
|
||||
this.addItems( [ newRow ] );
|
||||
|
||||
// If this is the first data being added, refresh headers and insertion row
|
||||
if ( this.model.getAllRowProperties().length === 1 ) {
|
||||
this.refreshTableMarginals();
|
||||
}
|
||||
|
||||
for ( i = 0, len = data.length; i < len; i++ ) {
|
||||
this.emit( 'change', index, key, i, colProps[ i ].key, data[ i ] );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model column insertions
|
||||
*
|
||||
* @private
|
||||
* @param {Array} data The initial data
|
||||
* @param {number} index The index in which to insert the new column
|
||||
* @param {string} key The row key
|
||||
* @param {string} label The row label
|
||||
*
|
||||
* @fires change
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onInsertColumn = function ( data, index, key, label ) {
|
||||
var tableProps = this.model.getTableProperties(),
|
||||
items = this.getItems(),
|
||||
rowProps = this.model.getAllRowProperties(),
|
||||
i, len;
|
||||
|
||||
for ( i = 0, len = items.length; i < len; i++ ) {
|
||||
items[ i ].insertCell( data[ i ], index, key );
|
||||
this.emit( 'change', i, rowProps[ i ].key, index, key, data[ i ] );
|
||||
}
|
||||
|
||||
if ( tableProps.showHeaders ) {
|
||||
this.headerRow.addItems( [
|
||||
new OO.ui.TextInputWidget( {
|
||||
value: label || key || index,
|
||||
// TODO: Allow editing of fields
|
||||
disabled: true
|
||||
} )
|
||||
] );
|
||||
}
|
||||
|
||||
if ( tableProps.handleRowInsertion ) {
|
||||
this.insertionRow.addItems( [
|
||||
new OO.ui.TextInputWidget( {
|
||||
validate: this.model.getValidationPattern()
|
||||
} )
|
||||
] );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model row removals
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The removed row index
|
||||
* @param {string} key The removed row key
|
||||
* @fires removeRow
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onRemoveRow = function ( index, key ) {
|
||||
this.removeItems( [ this.getItems()[ index ] ] );
|
||||
this.emit( 'removeRow', index, key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model column removals
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The removed column index
|
||||
* @param {string} key The removed column key
|
||||
* @fires removeColumn
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onRemoveColumn = function ( index, key ) {
|
||||
var i, items = this.getItems();
|
||||
|
||||
for ( i = 0; i < items.length; i++ ) {
|
||||
items[ i ].removeCell( index );
|
||||
}
|
||||
|
||||
this.emit( 'removeColumn', index, key );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle model table clears
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} withProperties Clear row/column properties
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onClear = function ( withProperties ) {
|
||||
var i, len, rows;
|
||||
|
||||
if ( withProperties ) {
|
||||
this.removeItems( this.getItems() );
|
||||
} else {
|
||||
rows = this.getItems();
|
||||
|
||||
for ( i = 0, len = rows.length; i < len; i++ ) {
|
||||
rows[ i ].clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* React to input changes bubbled up from event aggregation
|
||||
*
|
||||
* @private
|
||||
* @param {ve.ui.RowWidget} row The row that changed
|
||||
* @param {number} colIndex The column index of the cell that changed
|
||||
* @param {string} value The new value of the input
|
||||
* @fires change
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onRowInputChange = function ( row, colIndex, value ) {
|
||||
var items = this.getItems(),
|
||||
i, len, rowIndex;
|
||||
|
||||
for ( i = 0, len = items.length; i < len; i++ ) {
|
||||
if ( row === items[ i ] ) {
|
||||
rowIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.model.setValue( rowIndex, colIndex, value );
|
||||
};
|
||||
|
||||
/**
|
||||
* React to new row input changes
|
||||
*
|
||||
* @private
|
||||
* @param {number} colIndex The column index of the input that fired the change
|
||||
* @param {string} value The new row value
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onInsertionRowInputChange = function ( colIndex, value ) {
|
||||
var insertionRowItems = this.insertionRow.getItems(),
|
||||
newRowData = [],
|
||||
i, len, lastRow;
|
||||
|
||||
if ( this.listeningToInsertionRowChanges ) {
|
||||
for ( i = 0, len = insertionRowItems.length; i < len; i++ ) {
|
||||
if ( i === colIndex ) {
|
||||
newRowData.push( value );
|
||||
} else {
|
||||
newRowData.push( '' );
|
||||
}
|
||||
}
|
||||
|
||||
this.insertRow( newRowData );
|
||||
|
||||
// Focus newly inserted row
|
||||
lastRow = this.getItems().slice( -1 )[ 0 ];
|
||||
lastRow.getItems()[ colIndex ].focus();
|
||||
|
||||
// Reset insertion row
|
||||
this.listeningToInsertionRowChanges = false;
|
||||
this.insertionRow.clear();
|
||||
this.listeningToInsertionRowChanges = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle row deletion input
|
||||
*
|
||||
* @private
|
||||
* @param {ve.ui.RowWidget} row The row that asked for the deletion
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onRowDeleteButtonClick = function ( row ) {
|
||||
var items = this.getItems(),
|
||||
i = -1,
|
||||
len;
|
||||
|
||||
for ( i = 0, len = items.length; i < len; i++ ) {
|
||||
if ( items[ i ] === row ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.removeRow( i );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle disabled state changes
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} disabled The new state
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.onDisabledChange = function ( disabled ) {
|
||||
var rows = this.getItems(),
|
||||
i;
|
||||
|
||||
for ( i = 0; i < rows.length; i++ ) {
|
||||
rows[ i ].setDisabled( disabled );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh table header and insertion row
|
||||
*/
|
||||
ve.ui.TableWidget.prototype.refreshTableMarginals = function () {
|
||||
var tableProps = this.model.getTableProperties(),
|
||||
columnProps = this.model.getAllColumnProperties(),
|
||||
rowItems,
|
||||
i, len;
|
||||
|
||||
if ( tableProps.showHeaders ) {
|
||||
this.headerRow.removeItems( this.headerRow.getItems() );
|
||||
rowItems = [];
|
||||
|
||||
for ( i = 0, len = columnProps.length; i < len; i++ ) {
|
||||
rowItems.push( new OO.ui.TextInputWidget( {
|
||||
value: columnProps[ i ].key ? columnProps[ i ].key : columnProps[ i ].index,
|
||||
// TODO: Allow editing of fields
|
||||
disabled: true
|
||||
} ) );
|
||||
}
|
||||
|
||||
this.headerRow.addItems( rowItems );
|
||||
}
|
||||
|
||||
if ( tableProps.allowRowInsertion ) {
|
||||
this.insertionRow.clear();
|
||||
this.insertionRow.removeItems( this.insertionRow.getItems() );
|
||||
|
||||
for ( i = 0, len = columnProps.length; i < len; i++ ) {
|
||||
this.insertionRow.insertCell( '', columnProps[ i ].index, columnProps[ i ].key );
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user