Файловый менеджер - Редактировать - /var/www/html/ve-graph.zip
Ðазад
PK ! ��"�� � ve.ui.MWGraphIcons.cssnu �[��� /*! * VisualEditor UserInterface Graph icon styles. * * @copyright See AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ .oo-ui-icon-graph { /* @embed */ background-image: url( graph.svg ); } PK ! ���� � ve.ce.MWGraphNode.jsnu �[��� /*! * VisualEditor ContentEditable MWGraphNode class. * * @license The MIT License (MIT); see LICENSE.txt */ const loadGraph = require( 'ext.graph.render' ).loadGraph; /** * ContentEditable MediaWiki graph node. * * @class * @extends ve.ce.MWBlockExtensionNode * @mixes 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'; ve.ce.MWGraphNode.static.getDescription = function ( model ) { const graphModel = new ve.dm.MWGraphModel( ve.copy( model.getSpec() ) ); // The following messages are used here: // * graph-ve-dialog-edit-type-area // * graph-ve-dialog-edit-type-bar // * graph-ve-dialog-edit-type-line // * graph-ve-dialog-edit-type-unknown return ve.msg( 'graph-ve-dialog-edit-type-' + graphModel.getGraphType() ); }; /* 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<vega.View>} 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 ) { const deferred = $.Deferred(); // 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 { loadGraph( element, spec ).then( ( vegaInfo ) => { deferred.resolve( vegaInfo.view ); }, () => { 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 ) { const 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 () { // Clear element this.$graph.empty(); this.$element.toggleClass( 'mw-graph-vega1', this.getModel().isGraphLegacy() ); mw.loader.using( 'ext.graph.render' ).then( () => { this.$plot.detach(); this.constructor.static.vegaParseSpec( this.getModel().getSpec(), this.$graph[ 0 ] ).then( () => { // do nothing }, ( failMessageKey ) => { // The following messages are used here: // * graph-ve-no-spec // * graph-ve-empty-graph // * graph-ve-vega-error-no-render // * graph-ve-vega-error this.$graph.text( ve.msg( failMessageKey ) ); } ); } ); }; /** * @inheritdoc */ ve.ce.MWGraphNode.prototype.getAttributeChanges = function ( width, height ) { const 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 ); PK ! �t�4 graph.svgnu �[��� <?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"> <g id="graph"> <path id="axes" d="M2 3v14h16v-2H4V3z"/> <path id="data" d="M5 14v-3l4-3 3 2 5-4v8H5"/> </g> </svg> PK ! ��x�M M .stylelintrc.jsonnu �[��� { "rules": { "selector-class-pattern": "^(ve|mw|oo-ui|client|skin)-" } } PK ! ����� � ve.dm.MWGraphNode.jsnu �[��� /*! * VisualEditor DataModel MWGraphNode class. * * @license The MIT License (MIT); see LICENSE.txt */ /** * DataModel MediaWiki graph node. * * @class * @extends ve.dm.MWBlockExtensionNode * @mixes ve.dm.ResizableNode * * @constructor * @param {Object} [element] */ ve.dm.MWGraphNode = function VeDmMWGraphNode() { // 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 const mw = this.getAttribute( 'mw' ); const 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 ) { let 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 ( typeof 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 ) { const result = JSON.stringify( spec, null, '\t' ); return result || ''; }; /* Methods */ /** * @inheritdoc */ ve.dm.MWGraphNode.prototype.createScalable = function () { const 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 && Object.prototype.hasOwnProperty.call( this.spec, 'version' ) && this.spec.version < 2 ); }; /* Registration */ ve.dm.modelRegistry.register( ve.dm.MWGraphNode ); PK ! ���3 3 ve.ce.MWGraphNode.cssnu �[��� .mw-graph { display: inline-block; border: 1px solid transparent; position: relative; background-repeat: no-repeat; background-position: 50% 50%; background-image: url( ../../styles/images/ajax-loader.gif ); } .ve-ce-mwGraphNode-plot { position: absolute; } .mw-graph canvas { background: #fff; } PK ! -k�SX X .eslintrc.jsonnu �[��� { "globals": { "ve": "readonly" }, "rules": { "no-jquery/no-extend": "warn" } } PK ! U��)9 )9 ve.ui.MWGraphDialog.jsnu �[��� /*! * 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 = 'medium'; 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 () { let key; // Parent method ve.ui.MWGraphDialog.super.prototype.initialize.call( this ); /* Root layout */ this.rootLayout = new OO.ui.IndexLayout( { classes: [ 've-ui-mwGraphDialog-panel-root' ] } ); this.generalPage = new OO.ui.TabPanelLayout( 'general', { label: ve.msg( 'graph-ve-dialog-edit-page-general' ) } ); this.dataPage = new OO.ui.TabPanelLayout( 'data', { label: ve.msg( 'graph-ve-dialog-edit-page-data' ) } ); this.rawPage = new OO.ui.TabPanelLayout( 'raw', { label: ve.msg( 'graph-ve-dialog-edit-page-raw' ) } ); this.rootLayout.addTabPanels( [ this.generalPage, this.dataPage, this.rawPage ] ); /* General page */ this.graphTypeDropdownInput = new OO.ui.DropdownInputWidget(); const graphTypeField = new OO.ui.FieldLayout( this.graphTypeDropdownInput, { label: ve.msg( 'graph-ve-dialog-edit-field-graph-type' ), align: 'left' } ); 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 } ); const sizeField = new OO.ui.FieldLayout( this.sizeWidget, { label: ve.msg( 'graph-ve-dialog-edit-size-field' ), align: 'left' } ); this.paddingAutoCheckbox = new OO.ui.CheckboxInputWidget( { value: 'paddingAuto' } ); const paddingAutoField = new OO.ui.FieldLayout( this.paddingAutoCheckbox, { label: ve.msg( 'graph-ve-dialog-edit-padding-auto' ), align: 'left' } ); this.paddingInputs = { top: new OO.ui.NumberInputWidget( { min: 0, showButtons: false } ), bottom: new OO.ui.NumberInputWidget( { min: 0, showButtons: false } ), left: new OO.ui.NumberInputWidget( { min: 0, showButtons: false } ), right: new OO.ui.NumberInputWidget( { min: 0, showButtons: false } ) }; const paddingTopField = new OO.ui.FieldLayout( this.paddingInputs.top, { label: ve.msg( 'graph-ve-dialog-edit-padding-top' ), align: 'left' } ); const paddingBottomField = new OO.ui.FieldLayout( this.paddingInputs.bottom, { label: ve.msg( 'graph-ve-dialog-edit-padding-bottom' ), align: 'left' } ); const paddingLeftField = new OO.ui.FieldLayout( this.paddingInputs.left, { label: ve.msg( 'graph-ve-dialog-edit-padding-left' ), align: 'left' } ); const paddingRightField = new OO.ui.FieldLayout( this.paddingInputs.right, { label: ve.msg( 'graph-ve-dialog-edit-padding-right' ), align: 'left' } ); this.generalPage.$element.append( graphTypeField.$element, this.unknownGraphTypeWarningLabel.$element, sizeField.$element, paddingAutoField.$element, paddingTopField.$element, paddingBottomField.$element, paddingLeftField.$element, paddingRightField.$element ); /* Data page */ this.dataTable = new mw.widgets.TableWidget( { validate: /^[0-9]+$/, showRowLabels: false } ); this.dataPage.$element.append( this.dataTable.$element ); /* Raw JSON page */ 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' ); const 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' } ); for ( key in this.paddingInputs ) { this.paddingInputs[ key ].connect( this, { change: [ 'onPaddingInputChange', key ] } ); } 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 () { let newElement; this.getFragment().getSurface().pushStaging(); // Create new graph node if not present (insert mode) if ( !this.selectedNode ) { newElement = this.getNewElement(); this.fragment = this.getFragment().insertContent( [ newElement, { type: '/' + newElement.type } ] ); this.getFragment().select(); this.selectedNode = this.getFragment().getSelectedNode(); } // Set up model const 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; this.rootLayout.resetScroll(); // 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 () { const graphType = this.graphModel.getGraphType(), graphSize = this.graphModel.getSize(), paddings = this.graphModel.getPaddingObject(), readOnly = this.isReadOnly(), 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 ); let padding, i; // Graph type if ( graphType === 'unknown' ) { options.push( unknownGraphTypeOption ); } this.graphTypeDropdownInput .setOptions( options ) .setValue( graphType ) .setDisabled( readOnly ); // Size this.sizeWidget.setScalable( new ve.dm.Scalable( { currentDimensions: { width: graphSize.width, height: graphSize.height }, minDimensions: ve.dm.MWGraphModel.static.minDimensions, fixedRatio: false } ) ); this.sizeWidget.setDisabled( readOnly ); // Padding this.paddingAutoCheckbox.setSelected( this.graphModel.isPaddingAutomatic() ) .setDisabled( readOnly ); for ( padding in paddings ) { if ( Object.prototype.hasOwnProperty.call( paddings, padding ) ) { this.paddingInputs[ padding ].setValue( paddings[ padding ] ) .setReadOnly( readOnly ); } } // Data for ( i = 0; i < dataFields.length; i++ ) { this.dataTable.insertColumn( null, null, dataFields[ i ], dataFields[ i ] ); } this.dataTable.setDisabled( readOnly ); this.updateDataPage(); // JSON text input this.jsonTextInput .setValue( this.graphModel.getSpecString() ) .setReadOnly( readOnly ) .clearUndoStack(); }; /** * Update data page widgets based on the current spec */ ve.ui.MWGraphDialog.prototype.updateDataPage = function () { const pipeline = this.graphModel.getPipeline( 0 ); let i, row, field; for ( i = 0; i < pipeline.values.length; i++ ) { row = []; for ( field in pipeline.values[ i ] ) { if ( Object.prototype.hasOwnProperty.call( pipeline.values[ i ], 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 ) { const 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 ) { let 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' ) { this.jsonTextInput.adjustSize( true ); } }; /** * Handle auto padding mode changes * * @param {boolean} value New mode value */ ve.ui.MWGraphDialog.prototype.onPaddingAutoCheckboxChange = function ( value ) { let key; this.graphModel.setPaddingAuto( value ); for ( key in this.paddingInputs ) { this.paddingInputs[ key ].setDisabled( 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 changes * * @param {string} key 'top', 'bottom', 'left' or 'right' * @param {string} value The new value */ ve.ui.MWGraphDialog.prototype.onPaddingInputChange = function ( key, value ) { if ( value !== '' ) { this.graphModel.setPadding( key, +value ); } }; /** * Handle model spec change events * * @private */ ve.ui.MWGraphDialog.prototype.onSpecChange = function () { let padding; const 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 for ( padding in this.paddingInputs ) { this.paddingInputs[ padding ].setValue( '' ); } } else { // Fill padding table with model values if set to manual for ( padding in paddingObj ) { if ( Object.prototype.hasOwnProperty.call( paddingObj, padding ) ) { this.paddingInputs[ padding ].setValue( paddingObj[ padding ] ); } } } for ( padding in this.paddingInputs ) { this.paddingInputs[ padding ].setDisabled( paddingAuto ); } this.listeningToInputChanges = true; this.checkChanges(); } }; /** * Check for overall validity and enables/disables action abilities accordingly * * @private */ ve.ui.MWGraphDialog.prototype.checkChanges = function () { // Synchronous validation if ( !this.sizeWidget.isValid() ) { this.actions.setAbilities( { done: false } ); return; } // Asynchronous validation this.jsonTextInput.getValidity().then( () => { this.actions.setAbilities( { done: ( this.mode === 'insert' ) || this.graphModel.hasBeenChanged() } ); }, () => { this.actions.setAbilities( { done: false } ); } ); }; /* Registration */ ve.ui.windowFactory.register( ve.ui.MWGraphDialog ); PK ! �N�E ve.ui.MWGraphDialogTool.jsnu �[��� /** * 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' ] } ) ); PK ! �2+�S1 S1 ve.dm.MWGraphModel.jsnu �[��� /*! * VisualEditor DataModel MWGraphModel class. * * @license The MIT License (MIT); see LICENSE.txt */ /** * MediaWiki graph model. * * @class * @mixes 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 ) { let undefinedProperty, i; const undefinedProperties = ve.dm.MWGraphModel.static.getUndefinedProperties( params ); // Remove undefined properties from spec for ( i = 0; i < undefinedProperties.length; i++ ) { undefinedProperty = undefinedProperties[ i ].split( '.' ); ve.dm.MWGraphModel.static.removeProperty( spec, Object.assign( [], undefinedProperty ) ); ve.dm.MWGraphModel.static.removeProperty( params, Object.assign( [], 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 ) { let property; list = list || []; // Append . to the stack if it's defined stack = ( stack === undefined ) ? '' : stack + '.'; for ( property in obj ) { if ( Object.prototype.hasOwnProperty.call( obj, property ) ) { if ( typeof obj[ property ] === 'object' ) { 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 ) { const firstProp = prop.shift(); try { if ( prop.length > 0 ) { ve.dm.MWGraphModel.static.removeProperty( obj[ firstProp ], prop ); } else { if ( Array.isArray( obj ) ) { 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 ) { const 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 ) { const 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 ) { const 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 ) { const 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 () { const 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 () { let i; const 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 ) { const 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 ) { const fields = this.getPipelineFields( pipelineId ), newEntry = {}; let 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 ); }; PK ! ��X��) �) $ tests/ext.graph.visualEditor.test.jsnu �[��� /*! * VisualEditor MWGraphNode tests. */ QUnit.module( 'ext.graph.visualEditor' ); ( function () { 'use strict'; /* Sample specs */ const 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', ( assert ) => { const 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', ( assert ) => { const view = ve.test.utils.createSurfaceViewFromHtml( '<div typeof="mw:Extension/graph"></div>' ), documentNode = view.getDocument().getDocumentNode(), node = documentNode.children[ 0 ]; assert.strictEqual( node.type, 'mwGraph', 'Parsoid HTML graphs are properly recognized as graph nodes' ); } ); QUnit.test( 've.ce.MWGraphNode.static', ( assert ) => { const testElement = document.createElement( 'div' ), renderValidTest = assert.async(), renderInvalidTest = assert.async(); $( '#qunit-fixture' ).append( testElement ); testElement.dataset.graphId = 'areaGraph'; const promise = ve.ce.MWGraphNode.static.vegaParseSpec( sampleSpecs.areaGraph, testElement ); promise.always( () => { assert.strictEqual( promise.state(), 'resolved', 'Single graph gets rendered correctly' ); renderValidTest(); } ); testElement.dataset.graphId = 'invalidAxesBarGraph'; ve.ce.MWGraphNode.static.vegaParseSpec( sampleSpecs.invalidAxesBarGraph, testElement ).always( ( failMessageKey ) => { assert.strictEqual( failMessageKey, 'graph-ve-vega-error', 'Invalid graph triggers an error at rendering' ); renderInvalidTest(); } ); } ); QUnit.test( 've.dm.MWGraphModel', ( assert ) => { const 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.strictEqual( model.hasBeenChanged(), false, 'Model changes are correctly initialized' ); model.setSpecFromString( 'invalid json string' ); assert.strictEqual( model.hasBeenChanged(), true, 'Model spec resets to an empty object when fed invalid data' ); model.setSpecFromString( JSON.stringify( sampleSpecs.areaGraph, null, '\t' ) ); assert.strictEqual( model.hasBeenChanged(), false, 'Model doesn\'t throw false positives after applying no changes' ); model.setSpecFromString( JSON.stringify( sampleSpecs.stackedAreaGraph ) ); assert.strictEqual( 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', ( assert ) => { let result; const 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' ); ve.dm.MWGraphModel.static.removeProperty( complexTestObj, [ 'b' ] ); assert.deepEqual( complexTestObj, removePropComplexExpected, 'Trying to delete an invalid property does nothing' ); } ); }() ); PK ! �Z�x x tests/.eslintrc.jsonnu �[��� { "extends": [ "wikimedia/qunit", "../.eslintrc.json" ], "rules": { "no-jquery/no-global-selector": "off" } } PK ! ��"�� � ve.ui.MWGraphIcons.cssnu �[��� PK ! ���� � ve.ce.MWGraphNode.jsnu �[��� PK ! �t�4 graph.svgnu �[��� PK ! ��x�M M P .stylelintrc.jsonnu �[��� PK ! ����� � � ve.dm.MWGraphNode.jsnu �[��� PK ! ���3 3 �'