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:
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