1
0
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:
gnosygnu
2018-11-02 09:58:55 -04:00
parent a672fd8340
commit 5721913241
6057 changed files with 1156950 additions and 0 deletions

View 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

View File

@@ -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' );
} );
}() );

View File

@@ -0,0 +1,9 @@
.mw-graph {
display: inline-block;
border: 1px solid transparent;
position: relative;
}
.ve-ce-mwGraphNode-plot {
position: absolute;
}

View File

@@ -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 );

View File

@@ -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 );
};

View File

@@ -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 );

View File

@@ -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 );

View File

@@ -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' ] }
)
);

View File

@@ -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 );
}

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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;
}

View File

@@ -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 );
}
};

View File

@@ -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;
}

View File

@@ -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 );
}
}
};