All files / src/ui ve.ui.Sequence.js

73.13% Statements 49/67
56.81% Branches 25/44
71.42% Functions 5/7
73.13% Lines 49/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201                                                          1x 6x 6x 6x 6x 6x   1x 1x 1x 1x     5x 5x 5x 5x           1x                       1x 8676x   8676x 1446x 1446x     7230x 7315x 7312x 7227x   3x       3x                   1x 3x   3x       3x   3x         3x 3x 3x           3x       3x 3x             3x           3x     3x       3x         3x       3x             3x               1x 6x               1x 6x                               1x                          
/*!
 * VisualEditor UserInterface Sequence class.
 *
 * @copyright See AUTHORS.txt
 */
 
/**
 * Key sequence.
 *
 * @class
 *
 * @constructor
 * @param {string} name Symbolic name
 * @param {string} commandName Command name this sequence executes
 * @param {string|Array|RegExp} data Data to match. String, linear data array, or regular expression.
 *         When using a RegularExpression always match the end of the sequence with a '$' so that
 *         only sequences next to the user's cursor match.
 * @param {number} [strip=0] Number of data elements to strip after execution
 *         (from the right)
 * @param {Object} [config] [description]
 * @cfg {boolean} [setSelection=false] Whether to set the selection to the
 *       range matching the sequence before executing the command.
 * @cfg {boolean} [delayed=false] Whether to wait for the user to stop typing matching content
 *       before executing the command. When the sequence matches typed text, it will not be executed
 *       immediately, but only after more non-matching text is added afterwards or the selection is
 *       changed. This is useful for variable-length sequences (defined with RegExps).
 * @cfg {boolean} [checkOnPaste=false] Whether the sequence should also be matched after paste.
 * @cfg {boolean} [checkOnDelete=false] Whether the sequence should also be matched after delete.
 */
ve.ui.Sequence = function VeUiSequence( name, commandName, data, strip, config ) {
	this.name = name;
	this.commandName = commandName;
	this.data = data;
	this.strip = strip || 0;
	if ( typeof config === 'object' ) {
		// TODO: Add `config = config || {};` when variadic fallback is dropped.
		this.setSelection = !!config.setSelection;
		this.delayed = !!config.delayed;
		this.checkOnPaste = !!config.checkOnPaste;
		this.checkOnDelete = !!config.checkOnDelete;
	} else {
		// Backwards compatibility with variadic arguments
		this.setSelection = !!arguments[ 4 ];
		this.delayed = !!arguments[ 5 ];
		this.checkOnPaste = !!arguments[ 6 ];
		this.checkOnDelete = !!arguments[ 7 ];
	}
};
 
/* Inheritance */
 
OO.initClass( ve.ui.Sequence );
 
/* Methods */
 
/**
 * Check if the sequence matches a given offset in the data
 *
 * @param {ve.dm.ElementLinearData} data String or linear data
 * @param {number} offset
 * @param {string} plaintext Plain text of data
 * @return {ve.Range|null} Range corresponding to the match, or else null
 */
ve.ui.Sequence.prototype.match = function ( data, offset, plaintext ) {
	var i, j = offset - 1;
 
	if ( this.data instanceof RegExp ) {
		i = plaintext.search( this.data );
		return ( i < 0 ) ? null :
			new ve.Range( offset - plaintext.length + i, offset );
	}
	for ( i = this.data.length - 1; i >= 0; i--, j-- ) {
		if ( typeof this.data[ i ] === 'string' ) {
			if ( this.data[ i ] !== data.getCharacterData( j ) ) {
				return null;
			}
		} else Iif ( !ve.compare( this.data[ i ], data.getData( j ), true ) ) {
			return null;
		}
	}
	return new ve.Range( offset - this.data.length, offset );
};
 
/**
 * Execute the command associated with the sequence
 *
 * @param {ve.ui.Surface} surface
 * @param {ve.Range} range Range to set
 * @return {boolean} The command executed
 */
ve.ui.Sequence.prototype.execute = function ( surface, range ) {
	var surfaceModel = surface.getModel();
 
	Iif ( surface.getCommands().indexOf( this.getCommandName() ) === -1 ) {
		return false;
	}
 
	var command = surface.commandRegistry.lookup( this.getCommandName() );
 
	Iif ( !command ) {
		return false;
	}
 
	var stripFragment;
	Eif ( this.strip ) {
		var stripRange = surfaceModel.getSelection().getRange();
		stripFragment = surfaceModel.getLinearFragment(
			// noAutoSelect = true, excludeInsertions = true
			new ve.Range( stripRange.end, stripRange.end - this.strip ), true, true
		);
	}
 
	surfaceModel.breakpoint();
 
	// Use SurfaceFragment rather than Selection to automatically adjust the selection for any changes
	// (additions, removals) caused by executing the command
	var originalSelectionFragment = surfaceModel.getFragment();
	Iif ( this.setSelection ) {
		surfaceModel.setLinearSelection( range );
	}
 
	var args;
	// For sequences that trigger dialogs, pass an extra flag so the window knows
	// to un-strip the sequence if it is closed without action. See ve.ui.WindowAction.
	Iif ( command.getAction() === 'window' && command.getMethod() === 'open' ) {
		args = ve.copy( command.args );
		args[ 1 ] = args[ 1 ] || {};
		args[ 1 ].strippedSequence = !!this.strip;
	}
 
	Eif ( stripFragment ) {
		// Strip the typed text. This will be undone if the action triggered was
		// window/open and the window is dismissed
		stripFragment.removeContent();
	}
 
	// `args` can be passed undefined, and the defaults will be used
	var executed = command.execute( surface, args, 'sequence' );
 
	// Restore user's selection if:
	// * This sequence was not executed after all
	// * This sequence is delayed, so it only executes after the user changed the selection
	Iif ( !executed || this.delayed ) {
		originalSelectionFragment.select();
	}
 
	Iif ( stripFragment && !executed ) {
		surfaceModel.undo();
		// Prevent redoing (which would remove the typed text)
		surfaceModel.truncateUndoStack();
		surfaceModel.emit( 'history' );
	}
 
	return executed;
};
 
/**
 * Get the symbolic name of the sequence
 *
 * @return {string} Symbolic name
 */
ve.ui.Sequence.prototype.getName = function () {
	return this.name;
};
 
/**
 * Get the command name which the sequence will execute
 *
 * @return {string} Command name
 */
ve.ui.Sequence.prototype.getCommandName = function () {
	return this.commandName;
};
 
/**
 * Get a representation of the sequence useful for display
 *
 * What this means depends a bit on how the sequence was defined:
 * - It strips out undisplayable things like the paragraph-start marker.
 * - Regexps are just returned as a toString of the regexp.
 *
 * @param {boolean} explode Whether to return the message split up into some
 *        reasonable sequence of inputs required to trigger the sequence (regexps
 *        in sequences will be considered a single "input" as a toString of
 *        the regexp, because they're hard to display no matter what…)
 * @return {string} Message for display
 */
ve.ui.Sequence.prototype.getMessage = function ( explode ) {
	var data;
	if ( typeof this.data === 'string' ) {
		data = this.data.split( '' );
	} else if ( this.data instanceof RegExp ) {
		data = [ this.data.toString() ];
	} else {
		data = this.data.filter( function ( key ) {
			return !ve.isPlainObject( key );
		} );
	}
	return explode ? data : data.join( '' );
};