Cleaned up toolbar annotation updates

Change-Id: I8cefd6c871ed6d1ad71f64eb66236de1b8b719b5
This commit is contained in:
Trevor Parscal 2012-06-13 14:31:33 -07:00
parent 26e0f6ed5f
commit 169b7e754d
8 changed files with 259 additions and 220 deletions

View file

@ -11,24 +11,24 @@ ve.ce.Surface = function( $container, model ) {
// Properties
this.model = model;
this.documentView = new ve.ce.Document( model.getDocument() );
this.contextView = new ve.ui.Context( this );
// Properties
this.documentView = null; // See initialization below
this.contextView = null; // See initialization below
this.$ = $container;
this.clipboard = {};
// Driven by mousedown and mouseup events
this.isMouseDown = false;
this.range = {
anchorNode: null,
anchorOffset: null,
focusNode: null,
focusOffset: null
'anchorNode': null,
'anchorOffset': null,
'focusNode': null,
'focusOffset': null
};
// init rangy in case of Toshiba...
rangy.init();
// Tracked using mouse events
this.isMouseDown = false;
// Events
this.model.on( 'select', ve.proxy( this.onSelect, this ) );
this.model.on( 'transact', ve.proxy( this.onTransact, this ) );
this.$.on( {
'keypress': ve.proxy( this.onKeyPress, this ),
'keydown': ve.proxy( this.onKeyDown, this ),
@ -43,31 +43,38 @@ ve.ce.Surface = function( $container, model ) {
return false;
}
} );
if ($.browser.msie) {
this.$.on('beforepaste', ve.proxy( this.onPaste, this ) );
if ( $.browser.msie ) {
this.$.on( 'beforepaste', ve.proxy( this.onPaste, this ) );
}
this.model.on( 'select', ve.proxy( this.onSelect, this ) );
// Initialization
this.$.append( this.documentView.documentNode.$ );
try {
document.execCommand( "enableObjectResizing", false, false );
document.execCommand( "enableInlineTableEditing", false, false );
} catch (e) { }
document.execCommand( 'enableObjectResizing', false, false );
document.execCommand( 'enableInlineTableEditing', false, false );
} catch ( e ) {
// Silently ignore
}
// Initialize rangy in case of Toshiba...
rangy.init();
// Must be initialized after select and transact listeners are added to model so respond first
this.documentView = new ve.ce.Document( model.getDocument() );
this.contextView = new ve.ui.Context( this );
this.$.append( this.documentView.documentNode.$ );
};
/* Methods */
ve.ce.Surface.prototype.onSelect = function( e ) {
console.log("onSelect", e);
ve.ce.Surface.prototype.onSelect = function( range ) {
//console.log( 'onSelect', range );
};
ve.ce.Surface.prototype.onTransact = function( tx ) {
//console.log( 'onTransact', tx );
this.showSelection( this.model.getSelection() );
};
ve.ce.Surface.prototype.onKeyPress = function( e ) {
console.log('onKeyPress');
//console.log( 'onKeyPress' );
switch ( e.which ) {
// Enter
case 13:
@ -78,13 +85,12 @@ ve.ce.Surface.prototype.onKeyPress = function( e ) {
};
ve.ce.Surface.prototype.onKeyDown = function( e ) {
console.log('onKeyDown');
//console.log( 'onKeyDown' );
// Prevent all interactions coming from keyboard when mouse is down (possibly selecting)
if ( this.isMouseDown === true ) {
e.preventDefault();
return false;
}
switch ( e.keyCode ) {
// Left arrow
case 37:
@ -94,12 +100,9 @@ ve.ce.Surface.prototype.onKeyDown = function( e ) {
break;
// Backspace
case 8:
tx = ve.dm.Transaction.newFromRemoval( this.documentView.model, this.model.getSelection() );
ve.dm.TransactionProcessor.commit( this.documentView.model, tx );
this.showCursor(this.model.getSelection().start);
e.preventDefault();
break;
// Delete
@ -111,7 +114,6 @@ ve.ce.Surface.prototype.onKeyDown = function( e ) {
ve.ce.Surface.prototype.onMouseDown = function( e ) {
this.isMouseDown = true;
// TODO: Add special handling for clicking on images and alien nodes
var $leaf = $( e.target ).closest( '.ve-ce-leafNode' );
};

View file

@ -494,7 +494,7 @@ ve.dm.Document.prototype.getDataFromNode = function( node ) {
*
* @method
* @param {Integer} offset Offset to get annotations for
* @returns {Object[]} A copy of all annotation objects offset is covered by
* @returns {Object} A copy of all annotation objects offset is covered by
*/
ve.dm.Document.prototype.getAnnotationsFromOffset = function( offset ) {
var annotations;
@ -516,7 +516,7 @@ ve.dm.Document.prototype.getAnnotationsFromOffset = function( offset ) {
if ( ve.isPlainObject( annotations ) ) {
//return ve.getObjectValues( annotations );
return annotations;
return ve.extendObject( {}, annotations );
}
return {};
};
@ -642,99 +642,59 @@ ve.dm.Document.getMatchingAnnotations = function( annotations, pattern ) {
return matches;
};
/**
* Returns an annotation from annotations that match a regular expression.
*
* @static
* @method
* @param {Array} annotations Annotations to search through
* @param {RegExp} pattern Regular expression pattern to match with
* @returns {Object} Annotation object
*/
ve.dm.Document.getMatchingAnnotation = function( annotations, pattern ) {
if ( !( pattern instanceof RegExp ) ) {
throw 'Invalid Pattern. Pattern not instance of RegExp';
}
if ( ve.isPlainObject( annotations ) ) {
for ( var hash in annotations ) {
if ( pattern.test( annotations[hash].type ) ){
return annotations[hash];
}
}
}
return;
};
/**
* Quick check for annotation inside annotations object
*
* @static
* @method
* @param {Object} annotations Annotations to search through
* @param {Object} pattern Regular expression pattern to match with
* @returns {Boolean} if annotation in annotations object
*/
ve.dm.Document.annotationsContainAnnotation = function( annotations, annotation ) {
var contains = false;
$.each(annotations, function(i, val){
if ( ve.compareObjects(val, annotation) ) {
contains = true;
}
});
return contains;
};
/**
* Gets an array of common annnotations across a range.
*
* @method
* @param {Integer} offset Offset to get annotations for
* @returns {Object[]} A copy of all annotation objects offset is covered by
* @param {Boolean} [all] Get all annotations found within the range, not just those that cover it
* @returns {Object} A copy of all annotation objects offset is covered by
*/
ve.dm.Document.prototype.getAnnotationsFromRange = function( range ) {
var currentChar = {},
annotations = {},
charCount = 0,
map = {};
ve.dm.Document.prototype.getAnnotationsFromRange = function( range, all ) {
range.normalize();
var annotations = {},
count = 0,
left,
right,
hash;
// Shorcut for zero-length ranges
if ( range.getLength() === 0 ) {
return this.getAnnotationsFromOffset( range.to );
return {};
}
for ( var i = range.start; i < range.end; i++ ) {
// skip non characters
// There's at least one character, get it's annotations
left = this.getAnnotationsFromOffset( range.start );
// Shorcut for single character ranges
if ( range.getLength() === 1 ) {
return left;
}
// Iterator over the range, looking for annotations, starting at the 2nd character
for ( var i = range.start + 1; i < range.end; i++ ) {
// Skip non character data
if ( ve.dm.Document.isElementData( this.data, i ) ) {
continue;
}
//current character annotations
currentChar = this.data[i][1];
// if a non annotated character, no commonality.
if ( currentChar === undefined ) {
return annotations;
}
charCount++;
// if current char annotations are not the same as previous char.
if ( ve.compareObjects( map, currentChar ) === false) {
//retain common annotations
if ( charCount > 1 ) {
// look for annotation in map
for ( var a in currentChar ) {
if( map[a] === undefined ) {
delete currentChar[a];
}
// Current character annotations
right = this.getAnnotationsFromOffset( i );
if ( all && right !== undefined ) {
ve.extendObject( left, right );
} else if ( !all ) {
// A non annotated character indicates there's no full coverage
if ( right === undefined ) {
return {};
}
// Exclude annotations that are in left but not right
for ( hash in left ) {
if ( right[hash] === undefined ) {
delete left[hash];
}
}
// If we've reduced left down to nothing, just stop looking
if ( ve.isEmptyObject( left ) ) {
break;
}
}
//save map
map = currentChar;
}
// build array of annotations
for ( var key in map ) {
annotations[key] = map[key];
}
return annotations;
return left;
};
/**

View file

@ -103,15 +103,6 @@ ve.dm.Surface.prototype.transact = function( transaction ) {
*/
ve.dm.Surface.prototype.annotate = function( method, annotation ) {
var selection = this.getSelection();
if ( method === 'toggle' ) {
var annotations = this.getDocument().getAnnotationsFromRange( selection );
if ( annotation in annotations ) {
method = 'clear';
} else {
method = 'set';
}
}
if ( this.selection.getLength() ) {
var tx = ve.dm.Transaction.newFromAnnotation(
this.getDocument(), selection, method, annotation

View file

@ -23,11 +23,11 @@ ve.ui.LinkInspector = function( toolbar, context ) {
return;
}
var surfaceModel = _this.context.getSurfaceView().getModel(),
annotation = _this.getSelectedLinkAnnotation();
var surfaceModel = _this.context.getSurfaceView().getModel(),
annotations = _this.getSelectedLinkAnnotations();
// If link annotation exists, clear it.
if ( annotation !== undefined ) {
surfaceModel.annotate( 'clear', annotation );
for ( var hash in annotations ) {
surfaceModel.annotate( 'clear', annotations[hash] );
}
_this.$locationInput.val( '' );
@ -46,7 +46,7 @@ ve.ui.LinkInspector = function( toolbar, context ) {
/* Methods */
ve.ui.LinkInspector.prototype.getSelectedLinkAnnotation = function(){
ve.ui.LinkInspector.prototype.getSelectedLinkAnnotations = function(){
var surfaceView = this.context.getSurfaceView(),
surfaceModel = surfaceView.getModel(),
documentModel = surfaceModel.getDocument(),
@ -54,9 +54,9 @@ ve.ui.LinkInspector.prototype.getSelectedLinkAnnotation = function(){
if ( data.length ) {
if ( ve.isPlainObject( data[0][1] ) ) {
var annotation = ve.dm.Document.getMatchingAnnotation( data[0][1], /link\/.*/ );
if ( ve.isPlainObject(annotation) ) {
return annotation;
var annotations = ve.dm.Document.getMatchingAnnotations( data[0][1], /link\/.*/ );
for ( var hash in annotations ) {
return annotations[hash];
}
}
}
@ -64,10 +64,12 @@ ve.ui.LinkInspector.prototype.getSelectedLinkAnnotation = function(){
};
ve.ui.LinkInspector.prototype.getTitleFromSelection = function() {
var annotation = this.getSelectedLinkAnnotation();
if ( annotation && annotation.data && annotation.data.title ) {
return annotation.data.title;
var annotations = this.getSelectedLinkAnnotations();
for ( var hash in annotations ) {
// Use the first one that has a title (there should only be one, but this is just in case)
if ( annotations[hash].data && annotations[hash].data.title ) {
return annotations[hash].data.title;
}
}
return null;
};
@ -96,11 +98,11 @@ ve.ui.LinkInspector.prototype.onClose = function( accept ) {
return;
}
var surfaceModel = this.context.getSurfaceView().getModel(),
annotation = this.getSelectedLinkAnnotation();
annotations = this.getSelectedLinkAnnotations();
// Clear link annotation if it exists
if ( annotation !== undefined ) {
surfaceModel.annotate( 'clear', annotation );
for ( var hash in annotations ) {
surfaceModel.annotate( 'clear', annotations[hash] );
}
surfaceModel.annotate( 'set', { 'type': 'link/wikiLink', 'data': { 'title': title } } );
}

View file

@ -46,13 +46,16 @@ ve.ui.AnnotationButtonTool.prototype.onClick = function() {
};
ve.ui.AnnotationButtonTool.prototype.updateState = function( annotations, nodes ) {
if ( ve.dm.Document.annotationsContainAnnotation(annotations, this.annotation) ) {
this.$.addClass( 'es-toolbarButtonTool-down' );
this.active = true;
return;
var matches = ve.dm.Document.getMatchingAnnotations(
annotations, new RegExp( '^' + this.annotation.type + '$' )
);
if ( ve.isEmptyObject( matches ) ) {
this.$.removeClass( 'es-toolbarButtonTool-down' );
this.active = false;
} else {
this.$.addClass( 'es-toolbarButtonTool-down' );
this.active = true;
}
this.$.removeClass( 'es-toolbarButtonTool-down' );
this.active = false;
};
/* Registration */

View file

@ -18,7 +18,7 @@ ve.ui.ClearButtonTool = function( toolbar, name, title ) {
/* Methods */
ve.ui.ClearButtonTool.prototype.getAnnotation = function(){
ve.ui.ClearButtonTool.prototype.getAnnotations = function(){
var surfaceView = this.toolbar.getSurfaceView(),
surfaceModel = surfaceView.getModel(),
documentModel = surfaceModel.getDocument(),
@ -26,10 +26,7 @@ ve.ui.ClearButtonTool.prototype.getAnnotation = function(){
if ( data.length ) {
if ( ve.isPlainObject( data[0][1] ) ) {
var annotation = ve.dm.Document.getMatchingAnnotation( data[0][1], this.pattern );
if ( ve.isPlainObject( annotation ) ) {
return annotation;
}
return ve.dm.Document.getMatchingAnnotations( data[0][1], this.pattern );
}
}
return ;
@ -37,9 +34,11 @@ ve.ui.ClearButtonTool.prototype.getAnnotation = function(){
ve.ui.ClearButtonTool.prototype.onClick = function() {
var surfaceView = this.toolbar.getSurfaceView(),
model = surfaceView.getModel();
model.annotate( 'clear', this.getAnnotation() );
model = surfaceView.getModel(),
annotations = this.getAnnotations();
for ( var hash in annotations ) {
model.annotate( 'clear', annotations[hash] );
}
surfaceView.showSelection( model.getSelection() );
surfaceView.contextView.closeInspector();
};

View file

@ -32,27 +32,26 @@ ve.ui.Toolbar = function( $container, surfaceView, config ) {
/* Methods */
ve.ui.Toolbar.prototype.updateTools = function( e ) {
var _this = this,
model = _this.surfaceView.getModel(),
/**
* Triggers update events on all tools.
*
* @method
*/
ve.ui.Toolbar.prototype.updateTools = function() {
var model = this.surfaceView.getModel(),
doc = model.getDocument(),
annotations,
nodes = [],
range = model.getSelection(),
startNode,
endNode;
// On transact: set e, and redraw selection
if ( e.from === undefined ) {
e = model.getSelection();
this.surfaceView.showSelection( e );
}
if( e !== null ) {
if ( e.from === e.to ){
nodes.push( doc.getNodeFromOffset( e.from ) );
if ( range !== null ) {
if ( range.from === range.to ){
nodes.push( doc.getNodeFromOffset( range.from ) );
} else {
startNode = doc.getNodeFromOffset( e.from );
endNode = doc.getNodeFromOffset ( e.end );
startNode = doc.getNodeFromOffset( range.from );
endNode = doc.getNodeFromOffset ( range.end );
// These should be different, alas just in case.
if ( startNode === endNode ) {
nodes.push( startNode );
@ -67,22 +66,24 @@ ve.ui.Toolbar.prototype.updateTools = function( e ) {
}
}
// Update Context
if ( e.getLength() > 0 ) {
_this.surfaceView.contextView.set();
if ( range.getLength() > 0 ) {
this.surfaceView.contextView.set();
annotations = doc.getAnnotationsFromRange( range );
} else {
_this.surfaceView.contextView.clear();
this.surfaceView.contextView.clear();
annotations = doc.getAnnotationsFromOffset(
doc.getNearestContentOffset( range.start - 1 )
);
}
annotations = doc.getAnnotationsFromRange( e );
// Update state
for ( i = 0; i < _this.tools.length; i++ ) {
_this.tools[i].updateState( annotations, nodes );
for ( i = 0; i < this.tools.length; i++ ) {
this.tools[i].updateState( annotations, nodes );
}
} else {
// Clear state
_this.surfaceView.contextView.clear();
for ( i = 0; i < _this.tools.length; i++ ) {
_this.tools[i].clearState();
this.surfaceView.contextView.clear();
for ( i = 0; i < this.tools.length; i++ ) {
this.tools[i].clearState();
}
}
};

View file

@ -205,12 +205,9 @@ test( 'getAnnotationsFromOffset', 1, function() {
} );
test( 'getAnnotationsFromRange', 1, function() {
var doc,
range,
annotations,
cases = [
var cases = [
{
'msg': 'all bold',
'msg': 'single annotations',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ]
@ -218,38 +215,41 @@ test( 'getAnnotationsFromRange', 1, function() {
'expected': { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } }
},
{
'msg': 'bold and italic',
'msg': 'mutliple annotations',
'data': [
['a',
[
'a',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
],
['b',
[
'b',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
]
],
'expected':
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
'expected': {
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
},
{
'msg': 'bold and italic',
'msg': 'lowest common coverage',
'data': [
['a',
[
'a',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
],
['b',
[
'b',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'},
@ -257,22 +257,67 @@ test( 'getAnnotationsFromRange', 1, function() {
}
]
],
'expected':
'expected': {
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
},
{
'msg': 'no common coverage due to plain character at the start',
'data': [
['a'],
[
'b',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'},
'{"type":"textStyle/underline"}': { 'type': 'textStyle/underline'}
}
],
[
'c',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
]
],
'expected': {}
},
{
'msg': 'none common, non annotated character at end',
'msg': 'no common coverage due to plain character in the middle',
'data': [
['a',
[
'a',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'},
'{"type":"textStyle/underline"}': { 'type': 'textStyle/underline'}
}
],
['b'],
[
'c',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
]
],
'expected': {}
},
{
'msg': 'no common coverage due to plain character at the end',
'data': [
[
'a',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
],
['b',
[
'b',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'},
@ -284,27 +329,7 @@ test( 'getAnnotationsFromRange', 1, function() {
'expected': {}
},
{
'msg': 'none common, reverse of previous',
'data': [
['a'],
['b',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'},
'{"type":"textStyle/underline"}': { 'type': 'textStyle/underline'}
}
],
['c',
{
'{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type":"textStyle/italic"}': { 'type': 'textStyle/italic'}
}
]
],
'expected': {}
},
{
'msg': 'all different',
'msg': 'no common coverage due to mismatched annotations',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' } } ]
@ -312,22 +337,78 @@ test( 'getAnnotationsFromRange', 1, function() {
'expected': {}
},
{
'msg': 'no annotations',
'msg': 'annotations are collected using all with mismatched annotations',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' } } ]
],
'all': true,
'expected': {
'{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' }
}
},
{
'msg': 'annotations are collected using all, even with a plain character at the start',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' } } ],
['c']
],
'all': true,
'expected': {
'{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' }
}
},
{
'msg': 'annotations are collected using all, even with a plain character at the middle',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' } } ],
['c']
],
'all': true,
'expected': {
'{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' }
}
},
{
'msg': 'annotations are collected using all, even with a plain character at the end',
'data': [
['a', { '{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' } } ],
['b', { '{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' } } ],
['c']
],
'all': true,
'expected': {
'{"type:"textStyle/bold"}': { 'type': 'textStyle/bold' },
'{"type:"textStyle/italic"}': { 'type': 'textStyle/italic' }
}
},
{
'msg': 'no common coverage from all plain characters',
'data': ['a', 'b'],
'expected': {}
},
{
'msg': 'no common coverage using all from all plain characters',
'data': ['a', 'b'],
'all': true,
'expected': {}
}
];
expect( cases.length );
for ( var i = 0; i < cases.length; i++ ) {
doc = new ve.dm.Document ( cases[i].data );
range = new ve.Range( 0, doc.getData().length );
annotations = doc.getAnnotationsFromRange( range );
var doc = new ve.dm.Document ( cases[i].data );
deepEqual(
annotations, cases[i].expected, cases[i].msg
doc.getAnnotationsFromRange( new ve.Range( 0, cases[i].data.length ), cases[i].all ),
cases[i].expected,
cases[i].msg
);
}
} );