Esanders has uploaded a new change for review. https://gerrit.wikimedia.org/r/175334
Change subject: Create ve.ui.Sequence which matches commands to typed text ...................................................................... Create ve.ui.Sequence which matches commands to typed text Register two sequences: - '<p>* ' creates a bullet list - '<p>1. ' creates a numbered list (note this isn't '#' as that is MW-specific) Change-Id: I6a4d71d845ecde460193466d9d8d12a0c9050c98 --- M .docs/eg-iframe.html M build/modules.json M demos/ve/desktop.html M demos/ve/mobile.html M src/ce/ve.ce.Surface.js M src/ui/actions/ve.ui.ListAction.js M src/ui/ve.ui.CommandRegistry.js A src/ui/ve.ui.Sequence.js A src/ui/ve.ui.SequenceRegistry.js M tests/index.html 10 files changed, 252 insertions(+), 13 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor refs/changes/34/175334/1 diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html index 9562a05..d1d726e 100644 --- a/.docs/eg-iframe.html +++ b/.docs/eg-iframe.html @@ -284,6 +284,8 @@ <script src="../src/ui/ve.ui.CommandRegistry.js"></script> <script src="../src/ui/ve.ui.Trigger.js"></script> <script src="../src/ui/ve.ui.TriggerRegistry.js"></script> + <script src="../src/ui/ve.ui.Sequence.js"></script> + <script src="../src/ui/ve.ui.SequenceRegistry.js"></script> <script src="../src/ui/ve.ui.Action.js"></script> <script src="../src/ui/ve.ui.ActionFactory.js"></script> <script src="../src/ui/ve.ui.FileDropHandler.js"></script> diff --git a/build/modules.json b/build/modules.json index c96abff..6a5eab1 100644 --- a/build/modules.json +++ b/build/modules.json @@ -310,6 +310,8 @@ "src/ui/ve.ui.CommandRegistry.js", "src/ui/ve.ui.Trigger.js", "src/ui/ve.ui.TriggerRegistry.js", + "src/ui/ve.ui.Sequence.js", + "src/ui/ve.ui.SequenceRegistry.js", "src/ui/ve.ui.Action.js", "src/ui/ve.ui.ActionFactory.js", "src/ui/ve.ui.FileDropHandler.js", diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html index 41ab04d..9038e66 100644 --- a/demos/ve/desktop.html +++ b/demos/ve/desktop.html @@ -297,6 +297,8 @@ <script src="../../src/ui/ve.ui.CommandRegistry.js"></script> <script src="../../src/ui/ve.ui.Trigger.js"></script> <script src="../../src/ui/ve.ui.TriggerRegistry.js"></script> + <script src="../../src/ui/ve.ui.Sequence.js"></script> + <script src="../../src/ui/ve.ui.SequenceRegistry.js"></script> <script src="../../src/ui/ve.ui.Action.js"></script> <script src="../../src/ui/ve.ui.ActionFactory.js"></script> <script src="../../src/ui/ve.ui.FileDropHandler.js"></script> diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html index ed878ed..a437661 100644 --- a/demos/ve/mobile.html +++ b/demos/ve/mobile.html @@ -298,6 +298,8 @@ <script src="../../src/ui/ve.ui.CommandRegistry.js"></script> <script src="../../src/ui/ve.ui.Trigger.js"></script> <script src="../../src/ui/ve.ui.TriggerRegistry.js"></script> + <script src="../../src/ui/ve.ui.Sequence.js"></script> + <script src="../../src/ui/ve.ui.SequenceRegistry.js"></script> <script src="../../src/ui/ve.ui.Action.js"></script> <script src="../../src/ui/ve.ui.ActionFactory.js"></script> <script src="../../src/ui/ve.ui.FileDropHandler.js"></script> diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js index 9a442db..e788610 100644 --- a/src/ce/ve.ce.Surface.js +++ b/src/ce/ve.ce.Surface.js @@ -2245,6 +2245,9 @@ } finally { this.decRenderLock(); } + setTimeout( function () { + surface.checkSequences(); + } ); return; } @@ -2311,6 +2314,24 @@ ve.dm.Transaction.newFromReplacement( this.documentView.model, replacementRange, data ), new ve.dm.LinearSelection( this.documentView.model, newRange ) ); + this.queueCheckSequences = true; + setTimeout( function () { + surface.checkSequences(); + } ); +}; + +/** + * Check the current surface offset for sequence matches + */ +ve.ce.Surface.prototype.checkSequences = function () { + var i, surfaceModel = this.surface.getModel(), + sequences = ve.ui.sequenceRegistry.findMatching( surfaceModel.getDocument().data, surfaceModel.getSelection().getRange().end ); + + // sequences.length will likely be 0 or 1 so don't cache + for ( i = 0; i < sequences.length; i++ ) { + sequences[i].execute( this.surface ); + } + this.showSelection( this.surface.getModel().getSelection() ); }; /** diff --git a/src/ui/actions/ve.ui.ListAction.js b/src/ui/actions/ve.ui.ListAction.js index 920dfe7..f01b231 100644 --- a/src/ui/actions/ve.ui.ListAction.js +++ b/src/ui/actions/ve.ui.ListAction.js @@ -31,32 +31,61 @@ * @static * @property */ -ve.ui.ListAction.static.methods = [ 'wrap', 'unwrap', 'toggle' ]; +ve.ui.ListAction.static.methods = [ 'wrap', 'unwrap', 'toggle', 'wrapOnce' ]; /* Methods */ /** - * Toggle a list around content. + * Check if the current selection is wrapped in a list of a given style * * @method - * @param {string} style List style, e.g. 'number' or 'bullet' - * @return {boolean} Action was executed + * @param {string|null} style List style, e.g. 'number' or 'bullet', or null for any style + * @return {boolean} Current selection is all wrapped in a list */ -ve.ui.ListAction.prototype.toggle = function ( style ) { +ve.ui.ListAction.prototype.allWrapped = function ( style ) { var i, len, + attributes = style ? { style: style } : undefined, nodes = this.surface.getModel().getFragment().getLeafNodes(), all = !!nodes.length; for ( i = 0, len = nodes.length; i < len; i++ ) { if ( ( len === 1 || !nodes[i].range || nodes[i].range.getLength() ) && - !nodes[i].node.hasMatchingAncestor( 'list', { style: style } ) + !nodes[i].node.hasMatchingAncestor( 'list', attributes ) ) { all = false; break; } } - return this[all ? 'unwrap' : 'wrap']( style ); + return all; +}; + +/** + * Toggle a list around content. + * + * @method + * @param {string} style List style, e.g. 'number' or 'bullet' + * @param {boolean} noBreakpoints Don't create breakpoints + * @return {boolean} Action was executed + */ +ve.ui.ListAction.prototype.toggle = function ( style, noBreakpoints ) { + return this[this.allWrapped( style ) ? 'unwrap' : 'wrap']( style, noBreakpoints ); +}; + +/** + * Add a list around content only if it has no list already. + * + * @method + * @param {string} style List style, e.g. 'number' or 'bullet' + * @param {boolean} noBreakpoints Don't create breakpoints + * @return {boolean} Action was executed + */ +ve.ui.ListAction.prototype.wrapOnce = function ( style, noBreakpoints ) { + // Check for a list of any style + if ( !this.allWrapped() ) { + return this.wrap( style, noBreakpoints ); + } + return false; }; /** @@ -66,9 +95,10 @@ * * @method * @param {string} style List style, e.g. 'number' or 'bullet' + * @param {boolean} noBreakpoints Don't create breakpoints * @return {boolean} Action was executed */ -ve.ui.ListAction.prototype.wrap = function ( style ) { +ve.ui.ListAction.prototype.wrap = function ( style, noBreakpoints ) { var tx, i, previousList, groupRange, group, range, surfaceModel = this.surface.getModel(), documentModel = surfaceModel.getDocument(), @@ -81,7 +111,9 @@ range = selection.getRange(); - surfaceModel.breakpoint(); + if ( !noBreakpoints ) { + surfaceModel.breakpoint(); + } // TODO: Would be good to refactor at some point and avoid/abstract path split for block slug // and not block slug. @@ -153,7 +185,9 @@ } } } - surfaceModel.breakpoint(); + if ( !noBreakpoints ) { + surfaceModel.breakpoint(); + } this.surface.getView().focus(); return true; }; @@ -164,9 +198,10 @@ * TODO: Refactor functionality into {ve.dm.SurfaceFragment}. * * @method + * @param {boolean} noBreakpoints Don't create breakpoints * @return {boolean} Action was executed */ -ve.ui.ListAction.prototype.unwrap = function () { +ve.ui.ListAction.prototype.unwrap = function ( noBreakpoints ) { var node, surfaceModel = this.surface.getModel(), documentModel = surfaceModel.getDocument(); @@ -175,13 +210,18 @@ return false; } - surfaceModel.breakpoint(); + if ( !noBreakpoints ) { + surfaceModel.breakpoint(); + } do { node = documentModel.getBranchNodeFromOffset( surfaceModel.getSelection().getRange().start ); } while ( node.hasMatchingAncestor( 'list' ) && this.surface.execute( 'indentation', 'decrease' ) ); - surfaceModel.breakpoint(); + if ( !noBreakpoints ) { + surfaceModel.breakpoint(); + } + this.surface.getView().focus(); return true; }; diff --git a/src/ui/ve.ui.CommandRegistry.js b/src/ui/ve.ui.CommandRegistry.js index d9277ef..9235d9a 100644 --- a/src/ui/ve.ui.CommandRegistry.js +++ b/src/ui/ve.ui.CommandRegistry.js @@ -124,6 +124,18 @@ ); ve.ui.commandRegistry.register( new ve.ui.Command( + 'numberWrapOnce', 'list', 'wrapOnce', + { args: ['number', true], supportedSelections: ['linear'] } + ) +); +ve.ui.commandRegistry.register( + new ve.ui.Command( + 'bulletWrapOnce', 'list', 'wrapOnce', + { args: ['bullet', true], supportedSelections: ['linear'] } + ) +); +ve.ui.commandRegistry.register( + new ve.ui.Command( 'commandHelp', 'window', 'open', { args: ['commandHelp'] } ) ); diff --git a/src/ui/ve.ui.Sequence.js b/src/ui/ve.ui.Sequence.js new file mode 100644 index 0000000..276cbed --- /dev/null +++ b/src/ui/ve.ui.Sequence.js @@ -0,0 +1,87 @@ +/*! + * VisualEditor UserInterface Sequence class. + * + * @copyright 2011-2014 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/** + * Key sequence. + * + * @class + * + * @constructor + */ +ve.ui.Sequence = function VeUiSequence( name, commandName, data, strip ) { + this.name = name; + this.commandName = commandName; + this.data = data; + this.strip = strip; +}; + +/* Inheritance */ + +OO.initClass( ve.ui.Sequence ); + +/* Methods */ + +/** + * Check if the sequence matches a given offset in the data + * + * @param {string|Array} data String or linear data + * @param {number} offset Offset + * @return {boolean} Sequence matches + */ +ve.ui.Sequence.prototype.match = function ( data, offset ) { + var i, j = offset - 1; + + for ( i = this.data.length - 1; i >= 0; i--, j-- ) { + if ( typeof this.data[i] === 'string' ) { + if ( this.data[i] !== data.getCharacterData( j ) ) { + return false; + } + } else if ( !ve.compare( this.data[i], data.getData( j ), true ) ) { + return false; + } + } + return true; +}; + +/** + * Execute the command associated with the sequence + * + * @param {ve.ui.Surface} surface surface + * @return {boolean} The command executed + * @throws {Error} Command not found + */ +ve.ui.Sequence.prototype.execute = function ( surface ) { + var range, executed, stripFragment, + surfaceModel = surface.getModel(), + command = ve.ui.commandRegistry.lookup( this.getCommandName() ); + + if ( !command ) { + throw new Error( 'Command not found: ' + this.getCommandName() ) ; + } + + if ( this.strip ) { + range = surfaceModel.getSelection().getRange(); + stripFragment = surfaceModel.getLinearFragment( new ve.Range( range.end, range.end - this.strip ) ); + } + + surfaceModel.breakpoint(); + + executed = command.execute( surface ); + + if ( executed && stripFragment ) { + stripFragment.removeContent(); + } + + return executed; +}; + +ve.ui.Sequence.prototype.getName = function () { + return this.name; +}; + +ve.ui.Sequence.prototype.getCommandName = function () { + return this.commandName; +}; diff --git a/src/ui/ve.ui.SequenceRegistry.js b/src/ui/ve.ui.SequenceRegistry.js new file mode 100644 index 0000000..bdb791e --- /dev/null +++ b/src/ui/ve.ui.SequenceRegistry.js @@ -0,0 +1,69 @@ +/*! + * VisualEditor UserInterface SequenceRegistry class. + * + * @copyright 2011-2014 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/** + * Sequence registry. + * + * @class + * @extends OO.Registry + * @constructor + */ +ve.ui.SequenceRegistry = function VeUiSequenceRegistry() { + // Parent constructor + ve.ui.SequenceRegistry.super.call( this ); +}; + +/* Inheritance */ + +OO.inheritClass( ve.ui.SequenceRegistry, OO.Registry ); + +/** + * Register a sequence with the factory. + * + * @method + * @param {ve.ui.Sequence} sequence Sequence object + * @throws {Error} If sequence is not an instance of ve.ui.Sequence + */ +ve.ui.SequenceRegistry.prototype.register = function ( sequence ) { + // Validate arguments + if ( !( sequence instanceof ve.ui.Sequence ) ) { + throw new Error( + 'sequence must be an instance of ve.ui.Sequence, cannot be a ' + typeof sequence + ); + } + + ve.ui.SequenceRegistry.super.prototype.register.call( this, sequence.getName(), sequence ); +}; + +/** + * Find sequence matches a given offset in the data + * + * @param {ve.dm.ElementLinearData} data Linear data + * @param {number} offset Offset + * @return {ve.ui.Sequence[]} Sequences which match + */ +ve.ui.SequenceRegistry.prototype.findMatching = function ( data, offset ) { + var name, sequences = []; + for ( name in this.registry ) { + if ( this.registry[name].match( data, offset ) ) { + sequences.push( this.registry[name] ); + } + } + return sequences; +}; + +/* Initialization */ + +ve.ui.sequenceRegistry = new ve.ui.SequenceRegistry(); + +/* Registrations */ + +ve.ui.sequenceRegistry.register( + new ve.ui.Sequence( 'bulletStar', 'bulletWrapOnce', [ { type: 'paragraph' }, '*', ' ' ], 2 ) +); +ve.ui.sequenceRegistry.register( + new ve.ui.Sequence( 'numberDot', 'numberWrapOnce', [ { type: 'paragraph' }, '1', '.', ' ' ], 3 ) +); diff --git a/tests/index.html b/tests/index.html index 4778413..69ca0c0 100644 --- a/tests/index.html +++ b/tests/index.html @@ -247,6 +247,8 @@ <script src="../src/ui/ve.ui.CommandRegistry.js"></script> <script src="../src/ui/ve.ui.Trigger.js"></script> <script src="../src/ui/ve.ui.TriggerRegistry.js"></script> + <script src="../src/ui/ve.ui.Sequence.js"></script> + <script src="../src/ui/ve.ui.SequenceRegistry.js"></script> <script src="../src/ui/ve.ui.Action.js"></script> <script src="../src/ui/ve.ui.ActionFactory.js"></script> <script src="../src/ui/ve.ui.FileDropHandler.js"></script> -- To view, visit https://gerrit.wikimedia.org/r/175334 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I6a4d71d845ecde460193466d9d8d12a0c9050c98 Gerrit-PatchSet: 1 Gerrit-Project: VisualEditor/VisualEditor Gerrit-Branch: master Gerrit-Owner: Esanders <esand...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits