Move ve2/ back to ve/

Change-Id: Ie51d8e48171fb1f84045d1560ee603cee62b91f6
This commit is contained in:
Catrope 2012-06-19 18:20:28 -07:00
parent fc4ba3019a
commit 6afed5e5cc
210 changed files with 4072 additions and 17524 deletions

View file

@ -1,18 +1,17 @@
/**
* Creates an ve.ce.DocumentNode object.
*
* ContentEditable node for a document.
*
* @class
* @constructor
* @extends {ve.ce.BranchNode}
* @param {ve.dm.DocumentNode} documentModel Document model to view
* @param {ve.ce.Surface} surfaceView Surface view this view is a child of
* @param model {ve.dm.DocumentNode} Model to observe
*/
ve.ce.DocumentNode = function( model, surfaceView ) {
ve.ce.DocumentNode = function( model, surface ) {
// Inheritance
ve.ce.BranchNode.call( this, model );
ve.ce.BranchNode.call( this, 'document', model );
// Properties
this.surfaceView = surfaceView;
this.surface = surface;
// DOM Changes
this.$.addClass( 've-ce-documentNode' );
@ -22,51 +21,32 @@ ve.ce.DocumentNode = function( model, surfaceView ) {
/* Static Members */
/**
* Mapping of symbolic names and splitting rules.
*
* Each rule is an object with a self and children property. Each of these properties may contain
* one of two possible values:
* Boolean - Whether a split is allowed
* Null - Node is a leaf, so there's nothing to split
*
* @example Paragraph rules
* {
* 'self': true
* 'children': null
* }
* @example List rules
* {
* 'self': false,
* 'children': true
* }
* @example ListItem rules
* {
* 'self': true,
* 'children': false
* }
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.DocumentNode.splitRules = {};
ve.ce.DocumentNode.rules = {
'canBeSplit': false
};
/* Methods */
/**
* Get the document offset of a position created from passed DOM event
*
* Gets the outer length, which for a document node is the same as the inner length.
*
* @method
* @param e {Event} Event to create ve.Position from
* @returns {Integer} Document offset
* @returns {Integer} Length of the entire node
*/
ve.ce.DocumentNode.prototype.getOffsetFromEvent = function( e ) {
var position = ve.Position.newFromEventPagePosition( e );
return this.getOffsetFromRenderedPosition( position );
ve.ce.DocumentNode.prototype.getOuterLength = function() {
return this.length;
};
ve.ce.DocumentNode.splitRules.document = {
'self': false,
'children': true
};
/* Registration */
ve.ce.nodeFactory.register( 'document', ve.ce.DocumentNode );
/* Inheritance */

View file

@ -1,36 +1,41 @@
/**
* Creates an ve.ce.HeadingNode object.
*
* ContentEditable node for a heading.
*
* @class
* @constructor
* @extends {ve.ce.LeafNode}
* @param {ve.dm.HeadingNode} model Heading model to view
* @extends {ve.ce.BranchNode}
* @param model {ve.dm.HeadingNode} Model to observe
*/
ve.ce.HeadingNode = function( model ) {
// Inheritance
var level = model.getElementAttribute( 'level' ),
type = ve.ce.HeadingNode.domNodeTypes[level];
if ( type === undefined ) {
throw 'Invalid level attribute for heading node: ' + level;
}
ve.ce.LeafNode.call( this, model, $( '<' + type + '></' + type + '>' ) );
// Properties
this.currentLevelHash = level;
// DOM Changes
this.$.addClass( 've-ce-headingNode' );
ve.ce.BranchNode.call(
this, 'heading', model, ve.ce.BranchNode.getDomWrapper( model, 'level' )
);
// Events
var _this = this;
this.model.on( 'update', function() {
_this.setLevel();
} );
this.model.addListenerMethod( this, 'update', 'onUpdate' );
};
/* Static Members */
ve.ce.HeadingNode.domNodeTypes = {
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.HeadingNode.rules = {
'canBeSplit': true
};
/**
* Mapping of heading level values and DOM wrapper element types.
*
* @static
* @member
*/
ve.ce.HeadingNode.domWrapperElementTypes = {
'1': 'h1',
'2': 'h2',
'3': 'h3',
@ -41,25 +46,21 @@ ve.ce.HeadingNode.domNodeTypes = {
/* Methods */
ve.ce.HeadingNode.prototype.setLevel = function() {
var level = this.model.getElementAttribute( 'level' ),
type = ve.ce.HeadingNode.domNodeTypes[level];
if ( type === undefined ) {
throw 'Invalid level attribute for heading node: ' + level;
}
if ( level !== this.currentLevelHash ) {
this.currentLevelHash = level;
this.convertDomElement( type );
}
/**
* Responds to model update events.
*
* If the level changed since last update the DOM wrapper will be replaced with an appropriate one.
*
* @method
*/
ve.ce.HeadingNode.prototype.onUpdate = function() {
this.updateDomWrapper( 'level' );
};
/* Registration */
ve.ce.DocumentNode.splitRules.heading = {
'self': true,
'children': null
};
ve.ce.nodeFactory.register( 'heading', ve.ce.HeadingNode );
/* Inheritance */
ve.extendClass( ve.ce.HeadingNode, ve.ce.LeafNode );
ve.extendClass( ve.ce.HeadingNode, ve.ce.BranchNode );

View file

@ -1,58 +1,32 @@
/**
* Creates an ve.ce.ListItemNode object.
*
* ContentEditable node for a list item.
*
* @class
* @constructor
* @extends {ve.ce.LeafNode}
* @param {ve.dm.ListItemNode} model List item model to view
* @extends {ve.ce.BranchNode}
* @param model {ve.dm.ListItemNode} Model to observe
*/
ve.ce.ListItemNode = function( model ) {
// Inheritance
var style = model.getElementAttribute( 'style' ),
type = ve.ce.ListItemNode.domNodeTypes[style];
ve.ce.BranchNode.call( this, model, $( '<' + type + '></' + type + '>' ) );
// Properties
this.currentStylesHash = null;
// DOM Changes
this.$.addClass( 've-ce-listItemNode' );
// Events
var _this = this;
this.model.on( 'update', function() {
_this.setStyle();
} );
ve.ce.BranchNode.call( this, 'listItem', model, $( '<li></li>' ) );
};
/* Static Members */
ve.ce.ListItemNode.domNodeTypes = {
'item': 'li',
'definition': 'dd',
'term': 'dt'
};
/* Methods */
ve.ce.ListItemNode.prototype.setStyle = function() {
var style = this.model.getElementAttribute( 'style' ),
type = ve.ce.ListItemNode.domNodeTypes[style];
if ( type === undefined ) {
throw 'Invalid style attribute for heading node: ' + style;
}
if ( style !== this.currentStyleHash ) {
this.currentStyleHash = style;
this.convertDomElement( type );
}
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.ListItemNode.rules = {
'canBeSplit': true
};
/* Registration */
ve.ce.DocumentNode.splitRules.listItem = {
'self': true,
'children': false
};
ve.ce.nodeFactory.register( 'listItem', ve.ce.ListItemNode );
/* Inheritance */

View file

@ -1,58 +1,59 @@
/**
* Creates an ve.ce.ListNode object.
*
* ContentEditable node for a list.
*
* @class
* @constructor
* @extends {ve.ce.BranchNode}
* @param {ve.dm.ListNode} model List model to view
* @param model {ve.dm.ListNode} Model to observe
*/
ve.ce.ListNode = function( model ) {
// Inheritance
var style = model.getElementAttribute( 'style' ),
type = ve.ce.ListNode.domNodeTypes[style];
ve.ce.BranchNode.call( this, model, $( '<' + type + '></' + type + '>' ) );
// Properties
this.currentStylesHash = null;
// DOM Changes
this.$.addClass( 've-ce-listNode' );
ve.ce.BranchNode.call( this, 'list', model, ve.ce.BranchNode.getDomWrapper( model, 'style' ) );
// Events
var _this = this;
this.model.on( 'update', function() {
_this.setStyle();
} );
this.model.addListenerMethod( this, 'update', 'onUpdate' );
};
/* Static Members */
ve.ce.ListNode.domNodeTypes = {
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.ListNode.rules = {
'canBeSplit': false
};
/**
* Mapping of list style values and DOM wrapper element types.
*
* @static
* @member
*/
ve.ce.ListNode.domWrapperElementTypes = {
'bullet': 'ul',
'number': 'ol',
'definition': 'dl'
'number': 'ol'
};
/* Methods */
ve.ce.ListNode.prototype.setStyle = function() {
var style = this.model.getElementAttribute( 'style' ),
type = ve.ce.ListItemNode.domNodeTypes[style];
if ( type === undefined ) {
throw 'Invalid style attribute for heading node: ' + style;
}
if ( style !== this.currentStyleHash ) {
this.currentStyleHash = style;
this.convertDomElement( type );
}
/**
* Responds to model update events.
*
* If the style changed since last update the DOM wrapper will be replaced with an appropriate one.
*
* @method
*/
ve.ce.ListNode.prototype.onUpdate = function() {
this.updateDomWrapper( 'style' );
};
/* Registration */
ve.ce.DocumentNode.splitRules.list = {
'self': false,
'children': true
};
ve.ce.nodeFactory.register( 'list', ve.ce.ListNode );
/* Inheritance */

View file

@ -1,26 +1,33 @@
/**
* Creates an ve.ce.ParagraphNode object.
*
* ContentEditable node for a paragraph.
*
* @class
* @constructor
* @extends {ve.ce.LeafNode}
* @param {ve.dm.ParagraphNode} model Paragraph model to view
* @extends {ve.ce.BranchNode}
* @param model {ve.dm.ParagraphNode} Model to observe
*/
ve.ce.ParagraphNode = function( model ) {
// Inheritance
ve.ce.LeafNode.call( this, model, $( '<p></p>' ) );
ve.ce.BranchNode.call( this, 'paragraph', model, $( '<p></p>' ) );
};
// DOM Changes
this.$.addClass( 've-ce-paragraphNode' );
/* Static Members */
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.ParagraphNode.rules = {
'canBeSplit': true
};
/* Registration */
ve.ce.DocumentNode.splitRules.paragraph = {
'self': true,
'children': null
};
ve.ce.nodeFactory.register( 'paragraph', ve.ce.ParagraphNode );
/* Inheritance */
ve.extendClass( ve.ce.ParagraphNode, ve.ce.LeafNode );
ve.extendClass( ve.ce.ParagraphNode, ve.ce.BranchNode );

View file

@ -1,26 +0,0 @@
/**
* Creates an ve.ce.PreNode object.
*
* @class
* @constructor
* @extends {ve.ce.LeafNode}
* @param {ve.dm.PreNode} model Pre model to view
*/
ve.ce.PreNode = function( model ) {
// Inheritance
ve.ce.LeafNode.call( this, model, $( '<pre></pre>' ), { 'pre': true } );
// DOM Changes
this.$.addClass( 've-ce-preNode' );
};
/* Registration */
ve.ce.DocumentNode.splitRules.pre = {
'self': true,
'children': null
};
/* Inheritance */
ve.extendClass( ve.ce.PreNode, ve.ce.LeafNode );

View file

@ -1,27 +1,61 @@
/**
* Creates an ve.ce.TableCellNode object.
*
* ContentEditable node for a table cell.
*
* @class
* @constructor
* @extends {ve.ce.BranchNode}
* @param {ve.dm.TableCellNode} model Table cell model to view
* @param model {ve.dm.TableCellNode} Model to observe
*/
ve.ce.TableCellNode = function( model ) {
// Inheritance
ve.ce.BranchNode.call( this, model, $( '<td></td>' ) );
ve.ce.BranchNode.call(
this, 'tableCell', model, ve.ce.BranchNode.getDomWrapper( model, 'style' )
);
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 've-ce-tableCellNode' );
// Events
this.model.addListenerMethod( this, 'update', 'onUpdate' );
};
/* Static Members */
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.TableCellNode.rules = {
'canBeSplit': false
};
/**
* Mapping of list item style values and DOM wrapper element types.
*
* @static
* @member
*/
ve.ce.TableCellNode.domWrapperElementTypes = {
'data': 'td',
'header': 'th'
};
/* Methods */
/**
* Responds to model update events.
*
* If the style changed since last update the DOM wrapper will be replaced with an appropriate one.
*
* @method
*/
ve.ce.TableCellNode.prototype.onUpdate = function() {
this.updateDomWrapper( 'style' );
};
/* Registration */
ve.ce.DocumentNode.splitRules.tableCell = {
'self': false,
'children': true
};
ve.ce.nodeFactory.register( 'tableCell', ve.ce.TableCellNode );
/* Inheritance */

View file

@ -1,27 +1,32 @@
/**
* Creates an ve.ce.TableNode object.
*
* ContentEditable node for a table.
*
* @class
* @constructor
* @extends {ve.ce.BranchNode}
* @param {ve.dm.TableNode} model Table model to view
* @param model {ve.dm.TableNode} Model to observe
*/
ve.ce.TableNode = function( model ) {
// Inheritance
ve.ce.BranchNode.call( this, model, $( '<table></table>' ) );
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 've-ce-tableNode' );
ve.ce.BranchNode.call( this, 'table', model, $( '<table border="1" cellpadding="5" cellspacing="5"></table>' ) );
};
/* Static Members */
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.TableNode.rules = {
'canBeSplit': false
};
/* Registration */
ve.ce.DocumentNode.splitRules.table = {
'self': false,
'children': false
};
ve.ce.nodeFactory.register( 'table', ve.ce.TableNode );
/* Inheritance */

View file

@ -1,27 +1,32 @@
/**
* Creates an ve.ce.TableRowNode object.
*
* ContentEditable node for a table row.
*
* @class
* @constructor
* @extends {ve.ce.BranchNode}
* @param {ve.dm.TableRowNode} model Table row model to view
* @param model {ve.dm.TableRowNode} Model to observe
*/
ve.ce.TableRowNode = function( model ) {
// Inheritance
ve.ce.BranchNode.call( this, model, $( '<tr></tr>' ), true );
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 've-ce-tableRowNode' );
ve.ce.BranchNode.call( this, 'tableRow', model, $( '<tr></tr>' ) );
};
/* Static Members */
/**
* Node rules.
*
* @see ve.ce.NodeFactory
* @static
* @member
*/
ve.ce.TableRowNode.rules = {
'canBeSplit': false
};
/* Registration */
ve.ce.DocumentNode.splitRules.tableRow = {
'self': false,
'children': false
};
ve.ce.nodeFactory.register( 'tableRow', ve.ce.TableRowNode );
/* Inheritance */

View file

@ -1,89 +0,0 @@
.ve-ce-content {
position: relative;
z-index: 1;
}
.ve-ce-content-line,
.ve-ce-content-ruler {
line-height: 1.5em;
cursor: text;
white-space: nowrap;
color: #000000;
}
.ve-ce-content-ruler {
position: absolute;
top: 0;
left: 0;
display: inline-block;
z-index: -1000;
}
.ve-ce-content-line.empty {
display: block;
width: 0px;
}
.ve-ce-content-whitespace {
color: #ffffff;
}
.ve-ce-content-range {
display: none;
position: absolute;
background-color: #b3d6f6;
cursor: text;
z-index: -1;
}
.ve-ce-content-format-object {
background-color: rgba(0,0,0,0.05);
border-radius: 0.25em;
margin: 1px 0 1px 1px;
padding: 0.25em 0;
cursor: default;
}
.ve-ce-content-format-object * {
cursor: default !important;
}
.ve-ce-content-format-object a:link,
.ve-ce-content-format-object a:visited,
.ve-ce-content-format-object a:active {
color: #0645AD;
text-decoration: none;
}
.ve-ce-content-format-textStyle-italic,
.ve-ce-content-format-textStyle-emphasize {
font-style: italic;
}
.ve-ce-content-format-textStyle-bold,
.ve-ce-content-format-textStyle-strong {
font-weight: bold;
}
.ve-ce-content-format-link {
color: #0645AD;
text-decoration: underline;
}
.ve-ce-content-format-textStyle-big {
font-size: 1.2em;
}
.ve-ce-content-format-textStyle-small,
.ve-ce-content-format-textStyle-subScript,
.ve-ce-content-format-textStyle-superScript {
font-size: .8em;
}
.ve-ce-content-format-textStyle-subScript {
vertical-align: sub;
}
.ve-ce-content-format-textStyle-superScript {
vertical-align: super;
}

View file

@ -1,10 +1,10 @@
.ve-ce-documentNode {
cursor: text;
overflow: hidden;
/*-webkit-user-select: none;*/
font-size: 0.8em;
}
/* Prevent focus outline on editor */
[contenteditable="true"] {
.ve-ce-documentNode[contenteditable="true"]:focus {
outline: none;
}

View file

@ -1,17 +0,0 @@
.ve-ce-alienBlockNode {
background-color: rgba(255,255,186,0.3);
border: rgba(0,0,0,0.3) dashed 1px;
display: block;
}
.ve-ce-alienInlineNode {
background-color: rgba(255,255,186,0.3);
border: rgba(0,0,0,0.3) dashed 1px;
display: inline;
}
.ve-ce-slug {
display: inline-block;
margin-right: -1px;
width: 1px;
}

View file

@ -25,3 +25,18 @@
width: 1px;
display: none;
}
#paste {
display: none;
height: 1px;
left: 0;
opacity: 0;
overflow: hidden;
position: fixed;
top: 0;
width: 1px;
}
#paste * {
height: 1px !important;
width: 1px !important;
}

View file

@ -1,114 +1,142 @@
/**
* Creates an ve.ce.BranchNode object.
*
* ContentEditable node that can have branch or leaf children.
*
* @class
* @abstract
* @constructor
* @extends {ve.BranchNode}
* @extends {ve.ce.Node}
* @param model {ve.ModelNode} Model to observe
* @param {String} type Symbolic name of node type
* @param model {ve.dm.BranchNode} Model to observe
* @param {jQuery} [$element] Element to use as a container
*/
ve.ce.BranchNode = function( model, $element, horizontal ) {
ve.ce.BranchNode = function( type, model, $element ) {
// Inheritance
ve.BranchNode.call( this );
ve.ce.Node.call( this, model, $element );
ve.ce.Node.call( this, type, model, $element );
// Properties
this.horizontal = horizontal || false;
this.domWrapperElementType = this.$.get(0).nodeName.toLowerCase();
this.$slugs = $();
if ( model ) {
// Append existing model children
var childModels = model.getChildren();
for ( var i = 0; i < childModels.length; i++ ) {
this.onAfterPush( childModels[i] );
}
// Events
this.model.addListenerMethod( this, 'splice', 'onSplice' );
// Observe and mimic changes on model
this.model.addListenerMethods( this, {
'afterPush': 'onAfterPush',
'afterUnshift': 'onAfterUnshift',
'afterPop': 'onAfterPop',
'afterShift': 'onAfterShift',
'afterSplice': 'onAfterSplice',
'afterSort': 'onAfterSort',
'afterReverse': 'onAfterReverse'
} );
// DOM Changes
this.$.addClass( 've-ce-branchNode' );
// Initialization
if ( model.getChildren().length ) {
this.onSplice.apply( this, [0, 0].concat( model.getChildren() ) );
}
};
/* Static Members */
ve.ce.BranchNode.$slugTemplate = $( '<span class="ve-ce-slug">&#xFEFF;</span>' );
/* Static Methods */
/**
* Gets the appropriate element type for the DOM wrapper of a node.
*
* This method reads the {key} attribute from a {model} and looks up a type in the node's statically
* defined {domWrapperElementTypes} member, which is a mapping of possible values of that attribute
* and DOM element types.
*
* @method
* @param {ve.dm.BranchNode} model Model node is based on
* @param {String} key Attribute name to read type value from
* @returns {String} DOM element type for wrapper
* @throws 'Undefined attribute' if attribute is not defined in the model
* @throws 'Invalid attribute value' if attribute value is not a key in {domWrapperElementTypes}
*/
ve.ce.BranchNode.getDomWrapperType = function( model, key ) {
var value = model.getAttribute( key );
if ( value === undefined ) {
throw 'Undefined attribute: ' + key;
}
var types = ve.ce.nodeFactory.lookup( model.getType() ).domWrapperElementTypes;
if ( types[value] === undefined ) {
throw 'Invalid attribute value: ' + value;
}
return types[value];
};
/**
* Gets a jQuery selection of a new DOM wrapper for a node.
*
* This method uses {getDomWrapperType} to determine the proper element type to use.
*
* @method
* @param {ve.dm.BranchNode} model Model node is based on
* @param {String} key Attribute name to read type value from
* @returns {jQuery} Selection of DOM wrapper
*/
ve.ce.BranchNode.getDomWrapper = function( model, key ) {
var type = ve.ce.BranchNode.getDomWrapperType( model, key );
return $( '<' + type + '></' + type + '>' );
};
/* Methods */
ve.ce.BranchNode.prototype.onAfterPush = function( childModel ) {
var childView = childModel.createView();
this.emit( 'beforePush', childView );
childView.attach( this );
childView.on( 'update', this.emitUpdate );
// Update children
this.children.push( childView );
// Update DOM
this.$.append( childView.$ );
// TODO: adding and deleting classes has to be implemented for unshift, shift, splice, sort
// and reverse as well
if ( this.children.length === 1 ) {
childView.$.addClass('es-viewBranchNode-firstChild');
/**
* Updates the DOM wrapper of this node if needed.
*
* This method uses {getDomWrapperType} to determine the proper element type to use.
*
* WARNING: The contents, .data( 'node' ) and any classes the wrapper already has will be moved to
* the new wrapper, but other attributes and any other information added using $.data() will be
* lost upon updating the wrapper. To retain information added to the wrapper, subscribe to the
* 'rewrap' event and copy information from the {$old} wrapper the {$new} wrapper.
*
* @method
* @param {String} key Attribute name to read type value from
* @emits rewrap ($old, $new)
*/
ve.ce.BranchNode.prototype.updateDomWrapper = function( key ) {
var type = ve.ce.BranchNode.getDomWrapperType( this.model, key );
if ( type !== this.domWrapperElementType ) {
var $element = $( '<' + type + '></' + type + '>' );
// Copy classes
$element.attr( 'class', this.$.attr( 'class' ) );
// Copy .data( 'node' )
$element.data( 'node', this.$.data( 'node' ) );
// Move contents
$element.append( this.$.contents() );
// Emit an event that can be handled to copy other things over if needed
this.emit( 'rewrap', this.$, $element );
// Swap elements
this.$.replaceWith( $element );
// Use new element from now on
this.$ = $element;
}
this.emit( 'afterPush', childView );
this.emit( 'update' );
};
ve.ce.BranchNode.prototype.onAfterUnshift = function( childModel ) {
var childView = childModel.createView();
this.emit( 'beforeUnshift', childView );
childView.attach( this );
childView.on( 'update', this.emitUpdate );
// Update children
this.children.unshift( childView );
// Update DOM
this.$.prepend( childView.$ );
this.emit( 'afterUnshift', childView );
this.emit( 'update' );
};
ve.ce.BranchNode.prototype.onAfterPop = function() {
this.emit( 'beforePop' );
// Update children
var childView = this.children.pop();
childView.detach();
childView.removeEventListener( 'update', this.emitUpdate );
// Update DOM
childView.$.detach();
this.emit( 'afterPop' );
this.emit( 'update' );
};
ve.ce.BranchNode.prototype.onAfterShift = function() {
this.emit( 'beforeShift' );
// Update children
var childView = this.children.shift();
childView.detach();
childView.removeEventListener( 'update', this.emitUpdate );
// Update DOM
childView.$.detach();
this.emit( 'afterShift' );
this.emit( 'update' );
};
ve.ce.BranchNode.prototype.onAfterSplice = function( index, howmany ) {
/**
* Responds to splice events on a ve.dm.BranchNode.
*
* ve.ce.Node objects are generated from the inserted ve.dm.Node objects, producing a view that's a
* mirror of it's model.
*
* @method
* @param {Integer} index Index to remove and or insert nodes at
* @param {Integer} howmany Number of nodes to remove
* @param {ve.dm.BranchNode} [...] Variadic list of nodes to insert
*/
ve.ce.BranchNode.prototype.onSplice = function( index, howmany ) {
var i,
length,
args = Array.prototype.slice.call( arguments, 0 );
// Convert models to views and attach them to this node
if ( args.length >= 3 ) {
for ( i = 2, length = args.length; i < length; i++ ) {
args[i] = args[i].createView();
args[i] = ve.ce.nodeFactory.create( args[i].getType(), args[i] );
}
}
this.emit.apply( this, ['beforeSplice'].concat( args ) );
var removals = this.children.splice.apply( this.children, args );
for ( i = 0, length = removals.length; i < length; i++ ) {
removals[i].detach();
removals[i].removeListener( 'update', this.emitUpdate );
// Update DOM
removals[i].$.detach();
}
@ -116,11 +144,10 @@ ve.ce.BranchNode.prototype.onAfterSplice = function( index, howmany ) {
var $target;
if ( index ) {
// Get the element before the insertion point
$anchor = this.$.children().eq( index - 1 );
$anchor = this.$.children(':not(.ve-ce-slug)').eq( index - 1 );
}
for ( i = args.length - 1; i >= 2; i-- ) {
args[i].attach( this );
args[i].on( 'update', this.emitUpdate );
if ( index ) {
$anchor.after( args[i].$ );
} else {
@ -128,144 +155,66 @@ ve.ce.BranchNode.prototype.onAfterSplice = function( index, howmany ) {
}
}
}
this.emit.apply( this, ['afterSplice'].concat( args ) );
if ( args.length >= 3 ) {
for ( i = 2, length = args.length; i < length; i++ ) {
args[i].renderContent();
}
}
this.emit( 'update' );
};
ve.ce.BranchNode.prototype.onAfterSort = function() {
this.emit( 'beforeSort' );
var childModels = this.model.getChildren();
for ( var i = 0; i < childModels.length; i++ ) {
for ( var j = 0; j < this.children.length; j++ ) {
if ( this.children[j].getModel() === childModels[i] ) {
var childView = this.children[j];
// Update children
this.children.splice( j, 1 );
this.children.push( childView );
// Update DOM
this.$.append( childView.$ );
// Remove all slugs in this branch
this.$slugs.remove();
var $slug = ve.ce.BranchNode.$slugTemplate.clone();
if ( this.canHaveGrandchildren() ) {
$slug.css( 'display', 'block');
}
// Iterate over all children of this branch and add slugs in appropriate places
for ( i = 0; i < this.children.length; i++ ) {
if ( this.children[i].canHaveSlug() ) {
if ( i === 0 ) {
// First sluggable child (left side)
this.$slugs = this.$slugs.add(
$slug.clone().insertBefore( this.children[i].$ )
);
}
if (
// Last sluggable child (right side)
i === this.children.length - 1 ||
// Sluggable child followed by another sluggable child (in between)
( this.children[i + 1] && this.children[i + 1].canHaveSlug() )
) {
this.$slugs = this.$slugs.add(
$slug.clone().insertAfter( this.children[i].$ )
);
}
}
}
this.emit( 'afterSort' );
this.emit( 'update' );
this.renderContent();
};
ve.ce.BranchNode.prototype.onAfterReverse = function() {
this.emit( 'beforeReverse' );
// Update children
this.reverse();
// Update DOM
this.$.children().each( function() {
$(this).prependTo( $(this).parent() );
} );
this.emit( 'afterReverse' );
this.emit( 'update' );
this.renderContent();
};
/**
* Render content.
*
* @method
*/
ve.ce.BranchNode.prototype.renderContent = function() {
ve.ce.BranchNode.prototype.hasSlugAtOffset = function( offset ) {
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].renderContent();
}
};
/**
* Draw selection around a given range.
*
* @method
* @param {ve.Range} range Range of content to draw selection around
*/
ve.ce.BranchNode.prototype.drawSelection = function( range ) {
var selectedNodes = this.selectNodes( range, true );
for ( var i = 0; i < this.children.length; i++ ) {
if ( selectedNodes.length && this.children[i] === selectedNodes[0].node ) {
for ( var j = 0; j < selectedNodes.length; j++ ) {
selectedNodes[j].node.drawSelection( selectedNodes[j].range );
if ( this.children[i].canHaveSlug() ) {
var nodeOffset = this.children[i].model.getRoot().getOffsetFromNode( this.children[i].model );
var nodeLength = this.children[i].model.getOuterLength();
if ( i === 0 ) {
if ( nodeOffset === offset ) {
return true;
}
}
if ( i === this.children.length - 1 || ( this.children[i + 1] && this.children[i + 1].canHaveSlug() ) ) {
if ( nodeOffset + nodeLength === offset ) {
return true;
}
}
i += selectedNodes.length - 1;
} else {
this.children[i].clearSelection();
}
}
return false;
};
/**
* Clear selection.
*
* @method
*/
ve.ce.BranchNode.prototype.clearSelection = function() {
ve.ce.BranchNode.prototype.clean = function() {
this.$.empty();
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].clearSelection();
this.$.append( this.children[i].$ );
}
};
/**
* Gets the nearest offset of a rendered position.
*
* @method
* @param {ve.Position} position Position to get offset for
* @returns {Integer} Offset of position
*/
ve.ce.BranchNode.prototype.getOffsetFromRenderedPosition = function( position ) {
if ( this.children.length === 0 ) {
return 0;
}
var node = this.children[0];
for ( var i = 1; i < this.children.length; i++ ) {
if ( this.horizontal && this.children[i].$.offset().left > position.left ) {
break;
} else if ( !this.horizontal && this.children[i].$.offset().top > position.top ) {
break;
}
node = this.children[i];
}
return node.getParent().getOffsetFromNode( node, true ) +
node.getOffsetFromRenderedPosition( position ) + 1;
};
/**
* Gets rendered position of offset within content.
*
* @method
* @param {Integer} offset Offset to get position for
* @returns {ve.Position} Position of offset
*/
ve.ce.BranchNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
var node = this.getNodeFromOffset( offset, true );
if ( node !== null ) {
return node.getRenderedPositionFromOffset(
offset - this.getOffsetFromNode( node, true ) - 1,
leftBias
);
}
return null;
};
ve.ce.BranchNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
var node = this.getNodeFromOffset( offset, true );
if ( node !== null ) {
var nodeOffset = this.getOffsetFromNode( node, true );
return ve.Range.newFromTranslatedRange(
node.getRenderedLineRangeFromOffset( offset - nodeOffset - 1 ),
nodeOffset + 1
);
}
return null;
};
/* Inheritance */
ve.extendClass( ve.ce.BranchNode, ve.BranchNode );

View file

@ -1,259 +0,0 @@
ve.ce.Content = function( model, $container, parent ) {
// Inheritance
ve.EventEmitter.call( this );
// Properties
this.$ = $container;
this.model = model;
this.parent = parent;
if ( model ) {
// Events
var _this = this;
this.model.on( 'update', function( offset ) {
var surfaceView = _this.getSurfaceView();
if (surfaceView.autoRender) {
_this.render( offset || 0 );
}
} );
}
};
/* Static Members */
/**
* List of annotation rendering implementations.
*
* Each supported annotation renderer must have an open and close property, each either a string or
* a function which accepts a data argument.
*
* @static
* @member
*/
ve.ce.Content.annotationRenderers = {
'object/template': {
'open': function( data ) {
return '<span class="ve-ce-content-format-object">' + data.html;
},
'close': '</span>'
},
'object/hook': {
'open': function( data ) {
return '<span class="ve-ce-content-format-object">' + data.html;
},
'close': '</span>'
},
'textStyle/bold': {
'open': '<b>',
'close': '</b>'
},
'textStyle/italic': {
'open': '<i>',
'close': '</i>'
},
'textStyle/strong': {
'open': '<span class="ve-ce-content-format-textStyle-strong">',
'close': '</span>'
},
'textStyle/emphasize': {
'open': '<span class="ve-ce-content-format-textStyle-emphasize">',
'close': '</span>'
},
'textStyle/big': {
'open': '<span class="ve-ce-content-format-textStyle-big">',
'close': '</span>'
},
'textStyle/small': {
'open': '<span class="ve-ce-content-format-textStyle-small">',
'close': '</span>'
},
'textStyle/superScript': {
'open': '<span class="ve-ce-content-format-textStyle-superScript">',
'close': '</span>'
},
'textStyle/subScript': {
'open': '<span class="ve-ce-content-format-textStyle-subScript">',
'close': '</span>'
},
'link/external': {
'open': function( data ) {
return '<span class="ve-ce-content-format-link" data-href="' + data.href + '">';
},
'close': '</span>'
},
'link/internal': {
'open': function( data ) {
return '<span class="ve-ce-content-format-link" data-title="wiki/' + data.title + '">';
},
'close': '</span>'
}
};
/**
* Mapping of character and HTML entities or renderings.
*
* @static
* @member
*/
ve.ce.Content.htmlCharacters = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'\'': '&#039;',
'"': '&quot;',
'\n': '<span class="ve-ce-content-whitespace">&#182;</span>',
'\t': '<span class="ve-ce-content-whitespace">&#8702;</span>'
};
/* Static Methods */
/**
* Gets a rendered opening or closing of an annotation.
*
* Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
* argument should be used while rendering content.
*
* @static
* @method
* @param {String} bias Which side of the annotation to render, either "open" or "close"
* @param {Object} annotation Annotation to render
* @param {Array} stack List of currently open annotations
* @returns {String} Rendered annotation
*/
ve.ce.Content.renderAnnotation = function( bias, annotation, stack ) {
var renderers = ve.ce.Content.annotationRenderers,
type = annotation.type,
out = '';
if ( type in renderers ) {
if ( bias === 'open' ) {
// Add annotation to the top of the stack
stack.push( annotation );
// Open annotation
out += typeof renderers[type].open === 'function' ?
renderers[type].open( annotation.data ) : renderers[type].open;
} else {
if ( stack[stack.length - 1] === annotation ) {
// Remove annotation from top of the stack
stack.pop();
// Close annotation
out += typeof renderers[type].close === 'function' ?
renderers[type].close( annotation.data ) : renderers[type].close;
} else {
// Find the annotation in the stack
var depth = ve.inArray( annotation, stack ),
i;
if ( depth === -1 ) {
throw 'Invalid stack error. An element is missing from the stack.';
}
// Close each already opened annotation
for ( i = stack.length - 1; i >= depth + 1; i-- ) {
out += typeof renderers[stack[i].type].close === 'function' ?
renderers[stack[i].type].close( stack[i].data ) :
renderers[stack[i].type].close;
}
// Close the buried annotation
out += typeof renderers[type].close === 'function' ?
renderers[type].close( annotation.data ) : renderers[type].close;
// Re-open each previously opened annotation
for ( i = depth + 1; i < stack.length; i++ ) {
out += typeof renderers[stack[i].type].open === 'function' ?
renderers[stack[i].type].open( stack[i].data ) :
renderers[stack[i].type].open;
}
// Remove the annotation from the middle of the stack
stack.splice( depth, 1 );
}
}
}
return out;
};
/* Methods */
ve.ce.Content.prototype.render = function( offset ) {
this.$.html( this.getHtml( 0, this.model.getContentLength() ) );
};
ve.ce.Content.prototype.setContainer = function( $container ) {
this.$ = $container;
this.render();
};
/**
* Gets an HTML rendering of a range of data within content model.
*
* @method
* @param {ve.Range} range Range of content to render
* @param {String} Rendered HTML of data within content model
*/
ve.ce.Content.prototype.getHtml = function( range, options ) {
if ( range ) {
range.normalize();
} else {
range = { 'start': 0, 'end': undefined };
}
var data = this.model.getContentData(),
render = ve.ce.Content.renderAnnotation,
htmlChars = ve.ce.Content.htmlCharacters;
var out = '',
left = '',
right,
leftPlain,
rightPlain,
stack = [],
chr,
i,
j;
for ( i = 0; i < data.length; i++ ) {
right = data[i];
leftPlain = typeof left === 'string';
rightPlain = typeof right === 'string';
if ( !leftPlain && rightPlain ) {
// [formatted][plain] pair, close any annotations for left
for ( j = 1; j < left.length; j++ ) {
out += render( 'close', left[j], stack );
}
} else if ( leftPlain && !rightPlain ) {
// [plain][formatted] pair, open any annotations for right
for ( j = 1; j < right.length; j++ ) {
out += render( 'open', right[j], stack );
}
} else if ( !leftPlain && !rightPlain ) {
// [formatted][formatted] pair, open/close any differences
for ( j = 1; j < left.length; j++ ) {
if ( ve.inArray( left[j], right ) === -1 ) {
out += render( 'close', left[j], stack );
}
}
for ( j = 1; j < right.length; j++ ) {
if ( ve.inArray( right[j], left ) === -1 ) {
out += render( 'open', right[j], stack );
}
}
}
chr = rightPlain ? right : right[0];
out += chr in htmlChars ? htmlChars[chr] : chr;
left = right;
}
// Close all remaining tags at the end of the content
if ( !rightPlain && right ) {
for ( j = 1; j < right.length; j++ ) {
out += render( 'close', right[j], stack );
}
}
return out;
};
ve.ce.Content.prototype.getSurfaceView = function() {
var view = this;
while(!view.surfaceView) {
view = view.parent;
}
return view.surfaceView;
};
/* Inheritance */
ve.extendClass( ve.ce.Content, ve.EventEmitter );

View file

@ -1,59 +0,0 @@
ve.ce.ContentObserver = function( documentView ) {
// Inheritance
ve.EventEmitter.call( this );
this.$node = null;
this.interval = null;
this.frequency = 100;
this.prevText = null;
this.prevHash = null;
};
ve.ce.ContentObserver.prototype.setElement = function( $node ) {
if ( this.$node !== $node ) {
this.stop();
this.$node = $node;
this.prevText = ve.ce.Surface.getDOMText2( this.$node[0] );
this.prevHash = ve.ce.Surface.getDOMHash( this.$node[0] );
this.start();
}
};
ve.ce.ContentObserver.prototype.stop = function() {
if ( this.interval !== null ) {
clearInterval( this.interval );
this.interval = null;
this.poll();
this.$node = null;
}
};
ve.ce.ContentObserver.prototype.start = function() {
this.poll();
var _this = this;
setTimeout( function() { _this.poll(); }, 0);
this.interval = setInterval( function() { _this.poll(); }, this.frequency );
};
ve.ce.ContentObserver.prototype.poll = function() {
var text = ve.ce.Surface.getDOMText2( this.$node[0] );
var hash = ve.ce.Surface.getDOMHash( this.$node[0] );
if ( text !== this.prevText || hash !== this.prevHash ) {
this.emit('change', {
$node: this.$node,
prevText: this.prevText,
text: text,
prevHash: this.prevHash,
hash: hash
} );
this.prevText = text;
this.prevHash = hash;
}
};
/* Inheritance */
ve.extendClass( ve.ce.ContentObserver , ve.EventEmitter );

View file

@ -1,98 +0,0 @@
ve.ce.CursorObserver = function( documentView ) {
// Inheritance
ve.EventEmitter.call( this );
this.documentView = documentView;
this.anchorNode = null;
this.anchorOffset = null;
this.focusNode = null;
this.focusOffset = null;
};
ve.ce.CursorObserver.prototype.update = function() {
var _this = this;
setTimeout( function() {
if ( !_this.documentView.$.is(':focus') ) {
if (
_this.anchorNode !== null ||
_this.anchorOffset !== null ||
_this.focusNode !== null ||
_this.focusOffset !== null
) {
_this.anchorNode = _this.anchorOffset = _this.focusNode = _this.focusOffset = null;
_this.emit( 'change', null );
}
} else {
var rangySel = rangy.getSelection(),
range;
if ( rangySel.anchorNode !== _this.anchorNode ||
rangySel.anchorOffset !== _this.anchorOffset ||
rangySel.focusNode !== _this.focusNode ||
rangySel.focusOffset !== _this.focusOffset
) {
_this.anchorNode = rangySel.anchorNode;
_this.anchorOffset = rangySel.anchorOffset;
_this.focusNode = rangySel.focusNode;
_this.focusOffset = rangySel.focusOffset;
if ( rangySel.isCollapsed ) {
range = new ve.Range( _this.getOffset( _this.anchorNode, _this.anchorOffset ) );
} else {
range = new ve.Range(
_this.getOffset( _this.anchorNode, _this.anchorOffset ),
_this.getOffset( _this.focusNode, _this.focusOffset )
);
}
_this.emit( 'change', range );
}
}
}, 0 );
};
ve.ce.CursorObserver.prototype.getOffset = function( selectionNode, selectionOffset ) {
var $leafNode = ve.ce.Surface.getLeafNode( selectionNode ),
current = [$leafNode.contents(), 0],
stack = [current],
offset = 0;
while ( stack.length > 0 ) {
if ( current[1] >= current[0].length ) {
stack.pop();
current = stack[ stack.length - 1 ];
continue;
}
var item = current[0][current[1]];
var $item = current[0].eq( current[1] );
if ( item.nodeType === 3 ) {
if ( item === selectionNode ) {
offset += selectionOffset;
break;
} else {
offset += item.textContent.length;
}
} else if ( item.nodeType === 1 ) {
if ( $( item ).attr( 'contentEditable' ) === 'false' ) {
offset += 1;
} else {
if ( item === selectionNode ) {
offset += selectionOffset;
break;
}
stack.push( [$item.contents(), 0] );
current[1]++;
current = stack[stack.length-1];
continue;
}
}
current[1]++;
}
return this.documentView.getOffsetFromNode(
$leafNode.data( 'view' )
) + 1 + offset;
};
/* Inheritance */
ve.extendClass( ve.ce.CursorObserver , ve.EventEmitter );

View file

@ -1,48 +1,24 @@
/**
* Creates an ve.ce.LeafNode object.
*
* ContentEditable node that can not have any children.
*
* @class
* @abstract
* @constructor
* @extends {ve.LeafNode}
* @extends {ve.ce.Node}
* @param model {ve.ModelNode} Model to observe
* @param {String} type Symbolic name of node type
* @param model {ve.dm.LeafNode} Model to observe
* @param {jQuery} [$element] Element to use as a container
*/
ve.ce.LeafNode = function( model, $element ) {
ve.ce.LeafNode = function( type, model, $element ) {
// Inheritance
ve.LeafNode.call( this );
ve.ce.Node.call( this, model, $element );
ve.ce.Node.call( this, type, model, $element );
this.$.data( 'view', this );
this.$.addClass( 've-ce-leafNode' );
// Properties
this.contentView = new ve.ce.Content( model, this.$, this );
// Events
this.contentView.on( 'update', this.emitUpdate );
};
/* Methods */
ve.ce.LeafNode.prototype.convertDomElement = function( type ) {
ve.ce.Node.prototype.call( this, type );
// Transplant content view
this.contentView.setContainer( this.$ );
};
/**
* Render content.
*
* @method
*/
ve.ce.LeafNode.prototype.renderContent = function() {
this.contentView.render();
};
ve.ce.LeafNode.prototype.getDOMText = function() {
return ve.ce.getDOMText( this.$[0] );
// DOM Changes
if ( model.isWrapped() ) {
this.$.addClass( 've-ce-leafNode' );
}
};
/* Inheritance */

View file

@ -1,63 +1,195 @@
/**
* Creates an ve.ce.Node object.
*
* Generic ContentEditable node.
*
* @class
* @abstract
* @constructor
* @extends {ve.Node}
* @param {String} type Symbolic name of node type
* @param {ve.dm.Node} model Model to observe
* @param {jQuery} [$element=$( '<div></div>' )] Element to use as a container
* @param {jQuery} [$element] Element to use as a container
*/
ve.ce.Node = function( model, $element ) {
ve.ce.Node = function( type, model, $element ) {
// Inheritance
ve.Node.call( this );
ve.Node.call( this, type );
// Properties
this.model = model;
this.parent = null;
this.$ = $element || $( '<div></div>' );
this.parent = null;
this.$.data( 'node', this );
};
/* Methods */
ve.ce.Node.prototype.convertDomElement = function( type ) {
// Create new element
var $new = $( '<' + type + '></' + type + '>' );
// Copy classes
$new.attr( 'class', this.$.attr( 'class' ) );
// Move contents
$new.append( this.$.contents() );
// Swap elements
this.$.replaceWith( $new );
// Use new element from now on
this.$ = $new;
/**
* Gets a list of allowed child node types.
*
* This method passes through to the model.
*
* @method
* @returns {String[]|null} List of node types allowed as children or null if any type is allowed
*/
ve.ce.Node.prototype.getChildNodeTypes = function() {
return this.model.getChildNodeTypes();
};
/**
* Gets the length of the element in the model.
*
* Gets a list of allowed parent node types.
*
* This method passes through to the model.
*
* @method
* @see {ve.Node.prototype.getElementLength}
* @returns {Integer} Length of content
* @returns {String[]|null} List of node types allowed as parents or null if any type is allowed
*/
ve.ce.Node.prototype.getElementLength = function() {
return this.model.getElementLength();
ve.ce.Node.prototype.getParentNodeTypes = function() {
return this.model.getParentNodeTypes();
};
/**
* Gets the length of the content in the model.
*
* Checks if model is for a node that can have children.
*
* This method passes through to the model.
*
* @method
* @see {ve.Node.prototype.getContentLength}
* @returns {Integer} Length of content
* @returns {Boolean} Model node can have children
*/
ve.ce.Node.prototype.getContentLength = function() {
return this.model.getContentLength();
ve.ce.Node.prototype.canHaveChildren = function() {
return this.model.canHaveChildren();
};
/**
* Checks if model is for a node that can have children.
*
* This method passes through to the model.
*
* @method
* @returns {Boolean} Model node can have children
*/
ve.ce.Node.prototype.canHaveChildren = function() {
return this.model.canHaveChildren();
};
/**
* Checks if model is for a node that can have grandchildren.
*
* This method passes through to the model.
*
* @method
* @returns {Boolean} Model node can have grandchildren
*/
ve.ce.Node.prototype.canHaveGrandchildren = function() {
return this.model.canHaveGrandchildren();
};
/**
* Checks if model is for a wrapped element.
*
* This method passes through to the model.
*
* @method
* @returns {Boolean} Model node is a wrapped element
*/
ve.ce.Node.prototype.isWrapped = function() {
return this.model.isWrapped();
};
/**
* Checks if this node can contain content.
*
* @method
* @returns {Boolean} Node can contain content
*/
ve.ce.Node.prototype.canContainContent = function() {
return this.model.canContainContent();
};
/**
* Checks if this node is content.
*
* @method
* @returns {Boolean} Node is content
*/
ve.ce.Node.prototype.isContent = function() {
return this.model.isContent();
};
/**
* Checks if this node can have a slug before or after it.
*
* @static
* @method
* @returns {Boolean} Node can have a slug
*/
ve.ce.Node.prototype.canHaveSlug = function() {
return !this.canContainContent() && this.getParentNodeTypes() === null && this.type !== 'text';
};
/**
* Gets model length.
*
* This method passes through to the model.
*
* @method
* @returns {Integer} Model length
*/
ve.ce.Node.prototype.getLength = function() {
return this.model.getLength();
};
/**
* Gets model outer length.
*
* This method passes through to the model.
*
* @method
* @returns {Integer} Model outer length
*/
ve.ce.Node.prototype.getOuterLength = function() {
return this.model.getOuterLength();
};
/**
* Checks if this node can be split.
*
* @method
* @returns {Boolean} Node can be split
*/
ve.ce.Node.prototype.canBeSplit = function() {
return ve.ce.nodeFactory.canNodeBeSplit( this.type );
};
/**
* Gets a reference to the model this node observes.
*
* @method
* @returns {ve.dm.Node} Reference to the model this node observes
*/
ve.ce.Node.prototype.getModel = function() {
return this.model;
};
ve.ce.Node.getSplitableNode = function( node ) {
var splitableNode = null;
ve.Node.traverseUpstream( node, function( node ) {
ve.log(node);
if ( node.canBeSplit() ) {
splitableNode = node;
return true;
} else {
return false;
}
} );
return splitableNode;
};
/**
* Attaches node as a child to another node.
*
*
* @method
* @param {ve.ce.Node} parent Node to attach to
* @emits attach (parent)
@ -69,7 +201,7 @@ ve.ce.Node.prototype.attach = function( parent ) {
/**
* Detaches node from it's parent.
*
*
* @method
* @emits detach (parent)
*/
@ -79,42 +211,6 @@ ve.ce.Node.prototype.detach = function() {
this.emit( 'detach', parent );
};
/**
* Gets a reference to this node's parent.
*
* @method
* @returns {ve.ce.Node} Reference to this node's parent
*/
ve.ce.Node.prototype.getParent = function() {
return this.parent;
};
/**
* Gets a reference to the model this node observes.
*
* @method
* @returns {ve.dm.Node} Reference to the model this node observes
*/
ve.ce.Node.prototype.getModel = function() {
return this.model;
};
ve.ce.Node.getSplitableNode = function( node ) {
var splitableNode = null;
ve.Node.traverseUpstream( node, function( node ) {
var elementType = node.model.getElementType();
if (
splitableNode !== null &&
ve.ce.DocumentNode.splitRules[ elementType ].children === true
) {
return false;
}
splitableNode = ve.ce.DocumentNode.splitRules[ elementType ].self ? node : null;
} );
return splitableNode;
};
/* Inheritance */
ve.extendClass( ve.ce.Node, ve.Node );

File diff suppressed because it is too large Load diff

View file

@ -1,165 +0,0 @@
ve.ce.SurfaceObserver = function( documentView ) {
// Inheritance
ve.EventEmitter.call( this );
this.documentView = documentView;
this.anchorNode = null;
this.anchorOffset = null;
this.focusNode = null;
this.focusOffset = null;
this.range = null;
this.$node = null;
this.interval = null;
this.frequency = 100;
this.prevText = null;
this.prevHash = null;
this.prevRange = null;
var _this = this;
this.on( 'select', function( range ) {
if ( range !== null && range.getLength() === 0 ) {
var node = _this.documentView.getNodeFromOffset( range.start );
_this.setNode( node.$ );
} else {
_this.stop();
}
} );
};
ve.ce.SurfaceObserver.prototype.setNode = function( $node ) {
if ( this.$node !== $node ) {
this.stop();
this.$node = $node;
this.prevText = ve.ce.Surface.getDOMText2( this.$node[0] );
this.prevHash = ve.ce.Surface.getDOMHash( this.$node[0] );
this.start();
}
};
ve.ce.SurfaceObserver.prototype.stop = function() {
if ( this.interval !== null ) {
clearInterval( this.interval );
this.interval = null;
this.poll();
}
};
ve.ce.SurfaceObserver.prototype.start = function() {
this.poll();
var _this = this;
setTimeout( function() {_this.poll(); }, 0);
this.interval = setInterval( function() { _this.poll(); }, this.frequency );
};
ve.ce.SurfaceObserver.prototype.poll = function() {
var text = ve.ce.Surface.getDOMText2( this.$node[0] );
var hash = ve.ce.Surface.getDOMHash( this.$node[0] );
if ( text !== this.prevText || hash !== this.prevHash ) {
console.log(1);
this.emit('change', {
$node: this.$node,
prevText: this.prevText,
text: text,
prevHash: this.prevHash,
hash: hash
} );
this.prevText = text;
this.prevHash = hash;
}
};
ve.ce.SurfaceObserver.prototype.updateCursor = function( async ) {
if ( async ) {
var _this = this;
setTimeout( function() {
_this.updateCursor ( false );
}, 0 );
} else {
if ( !this.documentView.$.is(':focus') ) {
if (
this.anchorNode !== null ||
this.anchorOffset !== null ||
this.focusNode !== null ||
this.focusOffset !== null
) {
this.anchorNode = this.anchorOffset = this.focusNode = this.focusOffset = null;
this.range = null;
this.emit( 'select', this.range );
}
} else {
var rangySel = rangy.getSelection();
if (
rangySel.anchorNode !== this.anchorNode ||
rangySel.anchorOffset !== this.anchorOffset ||
rangySel.focusNode !== this.focusNode ||
rangySel.focusOffset !== this.focusOffset
) {
this.anchorNode = rangySel.anchorNode;
this.anchorOffset = rangySel.anchorOffset;
this.focusNode = rangySel.focusNode;
this.focusOffset = rangySel.focusOffset;
if ( rangySel.isCollapsed ) {
this.range = new ve.Range( this.getOffset( this.anchorNode, this.anchorOffset ) );
} else {
this.range = new ve.Range(
this.getOffset( this.anchorNode, this.anchorOffset ),
this.getOffset( this.focusNode, this.focusOffset )
);
}
this.emit( 'select', this.range );
}
}
}
};
ve.ce.SurfaceObserver.prototype.getOffset = function( selectionNode, selectionOffset ) {
var $leafNode = ve.ce.Surface.getLeafNode( selectionNode ),
current = [$leafNode.contents(), 0],
stack = [current],
offset = 0;
while ( stack.length > 0 ) {
if ( current[1] >= current[0].length ) {
stack.pop();
current = stack[ stack.length - 1 ];
continue;
}
var item = current[0][current[1]];
var $item = current[0].eq( current[1] );
if ( item.nodeType === 3 ) {
if ( item === selectionNode ) {
offset += selectionOffset;
break;
} else {
offset += item.textContent.length;
}
} else if ( item.nodeType === 1 ) {
if ( $( item ).attr( 'contentEditable' ) === 'false' ) {
offset += 1;
} else {
if ( item === selectionNode ) {
offset += selectionOffset;
break;
}
stack.push( [$item.contents(), 0] );
current[1]++;
current = stack[stack.length-1];
continue;
}
}
current[1]++;
}
return this.documentView.getOffsetFromNode(
$leafNode.data( 'view' )
) + 1 + offset;
};
/* Inheritance */
ve.extendClass( ve.ce.SurfaceObserver , ve.EventEmitter );

View file

@ -1,38 +1,90 @@
/**
* VisualEditor ContentEditable namespace.
*
* ContentEditable namespace.
*
* All classes and functions will be attached to this object to keep the global namespace clean.
*/
ve.ce = {
/**
* Gets the plain text of a DOM element.
*
* @static
* @method
* @param {HTMLElement} elem DOM element to get the plan text contents of
* @returns {String} Plain text contents of DOM element
*/
'getDOMText': function( elem ) {
var nodeType = elem.nodeType,
ret = '';
//'nodeFactory': Initialized in ve.ce.NodeFactory.js
};
if ( nodeType === 1 || nodeType === 9 ) {
/**
* RegExp pattern for matching all whitespaces in HTML text.
*
* \u0020 (32) space
* \u00A0 (160) non-breaking space
*
* @static
* @member
*/
ve.ce.whitespacePattern = /[\u0020\u00A0]/g;
/**
* Gets the plain text of a DOM element.
*
* In the returned string only the contents of text nodes are included.
*
* TODO: The idea of using this method over jQuery's .text() was that it will not traverse into
* elements that are not contentEditable, however this appears to be missing.
*
* @static
* @member
* @param {DOMElement} element DOM element to get text of
* @returns {String} Plain text of DOM element
*/
ve.ce.getDomText = function( element ) {
var func = function( element ) {
var nodeType = element.nodeType,
text = '';
if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent || innerText for elements
if ( typeof elem.textContent === 'string' ) {
return elem.textContent;
} else if ( typeof elem.innerText === 'string' ) {
if ( typeof element.textContent === 'string' ) {
return element.textContent;
} else if ( typeof element.innerText === 'string' ) {
// Replace IE's carriage returns
return elem.innerText.replace( /\r\n/g, '' );
return element.innerText.replace( /\r\n/g, '' );
} else {
// Traverse it's children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling) {
ret += ve.ce.getDOMText( elem );
for ( element = element.firstChild; element; element = element.nextSibling) {
text += func( element );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
return element.nodeValue;
}
return ret;
return text;
}
// Return the text, replacing spaces and non-breaking spaces with spaces?
// TODO: Why are we replacing spaces (\u0020) with spaces (' ')
return func( element ).replace( ve.ce.whitespacePattern, ' ' );
};
/**
* Gets a hash of a DOM element's structure.
*
* In the returned string text nodes are repesented as "#" and elements are represented as "<type>"
* and "</type>" where "type" is their element name. This effectively generates an HTML
* serialization without any attributes or text contents. This can be used to observer structural
* changes.
*
* @static
* @member
* @param {DOMElement} element DOM element to get hash of
* @returns {String} Hash of DOM element
*/
ve.ce.getDomHash = function( element ) {
var nodeType = element.nodeType,
nodeName = element.nodeName,
hash = '';
if ( nodeType === 3 || nodeType === 4 ) {
return '#';
} else if ( nodeType === 1 || nodeType === 9 ) {
hash += '<' + nodeName + '>';
// Traverse it's children
for ( element = element.firstChild; element; element = element.nextSibling) {
hash += ve.ce.getDomHash( element );
}
hash += '</' + nodeName + '>';
}
return hash;
};

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,69 @@
/**
* Creates an ve.dm.HeadingNode object.
*
* DataModel node for a heading.
*
* @class
* @constructor
* @extends {ve.dm.LeafNode}
* @param {Object} element Document data element of this node
* @param {Integer} length Length of document data element
* @extends {ve.dm.BranchNode}
* @param {ve.dm.LeafNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.HeadingNode = function( element, length ) {
ve.dm.HeadingNode = function( children, attributes ) {
// Inheritance
ve.dm.LeafNode.call( this, 'heading', element, length );
ve.dm.BranchNode.call( this, 'heading', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a heading view for this model.
*
* @method
* @returns {ve.ce.ParagraphNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.HeadingNode.prototype.createView = function() {
return new ve.ce.HeadingNode( this );
ve.dm.HeadingNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': true,
'childNodeTypes': null,
'parentNodeTypes': null
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.HeadingNode.converters = {
'domElementTypes': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
'toDomElement': function( type, element ) {
return element.attributes && ( {
1: document.createElement( 'h1' ),
2: document.createElement( 'h2' ),
3: document.createElement( 'h3' ),
4: document.createElement( 'h4' ),
5: document.createElement( 'h5' ),
6: document.createElement( 'h6' )
} )[element.attributes['level']];
},
'toDataElement': function( tag, element ) {
return ( {
'h1': { 'type': 'heading', 'attributes': { 'level': 1 } },
'h2': { 'type': 'heading', 'attributes': { 'level': 2 } },
'h3': { 'type': 'heading', 'attributes': { 'level': 3 } },
'h4': { 'type': 'heading', 'attributes': { 'level': 4 } },
'h5': { 'type': 'heading', 'attributes': { 'level': 5 } },
'h6': { 'type': 'heading', 'attributes': { 'level': 6 } }
} )[tag];
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.heading = ve.dm.HeadingNode;
ve.dm.DocumentNode.nodeRules.heading = {
'parents': null,
'children': []
};
ve.dm.nodeFactory.register( 'heading', ve.dm.HeadingNode );
/* Inheritance */
ve.extendClass( ve.dm.HeadingNode, ve.dm.LeafNode );
ve.extendClass( ve.dm.HeadingNode, ve.dm.BranchNode );

View file

@ -1,37 +1,54 @@
/**
* Creates an ve.dm.ListItemNode object.
*
* DataModel node for a list item.
*
* @class
* @constructor
* @extends {ve.dm.LeafNode}
* @param {Object} element Document data element of this node
* @param {Integer} length Length of document data element
* @extends {ve.dm.BranchNode}
* @param {ve.dm.BranchNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.ListItemNode = function( element, contents ) {
ve.dm.ListItemNode = function( children, attributes ) {
// Inheritance
ve.dm.BranchNode.call( this, 'listItem', element, contents );
ve.dm.BranchNode.call( this, 'listItem', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a list item view for this model.
*
* @method
* @returns {ve.ce.ListItemNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.ListItemNode.prototype.createView = function() {
return new ve.ce.ListItemNode( this );
ve.dm.ListItemNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': false,
'childNodeTypes': null,
'parentNodeTypes': ['list']
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.ListItemNode.converters = {
'domElementTypes': ['li'],
'toDomElement': function( type, element ) {
return document.createElement( 'li' );
},
'toDataElement': function( tag, element ) {
return { 'type': 'listItem' };
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.listItem = ve.dm.ListItemNode;
ve.dm.DocumentNode.nodeRules.listItem = {
'parents': ['list'],
'children': null
};
ve.dm.nodeFactory.register( 'listItem', ve.dm.ListItemNode );
/* Inheritance */

View file

@ -1,37 +1,60 @@
/**
* Creates an ve.dm.ListNode object.
*
* DataModel node for a list.
*
* @class
* @constructor
* @extends {ve.dm.BranchNode}
* @param {Object} element Document data element of this node
* @param {ve.dm.ListItemNode[]} contents List of child nodes to initially add
* @param {ve.dm.BranchNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.ListNode = function( element, contents ) {
ve.dm.ListNode = function( children, attributes ) {
// Inheritance
ve.dm.BranchNode.call( this, 'list', element, contents );
ve.dm.BranchNode.call( this, 'list', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a list view for this model.
*
* @method
* @returns {ve.ce.ListNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.ListNode.prototype.createView = function() {
return new ve.ce.ListNode( this );
ve.dm.ListNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': false,
'childNodeTypes': ['listItem'],
'parentNodeTypes': null
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.ListNode.converters = {
'domElementTypes': ['ul', 'ol'],
'toDomElement': function( type, element ) {
return element.attributes && ( {
'bullet': document.createElement( 'ul' ),
'number': document.createElement( 'ol' )
} )[element.attributes['style']];
},
'toDataElement': function( tag, element ) {
return ( {
'ul': { 'type': 'list', 'attributes': { 'style': 'bullet' } },
'ol': { 'type': 'list', 'attributes': { 'style': 'number' } }
} )[tag];
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.list = ve.dm.ListNode;
ve.dm.DocumentNode.nodeRules.list = {
'parents': null,
'children': ['listItem']
};
ve.dm.nodeFactory.register( 'list', ve.dm.ListNode );
/* Inheritance */

View file

@ -1,38 +1,55 @@
/**
* Creates an ve.dm.ParagraphNode object.
*
* DataModel node for a paragraph.
*
* @class
* @constructor
* @extends {ve.dm.LeafNode}
* @param {Object} element Document data element of this node
* @param {Integer} length Length of document data element
* @extends {ve.dm.BranchNode}
* @param {ve.dm.LeafNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.ParagraphNode = function( element, length ) {
ve.dm.ParagraphNode = function( children, attributes ) {
// Inheritance
ve.dm.LeafNode.call( this, 'paragraph', element, length );
ve.dm.BranchNode.call( this, 'paragraph', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a paragraph view for this model.
*
* @method
* @returns {ve.ce.ParagraphNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.ParagraphNode.prototype.createView = function() {
return new ve.ce.ParagraphNode( this );
ve.dm.ParagraphNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': true,
'childNodeTypes': null,
'parentNodeTypes': null
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.ParagraphNode.converters = {
'domElementTypes': ['p'],
'toDomElement': function( type, element ) {
return document.createElement( 'p' );
},
'toDataElement': function( tag, element ) {
return { 'type': 'paragraph' };
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.paragraph = ve.dm.ParagraphNode;
ve.dm.DocumentNode.nodeRules.paragraph = {
'parents': null,
'children': []
};
ve.dm.nodeFactory.register( 'paragraph', ve.dm.ParagraphNode );
/* Inheritance */
ve.extendClass( ve.dm.ParagraphNode, ve.dm.LeafNode );
ve.extendClass( ve.dm.ParagraphNode, ve.dm.BranchNode );

View file

@ -1,38 +0,0 @@
/**
* Creates an ve.dm.PreNode object.
*
* @class
* @constructor
* @extends {ve.dm.LeafNode}
* @param {Object} element Document data element of this node
* @param {Integer} length Length of document data element
*/
ve.dm.PreNode = function( element, length ) {
// Inheritance
ve.dm.LeafNode.call( this, 'pre', element, length );
};
/* Methods */
/**
* Creates a pre view for this model.
*
* @method
* @returns {ve.ce.PreNode}
*/
ve.dm.PreNode.prototype.createView = function() {
return new ve.ce.PreNode( this );
};
/* Registration */
ve.dm.DocumentNode.nodeModels.pre = ve.dm.PreNode;
ve.dm.DocumentNode.nodeRules.pre = {
'parents': null,
'children': []
};
/* Inheritance */
ve.extendClass( ve.dm.PreNode, ve.dm.LeafNode );

View file

@ -1,37 +1,60 @@
/**
* Creates an ve.dm.TableCellNode object.
*
* DataModel node for a table cell.
*
* @class
* @constructor
* @extends {ve.dm.BranchNode}
* @param {Object} element Document data element of this node
* @param {ve.dm.Node[]} contents List of child nodes to initially add
* @param {ve.dm.BranchNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.TableCellNode = function( element, contents ) {
ve.dm.TableCellNode = function( children, attributes ) {
// Inheritance
ve.dm.BranchNode.call( this, 'tableCell', element, contents );
ve.dm.BranchNode.call( this, 'tableCell', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a table cell view for this model.
*
* @method
* @returns {ve.ce.TableCellNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.TableCellNode.prototype.createView = function() {
return new ve.ce.TableCellNode( this );
ve.dm.TableCellNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': false,
'childNodeTypes': null,
'parentNodeTypes': ['tableRow']
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.TableCellNode.converters = {
'domElementTypes': ['td', 'th'],
'toDomElement': function( type, element ) {
return element.attributes && ( {
'data': document.createElement( 'td' ),
'header': document.createElement( 'th' )
} )[element.attributes['style']];
},
'toDataElement': function( tag, element ) {
return ( {
'td': { 'type': 'tableCell', 'attributes': { 'style': 'data' } },
'th': { 'type': 'tableCell', 'attributes': { 'style': 'header' } }
} )[tag];
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.tableCell = ve.dm.TableCellNode;
ve.dm.DocumentNode.nodeRules.tableCell = {
'parents': ['tableRow'],
'children': null
};
ve.dm.nodeFactory.register( 'tableCell', ve.dm.TableCellNode );
/* Inheritance */

View file

@ -1,37 +1,54 @@
/**
* Creates an ve.dm.TableNode object.
*
* DataModel node for a table.
*
* @class
* @constructor
* @extends {ve.dm.BranchNode}
* @param {Object} element Document data element of this node
* @param {ve.dm.TableCellNode[]} contents List of child nodes to initially add
* @param {ve.dm.BranchNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.TableNode = function( element, contents ) {
ve.dm.TableNode = function( children, attributes ) {
// Inheritance
ve.dm.BranchNode.call( this, 'table', element, contents );
ve.dm.BranchNode.call( this, 'table', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a table view for this model.
*
* @method
* @returns {ve.ce.TableNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.TableNode.prototype.createView = function() {
return new ve.ce.TableNode( this );
ve.dm.TableNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': false,
'childNodeTypes': ['tableRow'],
'parentNodeTypes': null
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.TableNode.converters = {
'domElementTypes': ['table'],
'toDomElement': function( type, element ) {
return document.createElement( 'table' );
},
'toDataElement': function( tag, element ) {
return { 'type': 'table' };
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.table = ve.dm.TableNode;
ve.dm.DocumentNode.nodeRules.table = {
'parents': null,
'children': ['tableRow']
};
ve.dm.nodeFactory.register( 'table', ve.dm.TableNode );
/* Inheritance */

View file

@ -1,37 +1,54 @@
/**
* Creates an ve.dm.TableRowNode object.
*
* DataModel node for a table row.
*
* @class
* @constructor
* @extends {ve.dm.BranchNode}
* @param {Object} element Document data element of this node
* @param {ve.dm.Node[]} contents List of child nodes to initially add
* @param {ve.dm.BranchNode[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.TableRowNode = function( element, contents ) {
ve.dm.TableRowNode = function( children, attributes ) {
// Inheritance
ve.dm.BranchNode.call( this, 'tableRow', element, contents );
ve.dm.BranchNode.call( this, 'tableRow', children, attributes );
};
/* Methods */
/* Static Members */
/**
* Creates a table row view for this model.
*
* @method
* @returns {ve.ce.TableRowNode}
* Node rules.
*
* @see ve.dm.NodeFactory
* @static
* @member
*/
ve.dm.TableRowNode.prototype.createView = function() {
return new ve.ce.TableRowNode( this );
ve.dm.TableRowNode.rules = {
'isWrapped': true,
'isContent': false,
'canContainContent': false,
'childNodeTypes': ['tableCell'],
'parentNodeTypes': ['tableSection']
};
/**
* Node converters.
*
* @see {ve.dm.Converter}
* @static
* @member
*/
ve.dm.TableRowNode.converters = {
'domElementTypes': ['tr'],
'toDomElement': function( type, element ) {
return document.createElement( 'tr' );
},
'toDataElement': function( tag, element ) {
return { 'type': 'tableRow' };
}
};
/* Registration */
ve.dm.DocumentNode.nodeModels.tableRow = ve.dm.TableRowNode;
ve.dm.DocumentNode.nodeRules.tableRow = {
'parents': ['table'],
'children': ['tableCell']
};
ve.dm.nodeFactory.register( 'tableRow', ve.dm.TableRowNode );
/* Inheritance */

View file

@ -1,77 +0,0 @@
/**
* Creates an annotation renderer object.
*
* @class
* @constructor
* @property annotations {Object} List of annotations to be applied
*/
ve.dm.AnnotationSerializer = function() {
this.annotations = {};
};
/* Static Methods */
/**
* Adds a set of annotations to be inserted around a range of text.
*
* Insertions for the same range will be nested in order of declaration.
* @example
* stack = new ve.dm.AnnotationSerializer();
* stack.add( new ve.Range( 1, 2 ), '[', ']' );
* stack.add( new ve.Range( 1, 2 ), '{', '}' );
* // Outputs: "a[{b}]c"
* console.log( stack.render( 'abc' ) );
*
* @method
* @param {ve.Range} range Range to insert text around
* @param {String} pre Text to insert before range
* @param {String} post Text to insert after range
*/
ve.dm.AnnotationSerializer.prototype.add = function( range, pre, post ) {
// Normalize the range if it can be normalized
if ( typeof range.normalize === 'function' ) {
range.normalize();
}
if ( !( range.start in this.annotations ) ) {
this.annotations[range.start] = [pre];
} else {
this.annotations[range.start].push( pre );
}
if ( !( range.end in this.annotations ) ) {
this.annotations[range.end] = [post];
} else {
this.annotations[range.end].unshift( post );
}
};
/**
* Adds a set of HTML tags to be inserted around a range of text.
*
* @method
* @param {ve.Range} range Range to insert text around
* @param {String} type Tag name
* @param {Object} [attributes] List of HTML attributes
*/
ve.dm.AnnotationSerializer.prototype.addTags = function( range, type, attributes ) {
this.add( range, ve.Html.makeOpeningTag( type, attributes ), ve.Html.makeClosingTag( type ) );
};
/**
* Renders annotations into text.
*
* @method
* @param {String} text Text to apply annotations to
* @returns {String} Wrapped text
*/
ve.dm.AnnotationSerializer.prototype.render = function( text ) {
var out = '';
for ( var i = 0, length = text.length; i <= length; i++ ) {
if ( i in this.annotations ) {
out += this.annotations[i].join( '' );
}
if ( i < length ) {
out += text[i];
}
}
return out;
};

View file

@ -1,210 +0,0 @@
/**
* Serializes a WikiDom plain object into an HTML string.
*
* @class
* @constructor
* @param {Object} options List of options for serialization
*/
ve.dm.HtmlSerializer = function( options ) {
this.options = $.extend( {
// defaults
}, options || {} );
};
/* Static Members */
ve.dm.HtmlSerializer.headingTags = {
'1': 'h1',
'2': 'h2',
'3': 'h3',
'4': 'h4',
'5': 'h5',
'6': 'h6'
};
ve.dm.HtmlSerializer.listTags = {
'bullet': 'ul',
'number': 'ol',
'definition': 'dl'
};
ve.dm.HtmlSerializer.listItemTags = {
'item': 'li',
'term': 'dt',
'definition': 'dd'
};
ve.dm.HtmlSerializer.tableCellTags = {
'tableHeading': 'th',
'tableCell': 'td'
};
/* Static Methods */
/**
* Get a serialized version of data.
*
* @static
* @method
* @param {Object} data Data to serialize
* @param {Object} options Options to use, @see {ve.dm.WikitextSerializer} for details
* @returns {String} Serialized version of data
*/
ve.dm.HtmlSerializer.stringify = function( data, options ) {
return ( new ve.dm.HtmlSerializer( options ) ).document( data );
};
ve.dm.HtmlSerializer.getHtmlAttributes = function( attributes ) {
var htmlAttributes = {},
count = 0;
for ( var key in attributes ) {
if ( key.indexOf( 'html/' ) === 0 ) {
htmlAttributes[key.substr( 5 )] = attributes[key];
count++;
}
}
return count ? htmlAttributes : null;
};
/* Methods */
ve.dm.HtmlSerializer.prototype.document = function( node, wrapWith, rawFirstParagraph ) {
var lines = [];
if ( wrapWith ) {
var htmlAttributes = ve.dm.HtmlSerializer.getHtmlAttributes( node.attributes );
lines.push( ve.Html.makeOpeningTag( wrapWith, htmlAttributes ) );
}
for ( var i = 0, length = node.children.length; i < length; i++ ) {
var child = node.children[i];
if ( child.type in this ) {
// Special case for paragraphs which have particular wrapping needs
if ( child.type === 'paragraph' ) {
lines.push( this.paragraph( child, rawFirstParagraph && i === 0 ) );
} else {
lines.push( this[child.type].call( this, child ) );
}
}
}
if ( wrapWith ) {
lines.push( ve.Html.makeClosingTag( wrapWith ) );
}
return lines.join( '\n' );
};
ve.dm.HtmlSerializer.prototype.comment = function( node ) {
return '<!--(' + node.text + ')-->';
};
ve.dm.HtmlSerializer.prototype.pre = function( node ) {
return ve.Html.makeTag(
'pre', {}, this.content( node.content, true )
);
};
ve.dm.HtmlSerializer.prototype.horizontalRule = function( node ) {
return ve.Html.makeTag( 'hr', {}, false );
};
ve.dm.HtmlSerializer.prototype.heading = function( node ) {
return ve.Html.makeTag(
ve.dm.HtmlSerializer.headingTags[node.attributes.level], {}, this.content( node.content )
);
};
ve.dm.HtmlSerializer.prototype.paragraph = function( node, raw ) {
if ( raw ) {
return this.content( node.content );
} else {
return ve.Html.makeTag( 'p', {}, this.content( node.content ) );
}
};
ve.dm.HtmlSerializer.prototype.list = function( node ) {
return this.document(
node, ve.dm.HtmlSerializer.listTags[node.attributes.style]
);
};
ve.dm.HtmlSerializer.prototype.listItem = function( node ) {
return this.document(
node, ve.dm.HtmlSerializer.listItemTags[node.attributes.style]
);
};
ve.dm.HtmlSerializer.prototype.table = function( node ) {
return this.document( node, 'table' );
};
ve.dm.HtmlSerializer.prototype.tableRow = function( node ) {
return this.document( node, 'tr' );
};
ve.dm.HtmlSerializer.prototype.tableCell = function( node ) {
return this.document(
node, ve.dm.HtmlSerializer.tableCellTags[node.attributes.type], true
);
};
ve.dm.HtmlSerializer.prototype.tableCaption = function( node ) {
return ve.Html.makeTag(
'caption',
ve.dm.HtmlSerializer.getHtmlAttributes( node.attributes ),
this.content( node.content )
);
};
ve.dm.HtmlSerializer.prototype.transclusion = function( node ) {
var title = [];
if ( node.namespace !== 'Main' ) {
title.push( node.namespace );
}
title.push( node.title );
title = title.join( ':' );
return ve.Html.makeTag( 'a', { 'href': '/wiki/' + title }, title );
};
ve.dm.HtmlSerializer.prototype.parameter = function( node ) {
return '{{{' + node.name + '}}}';
};
ve.dm.HtmlSerializer.prototype.content = function( node ) {
if ( 'annotations' in node && node.annotations.length ) {
var annotationSerializer = new ve.dm.AnnotationSerializer(),
tagTable = {
'textStyle/bold': 'b',
'textStyle/italic': 'i',
'textStyle/strong': 'strong',
'textStyle/emphasize': 'em',
'textStyle/big': 'big',
'textStyle/small': 'small',
'textStyle/superScript': 'sup',
'textStyle/subScript': 'sub'
};
for ( var i = 0, length = node.annotations.length; i < length; i++ ) {
var annotation = node.annotations[i];
if ( annotation.type in tagTable ) {
annotationSerializer.addTags( annotation.range, tagTable[annotation.type] );
} else {
switch ( annotation.type ) {
case 'link/external':
annotationSerializer.addTags(
annotation.range, 'a', { 'href': annotation.data.url }
);
break;
case 'link/internal':
annotationSerializer.addTags(
annotation.range, 'a', { 'href': '/wiki/' + annotation.data.title }
);
break;
case 'object/template':
case 'object/hook':
annotationSerializer.add( annotation.range, annotation.data.html, '' );
break;
}
}
}
return annotationSerializer.render( node.text );
} else {
return node.text;
}
};

View file

@ -1,129 +0,0 @@
/**
* Serializes a WikiDom plain object into a JSON string.
*
* @class
* @constructor
* @param {Object} options List of options for serialization
* @param {String} options.indentWith Text to use as indentation, such as \t or 4 spaces
* @param {String} options.joinWith Text to use as line joiner, such as \n or '' (empty string)
*/
ve.dm.JsonSerializer = function( options ) {
this.options = $.extend( {
'indentWith': '\t',
'joinWith': '\n'
}, options || {} );
};
/* Static Methods */
/**
* Get a serialized version of data.
*
* @static
* @method
* @param {Object} data Data to serialize
* @param {Object} options Options to use, @see {ve.dm.JsonSerializer} for details
* @returns {String} Serialized version of data
*/
ve.dm.JsonSerializer.stringify = function( data, options ) {
return ( new ve.dm.JsonSerializer( options ) ).stringify( data );
};
/**
* Gets the type of a given value.
*
* @static
* @method
* @param {Mixed} value Value to get type of
* @returns {String} Symbolic name of type
*/
ve.dm.JsonSerializer.typeOf = function( value ) {
if ( typeof value === 'object' ) {
if ( value === null ) {
return 'null';
}
switch ( value.constructor ) {
case [].constructor:
return 'array';
case ( new Date() ).constructor:
return 'date';
case ( new RegExp() ).constructor:
return 'regex';
default:
return 'object';
}
}
return typeof value;
};
/* Methods */
/**
* Get a serialized version of data.
*
* @method
* @param {Object} data Data to serialize
* @param {String} indentation String to prepend each line with (used internally with recursion)
* @returns {String} Serialized version of data
*/
ve.dm.JsonSerializer.prototype.stringify = function( data, indention ) {
if ( indention === undefined ) {
indention = '';
}
var type = ve.dm.JsonSerializer.typeOf( data ),
key;
// Open object/array
var json = '';
if ( type === 'array' ) {
if (data.length === 0) {
// Empty array
return '[]';
}
json += '[';
} else {
var empty = true;
for ( key in data ) {
if ( data.hasOwnProperty( key ) ) {
empty = false;
break;
}
}
if ( empty ) {
return '{}';
}
json += '{';
}
// Iterate over items
var comma = false;
for ( key in data ) {
if ( data.hasOwnProperty( key ) ) {
json += ( comma ? ',' : '' ) + this.options.joinWith + indention +
this.options.indentWith + ( type === 'array' ? '' : '"' + key + '"' + ': ' );
switch ( ve.dm.JsonSerializer.typeOf( data[key] ) ) {
case 'array':
case 'object':
json += this.stringify( data[key], indention + this.options.indentWith );
break;
case 'boolean':
case 'number':
json += data[key].toString();
break;
case 'null':
json += 'null';
break;
case 'string':
json += '"' + data[key].replace(/[\n]/g, '\\n').replace(/[\t]/g, '\\t') + '"';
break;
// Skip other types
}
comma = true;
}
}
// Close object/array
json += this.options.joinWith + indention + ( type === 'array' ? ']' : '}' );
return json;
};

View file

@ -1,214 +0,0 @@
/**
* Serializes a WikiDom plain object into a Wikitext string.
*
* @class
* @constructor
* @param options {Object} List of options for serialization
*/
ve.dm.WikitextSerializer = function( options ) {
this.options = $.extend( {
// defaults
}, options || {} );
};
/* Static Members */
ve.dm.HtmlSerializer.headingSymbols = {
'1': '=',
'2': '==',
'3': '===',
'4': '====',
'5': '=====',
'6': '======'
};
ve.dm.HtmlSerializer.listSymbols = {
'bullet': '*',
'number': '#',
'definition': ''
};
ve.dm.HtmlSerializer.listItemSymbols = {
'item': '',
'term': ':',
'definition': ';'
};
ve.dm.HtmlSerializer.tableCellSymbols = {
'tableHeading': '!',
'tableCell': '|'
};
/* Static Methods */
/**
* Get a serialized version of data.
*
* @static
* @method
* @param {Object} data Data to serialize
* @param {Object} options Options to use, @see {ve.dm.WikitextSerializer} for details
* @returns {String} Serialized version of data
*/
ve.dm.WikitextSerializer.stringify = function( data, options ) {
return ( new ve.dm.WikitextSerializer( options ) ).document( data );
};
ve.dm.WikitextSerializer.getHtmlAttributes = function( attributes ) {
var htmlAttributes = {},
count = 0;
for ( var key in attributes ) {
if ( key.indexOf( 'html/' ) === 0 ) {
htmlAttributes[key.substr( 5 )] = attributes[key];
count++;
}
}
return count ? htmlAttributes : null;
};
/* Methods */
ve.dm.WikitextSerializer.prototype.document = function( node, rawFirstParagraph ) {
var lines = [];
for ( var i = 0, length = node.children.length; i < length; i++ ) {
var childNode = node.children[i];
if ( childNode.type in this ) {
// Special case for paragraphs which have particular spacing needs
if ( childNode.type === 'paragraph' ) {
lines.push( this.paragraph( childNode, rawFirstParagraph && i === 0 ) );
if ( i + 1 < length /* && node.children[i + 1].type === 'paragraph' */ ) {
lines.push( '' );
}
} else {
lines.push( this[childNode.type].call( this, childNode ) );
}
}
}
return lines.join( '\n' );
};
ve.dm.WikitextSerializer.prototype.comment = function( node ) {
return '<!--' + node.text + '-->';
};
ve.dm.WikitextSerializer.prototype.horizontalRule = function( node ) {
return '----';
};
ve.dm.WikitextSerializer.prototype.heading = function( node ) {
var symbols = ve.repeatString( '=', node.attributes.level );
return symbols + this.content( node.content ) + symbols;
};
ve.dm.WikitextSerializer.prototype.paragraph = function( node ) {
return this.content( node.content );
};
ve.dm.WikitextSerializer.prototype.pre = function( node ) {
return ' ' + this.content( node.content ).replace( '\n', '\n ' );
};
ve.dm.WikitextSerializer.prototype.list = function( node, lead ) {
return '<!-- list item -->';
};
ve.dm.WikitextSerializer.prototype.listItem = function( node, lead ) {
return '<!-- list item -->';
};
ve.dm.WikitextSerializer.prototype.table = function( node ) {
var lines = [],
attributes = ve.dm.WikitextSerializer.getHtmlAttributes( node.attributes );
if ( attributes ) {
attributes = ve.Html.makeAttributeList( attributes );
}
lines.push( '{|' + attributes );
for ( var i = 0, length = node.children.length; i < length; i++ ) {
lines.push( this.tableRow( node.children[i], i === 0 ) );
}
lines.push( '|}' );
return lines.join( '\n' );
};
ve.dm.WikitextSerializer.prototype.tableRow = function( node, first ) {
var lines = [],
attributes = ve.dm.WikitextSerializer.getHtmlAttributes( node.attributes );
if ( attributes ) {
attributes = ve.Html.makeAttributeList( attributes );
}
if ( !first || attributes ) {
lines.push( '|-' + attributes );
}
for ( var i = 0, length = node.children.length; i < length; i++ ) {
lines.push( this.tableCell( node.children[i] ) );
}
return lines.join( '\n' );
};
ve.dm.WikitextSerializer.prototype.tableCell = function( node ) {
var attributes = ve.dm.WikitextSerializer.getHtmlAttributes( node.attributes );
if ( attributes ) {
attributes = ve.Html.makeAttributeList( attributes ) + '|';
}
return ve.dm.HtmlSerializer.tableCellSymbols[node.type] + attributes +
this.document( node, true );
};
ve.dm.WikitextSerializer.prototype.transclusion = function( node ) {
var title = [];
if ( node.namespace === 'Main' ) {
title.push( '' );
} else if ( node.namespace !== 'Template' ) {
title.push( node.namespace );
}
title.push( node.title );
return '{{' + title.join( ':' ) + '}}';
};
ve.dm.WikitextSerializer.prototype.parameter = function( node ) {
return '{{{' + node.name + '}}}';
};
ve.dm.WikitextSerializer.prototype.content = function( node ) {
if ( 'annotations' in node && node.annotations.length ) {
var annotationSerializer = new ve.dm.AnnotationSerializer(),
tagTable = {
'textStyle/strong': 'strong',
'textStyle/emphasize': 'em',
'textStyle/big': 'big',
'textStyle/small': 'small',
'textStyle/superScript': 'sup',
'textStyle/subScript': 'sub'
},
markupTable = {
'textStyle/bold': "'''",
'textStyle/italic': "''"
};
for ( var i = 0, length = node.annotations.length; i < length; i++ ) {
var annotation = node.annotations[i];
if ( annotation.type in tagTable ) {
annotationSerializer.addTags( annotation.range, tagTable[annotation.type] );
} else if ( annotation.type in markupTable ) {
annotationSerializer.add(
annotation.range, markupTable[annotation.type], markupTable[annotation.type]
);
} else {
switch ( annotation.type ) {
case 'link/external':
annotationSerializer.add(
annotation.range, '[' + annotation.data.href + ' ', ']'
);
break;
case 'link/internal':
annotationSerializer.add(
annotation.range, '[[' + annotation.data.title + '|', ']]'
);
break;
}
}
}
return annotationSerializer.render( node.text );
} else {
return node.text;
}
};

View file

@ -1,150 +1,119 @@
/**
* Creates an ve.dm.BranchNode object.
*
* DataModel node that can have branch or leaf children.
*
* @class
* @abstract
* @constructor
* @extends {ve.BranchNode}
* @extends {ve.dm.Node}
* @param {String} type Symbolic name of node type
* @param {Object} element Element object in document data
* @param {ve.dm.BranchNode[]} [contents] List of child nodes to append
* @param {ve.dm.Node[]} [children] Child nodes to attach
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.BranchNode = function( type, element, contents ) {
ve.dm.BranchNode = function( type, children, attributes ) {
// Inheritance
ve.dm.Node.call( this, type, 0, attributes );
ve.BranchNode.call( this );
ve.dm.Node.call( this, type, element, 0 );
// Child nodes
if ( ve.isArray( contents ) ) {
for ( var i = 0; i < contents.length; i++ ) {
this.push( contents[i] );
}
if ( ve.isArray( children ) && children.length ) {
this.splice.apply( this, [0, 0].concat( children ) );
}
};
/* Methods */
/**
* Gets a plain object representation of the document's data.
*
* Adds a node to the end of this node's children.
*
* @method
* @see {ve.dm.Node.getPlainObject}
* @see {ve.dm.DocumentNode.newFromPlainObject}
* @returns {Object} Plain object representation
* @param {ve.dm.BranchNode} childModel Item to add
* @returns {Integer} New number of children
* @emits splice (index, 0, [childModel])
* @emits update
*/
ve.dm.BranchNode.prototype.getPlainObject = function() {
var obj = { 'type': this.type };
if ( this.element && this.element.attributes ) {
obj.attributes = ve.copyObject( this.element.attributes );
ve.dm.BranchNode.prototype.push = function( childModel ) {
this.splice( this.children.length, 0, childModel );
return this.children.length;
};
/**
* Removes a node from the end of this node's children
*
* @method
* @returns {ve.dm.BranchNode} Removed childModel
* @emits splice (index, 1, [])
* @emits update
*/
ve.dm.BranchNode.prototype.pop = function() {
if ( this.children.length ) {
var childModel = this.children[this.children.length - 1];
this.splice( this.children.length - 1, 1 );
return childModel;
}
obj.children = [];
for ( var i = 0; i < this.children.length; i++ ) {
obj.children.push( this.children[i].getPlainObject() );
};
/**
* Adds a node to the beginning of this node's children.
*
* @method
* @param {ve.dm.BranchNode} childModel Item to add
* @returns {Integer} New number of children
* @emits splice (0, 0, [childModel])
* @emits update
*/
ve.dm.BranchNode.prototype.unshift = function( childModel ) {
this.splice( 0, 0, childModel );
return this.children.length;
};
/**
* Removes a node from the beginning of this node's children
*
* @method
* @returns {ve.dm.BranchNode} Removed childModel
* @emits splice (0, 1, [])
* @emits update
*/
ve.dm.BranchNode.prototype.shift = function() {
if ( this.children.length ) {
var childModel = this.children[0];
this.splice( 0, 1 );
return childModel;
}
return obj;
};
/**
* Adds and removes nodes from this node's children.
*
*
* @method
* @param {Integer} index Index to remove and or insert nodes at
* @param {Integer} howmany Number of nodes to remove
* @param {ve.dm.BranchNode} [...] Variadic list of nodes to insert
* @returns {ve.dm.BranchNode[]} Removed nodes
* @emits beforeSplice (index, howmany, [...])
* @emits afterSplice (index, howmany, [...])
* @emits update
* @emits splice (index, howmany, [...])
*/
ve.dm.BranchNode.prototype.splice = function( index, howmany ) {
var i,
length,
args = Array.prototype.slice.call( arguments, 0 ),
diff = 0;
this.emit.apply( this, ['beforeSplice'].concat( args ) );
if ( args.length >= 3 ) {
for ( i = 2, length = args.length; i < length; i++ ) {
length = args.length;
for ( i = 2; i < length; i++ ) {
args[i].attach( this );
args[i].on( 'update', this.emitUpdate );
diff += args[i].getElementLength();
diff += args[i].getOuterLength();
}
}
var removals = this.children.splice.apply( this.children, args );
for ( i = 0, length = removals.length; i < length; i++ ) {
removals[i].detach();
removals[i].removeListener( 'update', this.emitUpdate );
diff -= removals[i].getElementLength();
diff -= removals[i].getOuterLength();
}
this.adjustContentLength( diff, true );
this.emit.apply( this, ['afterSplice'].concat( args ) );
this.emit( 'update' );
this.adjustLength( diff, true );
this.emit.apply( this, ['splice'].concat( args ) );
return removals;
};
/**
* Sorts this node's children.
*
* @method
* @param {Function} sortfunc Function to use when sorting
* @emits beforeSort (sortfunc)
* @emits afterSort (sortfunc)
* @emits update
*/
ve.dm.BranchNode.prototype.sort = function( sortfunc ) {
this.emit( 'beforeSort', sortfunc );
this.children.sort( sortfunc );
this.emit( 'afterSort', sortfunc );
this.emit( 'update' );
};
/**
* Reverses the order of this node's children.
*
* @method
* @emits beforeReverse
* @emits afterReverse
* @emits update
*/
ve.dm.BranchNode.prototype.reverse = function() {
this.emit( 'beforeReverse' );
this.children.reverse();
this.emit( 'afterReverse' );
this.emit( 'update' );
};
/**
* Sets the root node to this and all of its descendants.
*
* @method
* @see {ve.dm.Node.prototype.setRoot}
* @param {ve.dm.Node} root Node to use as root
*/
ve.dm.BranchNode.prototype.setRoot = function( root ) {
if ( root == this.root ) {
// Nothing to do, don't recurse into all descendants
return;
}
this.root = root;
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].setRoot( root );
}
};
/**
* Clears the root node from this and all of it's children.
*
* @method
* @see {ve.dm.Node.prototype.clearRoot}
*/
ve.dm.BranchNode.prototype.clearRoot = function() {
this.root = null;
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].clearRoot();
}
};
/* Inheritance */
ve.extendClass( ve.dm.BranchNode, ve.BranchNode );

View file

@ -1,73 +1,183 @@
/**
* Creates an ve.dm.DocumentSynchronizer object.
*
* This object is a utility for collecting actions to be performed on the model tree
* in multiple steps and then processing those actions in a single step.
*
*
* This object is a utility for collecting actions to be performed on the model tree in multiple
* steps as the linear model is modified my a transaction processor and then processing those queued
* actions when the transaction is done being processed.
*
* IMPORTANT NOTE: It is assumed that:
* - The linear model has already been updated for the pushed actions
* - Actions are pushed in increasing offset order
* - Actions are non-overlapping
*
* @class
* @constructor
* @param {ve.dm.Document} doc Document to synchronize
*/
ve.dm.DocumentSynchronizer = function( model ) {
ve.dm.DocumentSynchronizer = function( doc ) {
// Properties
this.model = model;
this.actions = [];
this.document = doc;
this.actionQueue = [];
this.eventQueue = [];
};
/* Static Members */
/**
* Synchronization methods.
*
* Each method is specific to a type of action. Methods are called in the context of a document
* synchronizer, so they work similar to normal methods on the object.
*
* @static
* @member
*/
ve.dm.DocumentSynchronizer.synchronizers = {};
/* Static Methods */
/**
* Synchronizes an annotation action.
*
* This method is called within the context of a document synchronizer instance.
*
* @static
* @method
* @param {Object} action
*/
ve.dm.DocumentSynchronizer.synchronizers.annotation = function( action ) {
// Queue events for all leaf nodes covered by the range
// TODO test me
var i, selection = this.document.selectNodes( action.range, 'leaves' );
for ( i = 0; i < selection.length; i++ ) {
this.queueEvent( selection[i].node, 'annotation' );
this.queueEvent( selection[i].node, 'update' );
}
};
/**
* Synchronizes an attribute change action.
*
* This method is called within the context of a document synchronizer instance.
*
* @static
* @method
* @param {Object} action
*/
ve.dm.DocumentSynchronizer.synchronizers.attributeChange = function( action ) {
this.queueEvent( action.node, 'attributeChange', action.key, action.from, action.to );
this.queueEvent( action.node, 'update' );
};
/**
* Synchronizes a resize action.
*
* This method is called within the context of a document synchronizer instance.
*
* @static
* @method
* @param {Object} action
*/
ve.dm.DocumentSynchronizer.synchronizers.resize = function( action ) {
action.node.adjustLength( action.adjustment );
// no update needed, adjustLength causes an update event on it's own
};
/**
* Synchronizes a rebuild action.
*
* This method is called within the context of a document synchronizer instance.
*
* @static
* @method
* @param {Object} action
*/
ve.dm.DocumentSynchronizer.synchronizers.rebuild = function( action ) {
// Find the nodes contained by oldRange
var selection = this.document.selectNodes( action.oldRange, 'siblings' );
if ( selection.length === 0 ) {
// WTF? Nothing to rebuild, I guess. Whatever.
return;
}
var firstNode, parent, index, numNodes;
if ( 'indexInNode' in selection[0] ) {
// Insertion
parent = selection[0].node;
index = selection[0].indexInNode;
numNodes = 0;
} else {
// Rebuild
firstNode = selection[0].node,
parent = firstNode.getParent(),
index = selection[0].index;
numNodes = selection.length;
}
this.document.rebuildNodes( parent, index, numNodes, action.oldRange.from,
action.newRange.getLength()
);
};
/* Methods */
ve.dm.DocumentSynchronizer.prototype.getModel = function() {
return this.model;
/**
* Gets the document being synchronized.
*
* @method
* @returns {ve.dm.Document} Document being synchronized
*/
ve.dm.DocumentSynchronizer.prototype.getDocument = function() {
return this.document;
};
/**
* Add an insert action to the queue
* @param {ve.dm.BranchNode} node Node to insert
* @param {Integer} [offset] Offset of the inserted node, if known
* Add an annotation action to the queue.
*
* This finds all leaf nodes covered wholly or partially by the given range, and emits annotation
* events for all of them.
*
* @method
* @param {ve.Range} range Range that was annotated
*/
ve.dm.DocumentSynchronizer.prototype.pushInsert = function( node, offset ) {
this.actions.push( {
'type': 'insert',
ve.dm.DocumentSynchronizer.prototype.pushAnnotation = function( range ) {
this.actionQueue.push( {
'type': 'annotation',
'range': range
} );
};
/**
* Add an attribute change to the queue.
*
* This emits an attributeChange event for the given node with the provided metadata.
*
* @method
* @param {ve.dm.Node} node Node whose attribute changed
* @param {String} key Key of the attribute that changed
* @param {Mixed} from Old value of the attribute
* @param {Mixed} to New value of the attribute
*/
ve.dm.DocumentSynchronizer.prototype.pushAttributeChange = function( node, key, from, to ) {
this.actionQueue.push( {
'type': 'attributeChange',
'node': node,
'offset': offset || null
'key': key,
'from': from,
'to': to
} );
};
/**
* Add a delete action to the queue
* @param {ve.dm.BranchNode} node Node to delete
*/
ve.dm.DocumentSynchronizer.prototype.pushDelete = function( node ) {
this.actions.push( {
'type': 'delete',
'node': node
} );
};
/**
* Add a rebuild action to the queue. This rebuilds one or more nodes from data
* found in the linear model.
* @param {ve.Range} oldRange Range that the old nodes used to span. This is
* used to find the old nodes in the model tree.
* @param {ve.Range} newRange Range that contains the new nodes. This is used
* to get the new node data from the linear model.
*/
ve.dm.DocumentSynchronizer.prototype.pushRebuild = function( oldRange, newRange ) {
oldRange.normalize();
newRange.normalize();
this.actions.push( {
'type': 'rebuild',
'oldRange': oldRange,
'newRange': newRange
} );
};
/**
* Add a resize action to the queue. This changes the content length of a leaf node.
* @param {ve.dm.BranchNode} node Node to resize
* Add a resize action to the queue.
*
* This changes the length of a text node.
*
* @method
* @param {ve.dm.TextNode} node Node to resize
* @param {Integer} adjustment Length adjustment to apply to the node
*/
ve.dm.DocumentSynchronizer.prototype.pushResize = function( node, adjustment ) {
this.actions.push( {
this.actionQueue.push( {
'type': 'resize',
'node': node,
'adjustment': adjustment
@ -75,107 +185,82 @@ ve.dm.DocumentSynchronizer.prototype.pushResize = function( node, adjustment ) {
};
/**
* Add an update action to the queue
* @param {ve.dm.BranchNode} node Node to update
* Add a rebuild action to the queue.
*
* When a range of data has been changed arbitrarily this can be used to drop the nodes that
* represented the original range and replace them with new nodes that represent the new range.
*
* @method
* @param {ve.Range} oldRange Range of old nodes to be dropped
* @param {ve.Range} newRange Range for new nodes to be built from
*/
ve.dm.DocumentSynchronizer.prototype.pushUpdate = function( node ) {
this.actions.push( {
'type': 'update',
'node': node
ve.dm.DocumentSynchronizer.prototype.pushRebuild = function( oldRange, newRange ) {
this.actionQueue.push( {
'type': 'rebuild',
'oldRange': oldRange,
'newRange': newRange
} );
};
/**
* Apply queued actions to the model tree. This assumes that the linear model
* has already been updated, but the model tree has not yet been.
*
* Queue an event to be emitted on a node.
*
* This method is called by methods defined in {ve.dm.DocumentSynchronizer.synchronizers}.
*
* Duplicate events will be ignored only if all arguments match exactly. Hashes of each event that
* has been queued are stored in the nodes they will eventually be fired on.
*
* @method
* @param {ve.dm.Node} node
* @param {String} event Event name
* @param {Mixed} [...] Additional arguments to be passed to the event when fired
*/
ve.dm.DocumentSynchronizer.prototype.queueEvent = function( node, event ) {
// Check if this is already queued
var args = Array.prototype.slice.call( arguments, 1 );
var hash = $.toJSON( args );
if ( !node.queuedEventHashes ) {
node.queuedEventHashes = {};
}
if ( !node.queuedEventHashes[hash] ) {
node.queuedEventHashes[hash] = true;
this.eventQueue.push( { 'node': node, 'args': args } );
}
};
/**
* Synchronizes node tree using queued actions.
*
* This method uses the static methods defined in {ve.dm.DocumentSynchronizer.synchronizers} and
* calls them in the context of {this}.
*
* After synchronization is complete all queued events will be emitted. Hashes of queued events that
* have been stored on nodes are removed from the nodes after the events have all been emitted.
*
* This method also clears both action and event queues.
*
* @method
*/
ve.dm.DocumentSynchronizer.prototype.synchronize = function() {
// TODO: Normalize the actions list to clean up nested actions
// Perform all actions
var action,
offset,
parent;
for ( var i = 0, len = this.actions.length; i < len; i++ ) {
action = this.actions[i];
offset = action.offset || null;
switch ( action.type ) {
case 'insert':
// Compute the offset if it wasn't provided
if ( offset === null ) {
offset = this.model.getOffsetFromNode( action.node );
}
// Insert the new node at the given offset
var target = this.model.getNodeFromOffset( offset + 1 );
if ( target === this.model ) {
// Insert at the beginning of the document
this.model.splice( 0, 0, action.node );
} else if ( target === null ) {
// Insert at the end of the document
this.model.splice( this.model.getElementLength(), 0, action.node );
} else {
// Insert before the element currently at the offset
parent = target.getParent();
parent.splice( parent.indexOf( target ), 0, action.node );
}
break;
case 'delete':
// Replace original node with new node
parent = action.node.getParent();
parent.splice( parent.indexOf( action.node ), 1 );
break;
case 'rebuild':
// Find the node(s) contained by oldRange. This is done by repeatedly
// invoking selectNodes() in shallow mode until we find the right node(s).
// TODO this traversal could be made more efficient once we have an offset map
// TODO I need to add this recursive shallow stuff to selectNodes() as a 'siblings' mode
var selection, node = this.model, range = action.oldRange;
while ( true ) {
selection = node.selectNodes( range, true );
// We stop descending if:
// * we got more than one node, OR
// * we got a leaf node, OR
// * we got no range, which means the entire node is covered, OR
// * we got the same node back, which means we'd get in an infinite loop
if ( selection.length != 1 ||
!selection[0].node.hasChildren() ||
!selection[0].range ||
selection[0].node == node
) {
break;
}
// Descend into this node
node = selection[0].node;
range = selection[0].range;
}
if ( selection[0].node == this.model ) {
// We got some sort of weird input, ignore it
break;
}
// The first node we're rebuilding is selection[0].node , and we're rebuilding
// selection.length adjacent nodes.
// TODO selectNodes() discovers the index of selection[0].node in its parent,
// but discards it, and now we recompute it
this.model.rebuildNodes( selection[0].node.getParent(),
selection[0].node.getParent().indexOf( selection[0].node ),
selection.length, action.oldRange.from,
this.model.getData( action.newRange )
);
break;
case 'resize':
// Adjust node length - causes update events to be emitted
action.node.adjustContentLength( action.adjustment );
break;
case 'update':
// Emit update events
action.node.emit( 'update' );
break;
var action,
event,
i;
// Execute the actions in the queue
for ( i = 0; i < this.actionQueue.length; i++ ) {
action = this.actionQueue[i];
if ( action.type in ve.dm.DocumentSynchronizer.synchronizers ) {
ve.dm.DocumentSynchronizer.synchronizers[action.type].call( this, action );
} else {
throw 'Invalid action type ' + action.type;
}
}
// We've processed the queue, clear it
this.actions = [];
// Emit events in the event queue
for ( i = 0; i < this.eventQueue.length; i++ ) {
event = this.eventQueue[i];
event.node.emit.apply( event.node, event.args );
delete event.node.queuedEventHashes;
}
// Clear queues
this.actionQueue = [];
this.eventQueue = [];
};

View file

@ -1,43 +1,23 @@
/**
* Creates an ve.dm.LeafNode object.
*
* DataModel node that can not have children.
*
* @class
* @abstract
* @constructor
* @extends {ve.LeafNode}
* @extends {ve.dm.Node}
* @param {String} type Symbolic name of node type
* @param {Object} element Element object in document data
* @param {Integer} [length] Length of content data in document
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.LeafNode = function( type, element, length ) {
ve.dm.LeafNode = function( type, length, attributes ) {
// Inheritance
ve.dm.Node.call( this, type, length, attributes );
ve.LeafNode.call( this );
ve.dm.Node.call( this, type, element, length );
// Properties
this.contentLength = length || 0;
};
/* Methods */
/**
* Gets a plain object representation of the document's data.
*
* @method
* @see {ve.dm.Node.getPlainObject}
* @see {ve.dm.DocumentNode.newFromPlainObject}
* @returns {Object} Plain object representation,
*/
ve.dm.LeafNode.prototype.getPlainObject = function() {
var obj = { 'type': this.type };
if ( this.element && this.element.attributes ) {
obj.attributes = ve.copyObject( this.element.attributes );
}
obj.content = ve.dm.DocumentNode.getExpandedContentData( this.getContentData() );
return obj;
};
/* Inheritance */
ve.extendClass( ve.dm.LeafNode, ve.LeafNode );

View file

@ -1,286 +1,255 @@
/**
* Creates an ve.dm.Node object.
*
* Generic DataModel node.
*
* @class
* @abstract
* @constructor
* @extends {ve.Node}
* @param {String} type Symbolic name of node type
* @param {Object} element Element object in document data
* @param {Integer} [length] Length of content data in document
* @param {Object} [attributes] Reference to map of attribute key/value pairs
*/
ve.dm.Node = function( type, element, length ) {
ve.dm.Node = function( type, length, attributes ) {
// Inheritance
ve.Node.call( this );
ve.Node.call( this, type );
// Properties
this.type = type;
this.parent = null;
this.root = this;
this.element = element || null;
this.contentLength = length || 0;
};
/* Abstract Methods */
/**
* Creates a view for this node.
*
* @abstract
* @method
* @returns {ve.ce.Node} New item view associated with this model
*/
ve.dm.Node.prototype.createView = function() {
throw 'DocumentModelNode.createView not implemented in this subclass:' + this.constructor;
};
/**
* Gets a plain object representation of the document's data.
*
* @method
* @returns {Object} Plain object representation
*/
ve.dm.Node.prototype.getPlainObject = function() {
throw 'DocumentModelNode.getPlainObject not implemented in this subclass:' + this.constructor;
this.length = length || 0;
this.attributes = attributes || {};
this.doc = undefined;
};
/* Methods */
/**
* Gets the content length.
*
* Gets a list of allowed child node types.
*
* @method
* @see {ve.Node.prototype.getContentLength}
* @returns {Integer} Length of content
* @returns {String[]|null} List of node types allowed as children or null if any type is allowed
*/
ve.dm.Node.prototype.getContentLength = function() {
return this.contentLength;
ve.dm.Node.prototype.getChildNodeTypes = function() {
return ve.dm.nodeFactory.getChildNodeTypes( this.type );
};
/**
* Gets the element length.
*
* Gets a list of allowed parent node types.
*
* @method
* @see {ve.Node.prototype.getElementLength}
* @returns {Integer} Length of content
* @returns {String[]|null} List of node types allowed as parents or null if any type is allowed
*/
ve.dm.Node.prototype.getElementLength = function() {
return this.contentLength + 2;
ve.dm.Node.prototype.getParentNodeTypes = function() {
return ve.dm.nodeFactory.getParentNodeTypes( this.type );
};
/**
* Sets the content length.
*
* Checks if this node can have child nodes.
*
* @method
* @param {Integer} contentLength Length of content
* @throws Invalid content length error if contentLength is less than 0
* @returns {Boolean} Node can have children
*/
ve.dm.Node.prototype.setContentLength = function( contentLength ) {
if ( contentLength < 0 ) {
throw 'Invalid content length error. Content length can not be less than 0.';
ve.dm.Node.prototype.canHaveChildren = function() {
return ve.dm.nodeFactory.canNodeHaveChildren( this.type );
};
/**
* Checks if this node can have child nodes which can also have child nodes.
*
* @method
* @returns {Boolean} Node can have grandchildren
*/
ve.dm.Node.prototype.canHaveGrandchildren = function() {
return ve.dm.nodeFactory.canNodeHaveGrandchildren( this.type );
};
/**
* Checks if this node represents a wrapped element in the linear model.
*
* @method
* @returns {Boolean} Node represents a wrapped element
*/
ve.dm.Node.prototype.isWrapped = function() {
return ve.dm.nodeFactory.isNodeWrapped( this.type );
};
/**
* Checks if this node can contain content.
*
* @method
* @returns {Boolean} Node can contain content
*/
ve.dm.Node.prototype.canContainContent = function() {
return ve.dm.nodeFactory.canNodeContainContent( this.type );
};
/**
* Checks if this node is content.
*
* @method
* @returns {Boolean} Node is content
*/
ve.dm.Node.prototype.isContent = function() {
return ve.dm.nodeFactory.isNodeContent( this.type );
};
/**
* Gets the inner length.
*
* @method
* @returns {Integer} Length of the node's contents
*/
ve.dm.Node.prototype.getLength = function() {
return this.length;
};
/**
* Gets the outer length, including any opening/closing elements.
*
* @method
* @returns {Integer} Length of the entire node
*/
ve.dm.Node.prototype.getOuterLength = function() {
return this.length + ( this.isWrapped() ? 2 : 0 );
};
/**
* Gets the range inside the node.
*
* @method
* @returns {ve.Range} Inner node range
*/
ve.dm.Node.prototype.getRange = function() {
var offset = this.getOffset();
if ( this.isWrapped() ) {
offset++;
}
var diff = contentLength - this.contentLength;
this.contentLength = contentLength;
return new ve.Range( offset, offset + this.length );
};
/**
* Gets the range outside the node.
*
* @method
* @returns {ve.Range} Outer node range
*/
ve.dm.Node.prototype.getOuterRange = function() {
var offset = this.getOffset();
return new ve.Range( offset, offset + this.getOuterLength() );
};
/**
* Sets the inner length.
*
* @method
* @param {Integer} length Length of content
* @throws Invalid content length error if length is less than 0
* @emits lengthChange (diff)
* @emits update
*/
ve.dm.Node.prototype.setLength = function( length ) {
if ( length < 0 ) {
throw 'Length cannot be negative';
}
// Compute length adjustment from old length
var diff = length - this.length;
// Set new length
this.length = length;
// Adjust the parent's length
if ( this.parent ) {
this.parent.adjustContentLength( diff );
this.parent.adjustLength( diff );
}
// Emit events
this.emit( 'lengthChange', diff );
this.emit( 'update' );
};
/**
* Adjust the content length.
*
* Adjust the length.
*
* @method
* @param {Integer} adjustment Amount to adjust content length by
* @param {Integer} adjustment Amount to adjust length by
* @throws Invalid adjustment error if resulting length is less than 0
* @emits lengthChange (diff)
* @emits update
*/
ve.dm.Node.prototype.adjustContentLength = function( adjustment, quiet ) {
this.contentLength += adjustment;
// Make sure the adjustment was sane
if ( this.contentLength < 0 ) {
// Reverse the adjustment
this.contentLength -= adjustment;
// Complain about it
throw 'Invalid adjustment error. Content length can not be less than 0.';
}
if ( this.parent ) {
this.parent.adjustContentLength( adjustment, true );
}
if ( !quiet ) {
this.emit( 'update' );
}
ve.dm.Node.prototype.adjustLength = function( adjustment ) {
this.setLength( this.length + adjustment );
};
/**
* Attaches this node to another as a child.
*
* Gets the offset of this node within the document.
*
* If this node has no parent than the result will always be 0.
*
* @method
* @param {ve.dm.Node} parent Node to attach to
* @emits attach (parent)
* @returns {Integer} Offset of node
*/
ve.dm.Node.prototype.attach = function( parent ) {
this.emit( 'beforeAttach', parent );
this.parent = parent;
this.setRoot( parent.getRoot() );
this.emit( 'afterAttach', parent );
};
/**
* Detaches this node from it's parent.
*
* @method
* @emits detach
*/
ve.dm.Node.prototype.detach = function() {
this.emit( 'beforeDetach' );
this.parent = null;
this.clearRoot();
this.emit( 'afterDetach' );
};
/**
* Gets a reference to this node's parent.
*
* @method
* @returns {ve.dm.Node} Reference to this node's parent
*/
ve.dm.Node.prototype.getParent = function() {
return this.parent;
};
/**
* Gets the root node in the tree this node is currently attached to.
*
* @method
* @returns {ve.dm.Node} Root node
*/
ve.dm.Node.prototype.getRoot = function() {
return this.root;
};
/**
* Sets the root node to this and all of it's children.
*
* This method is overridden by nodes with children.
*
* @method
* @param {ve.dm.Node} root Node to use as root
*/
ve.dm.Node.prototype.setRoot = function( root ) {
this.root = root;
};
/**
* Clears the root node from this and all of it's children.
*
* This method is overridden by nodes with children.
*
* @method
*/
ve.dm.Node.prototype.clearRoot = function() {
this.root = null;
};
/**
* Gets the element object.
*
* @method
* @returns {Object} Element object in linear data model
*/
ve.dm.Node.prototype.getElement = function() {
return this.element;
};
/**
* Gets the symbolic element type name.
*
* @method
* @returns {String} Symbolic name of element type
*/
ve.dm.Node.prototype.getElementType = function() {
//return this.element.type;
// We can't use this.element.type because this.element may be null
// So this function now returns this.type and should really be called
// getType()
// TODO: Do we care?
return this.type;
ve.dm.Node.prototype.getOffset = function() {
return this.root === this ? 0 : this.root.getOffsetFromNode( this );
};
/**
* Gets an element attribute value.
*
*
* @method
* @returns {Mixed} Value of attribute, or null if no such attribute exists
* @returns {Mixed} Value of attribute, or undefined if no such attribute exists
*/
ve.dm.Node.prototype.getElementAttribute = function( key ) {
if ( this.element && this.element.attributes && key in this.element.attributes ) {
return this.element.attributes[key];
}
return null;
ve.dm.Node.prototype.getAttribute = function( key ) {
return this.attributes[key];
};
/**
* Gets all element data, including the element opening, closing and it's contents.
*
* Gets a reference to this node's attributes object
*
* @method
* @returns {Array} Element data
* @returns {Object} Attributes object (by reference)
*/
ve.dm.Node.prototype.getElementData = function() {
// Get reference to the document, which might be this node but otherwise should be this.root
var root = this.type === 'document' ?
this : ( this.root && this.root.type === 'document' ? this.root : null );
if ( root ) {
return root.getElementDataFromNode( this );
}
return [];
ve.dm.Node.prototype.getAttributes = function() {
return this.attributes;
};
/**
* Gets content data within a given range.
*
* @method
* @param {ve.Range} [range] Range of content to get
* @returns {Array} Content data
* Get a clone of the linear model element for this node. The attributes object is deep-copied.
*
* @returns {Object} Element object with 'type' and (optionally) 'attributes' fields
*/
ve.dm.Node.prototype.getContentData = function( range ) {
// Get reference to the document, which might be this node but otherwise should be this.root
var root = this.type === 'document' ?
this : ( this.root && this.root.type === 'document' ? this.root : null );
if ( root ) {
return root.getContentDataFromNode( this, range );
ve.dm.Node.prototype.getClonedElement = function() {
var retval = { 'type': this.type };
if ( !ve.isEmptyObject( this.attributes ) ) {
retval.attributes = ve.copyObject( this.attributes );
}
return [];
};
return retval;
}
/**
* Gets plain text version of the content within a specific range.
*
* Two newlines are inserted between leaf nodes.
*
* TODO: Maybe do something more adaptive with newlines
*
* Checks if this node can be merged with another.
*
* For two nodes to be mergeable, this node and the given node must either be the same node or:
* - Have the same type
* - Have the same depth
* - Have similar ancestory (each node upstream must have the same type)
*
* @method
* @param {ve.Range} [range] Range of text to get
* @returns {String} Text within given range
* @param {ve.dm.Node} node Node to consider merging with
* @returns {Boolean} Nodes can be merged
*/
ve.dm.Node.prototype.getContentText = function( range ) {
var content = this.getContentData( range );
// Copy characters
var text = '',
element = false;
for ( var i = 0, length = content.length; i < length; i++ ) {
if ( typeof content[i].type === 'string' ) {
if ( i ) {
element = true;
}
} else {
if ( element ) {
text += '\n\n';
element = false;
}
text += typeof content[i] === 'string' ? content[i] : content[i][0];
ve.dm.Node.prototype.canBeMergedWith = function( node ) {
var n1 = this,
n2 = node;
// Move up from n1 and n2 simultaneously until we find a common ancestor
while ( n1 !== n2 ) {
if (
// Check if we have reached a root (means there's no common ancestor or unequal depth)
( n1 === null || n2 === null ) ||
// Ensure that types match
n1.getType() !== n2.getType()
) {
return false;
}
// Move up
n1 = n1.getParent();
n2 = n2.getParent();
}
return text;
return true;
};
/* Inheritance */

View file

@ -1,31 +1,33 @@
/**
* Creates an ve.dm.Surface object.
* DataModel surface.
*
* @class
* @constructor
* @extends {ve.EventEmitter}
* @param {ve.dm.DocumentNode} doc Document model to create surface for
* @param {ve.dm.Document} doc Document model to create surface for
*/
ve.dm.Surface = function( doc ) {
// Inheritance
ve.EventEmitter.call( this );
// Properties
this.doc = doc;
this.documentModel = doc;
this.selection = null;
this.smallStack = [];
this.bigStack = [];
this.undoIndex = 0;
var _this = this;
setInterval( function () {
_this.breakpoint();
}, 750 );
this.historyTrackingInterval = null;
};
/* Methods */
ve.dm.Surface.prototype.startHistoryTracking = function() {
this.historyTrackingInterval = setInterval( ve.proxy( this.breakpoint, this ), 750 );
};
ve.dm.Surface.prototype.stopHistoryTracking = function() {
clearInterval( this.historyTrackingInterval );
};
ve.dm.Surface.prototype.purgeHistory = function() {
this.selection = null;
this.smallStack = [];
@ -48,7 +50,7 @@ ve.dm.Surface.prototype.getHistory = function() {
* @returns {ve.dm.DocumentNode} Document model of the surface
*/
ve.dm.Surface.prototype.getDocument = function() {
return this.doc;
return this.documentModel;
};
/**
@ -62,69 +64,56 @@ ve.dm.Surface.prototype.getSelection = function() {
};
/**
* Sets the selection
*
* @method
*/
ve.dm.Surface.prototype.setSelection = function( selection ) {
this.selection = selection;
};
/**
* Changes the selection.
*
* If changing the selection at a high frequency (such as while dragging) use the combine argument
* to avoid them being split up into multiple history items
* Applies a series of transactions to the content data and sets the selection.
*
* @method
* @param {ve.dm.Transaction} transaction Transaction to apply to the document
* @param {ve.Range} selection
* @param {Boolean} isManual Whether this selection was the result of a user action, and thus should
* be recorded in history...?
*/
ve.dm.Surface.prototype.select = function( selection, isManual ) {
selection.normalize();
/*if (
( ! this.selection ) || ( ! this.selection.equals( selection ) )
) {*/
if ( isManual ) {
this.breakpoint();
}
// check if the last thing is a selection, if so, swap it.
ve.dm.Surface.prototype.change = function( transaction, selection ) {
if ( transaction ) {
this.bigStack = this.bigStack.slice( 0, this.bigStack.length - this.undoIndex );
this.undoIndex = 0;
this.smallStack.push( transaction );
ve.dm.TransactionProcessor.commit( this.getDocument(), transaction );
}
if ( selection && ( !this.selection || !this.selection.equals ( selection ) ) ) {
selection.normalize();
this.selection = selection;
this.emit( 'select', this.selection.clone() );
//}
this.emit ('select', this.selection.clone() );
}
if ( transaction ) {
this.emit( 'transact', transaction );
}
this.emit( 'change', transaction, selection );
};
/**
* Applies a series of transactions to the content data.
*
* If committing multiple transactions which are the result of a single user action and need to be
* part of a single history item, use the isPartial argument for all but the last one to avoid them
* being split up into multple history items.
* Applies an annotation to the current selection
*
* @method
* @param {ve.dm.Transaction} transactions Tranasction to apply to the document
* @param {boolean} isPartial whether this transaction is part of a larger logical grouping of
* transactions (such as when replacing - delete, then insert)
* @param {String} annotation action: toggle, clear, set
* @param {Object} annotation object to apply.
*/
ve.dm.Surface.prototype.transact = function( transaction ) {
this.bigStack = this.bigStack.slice( 0, this.bigStack.length - this.undoIndex );
this.undoIndex = 0;
this.smallStack.push( transaction );
this.doc.commit( transaction );
this.emit( 'transact', transaction );
ve.dm.Surface.prototype.annotate = function( method, annotation ) {
var selection = this.getSelection();
if ( this.selection.getLength() ) {
var tx = ve.dm.Transaction.newFromAnnotation(
this.getDocument(), selection, method, annotation
);
this.change( tx );
}
};
ve.dm.Surface.prototype.breakpoint = function( selection ) {
/*
if( this.smallStack.length > 0 ) {
this.bigStack.push( {
stack: this.smallStack,
selection: selection || this.selection.clone()
} );
this.smallStack = [];
this.emit ( 'history' );
}
*/
};
ve.dm.Surface.prototype.undo = function() {
@ -134,14 +123,15 @@ ve.dm.Surface.prototype.undo = function() {
var diff = 0;
var item = this.bigStack[this.bigStack.length - this.undoIndex];
for( var i = item.stack.length - 1; i >= 0; i-- ) {
this.doc.rollback( item.stack[i] );
this.documentModel.rollback( item.stack[i] );
diff += item.stack[i].lengthDifference;
}
var selection = item.selection;
selection.from -= diff;
selection.to -= diff;
this.select( selection );
selection.end -= diff;
this.emit ( 'history' );
return selection;
}
return null;
};
ve.dm.Surface.prototype.redo = function() {
@ -151,19 +141,20 @@ ve.dm.Surface.prototype.redo = function() {
var diff = 0;
var item = this.bigStack[this.bigStack.length - this.undoIndex];
for( var i = 0; i < item.stack.length; i++ ) {
this.doc.commit( item.stack[i] );
this.documentModel.commit( item.stack[i] );
diff += item.stack[i].lengthDifference;
}
var selection = item.selection;
selection.from += diff;
selection.to += diff;
this.selection = null;
this.select( selection );
selection.end += diff;
}
this.undoIndex--;
this.emit ( 'history' );
return selection;
}
return null;
};
/* Inheritance */
ve.extendClass( ve.dm.Surface, ve.EventEmitter );

View file

@ -1,20 +1,444 @@
/**
* Creates an ve.dm.Transaction object.
*
* DataModel transaction.
*
* @class
* @constructor
* @param {Object[]} operations List of operations
*/
ve.dm.Transaction = function( operations ) {
this.operations = ve.isArray( operations ) ? operations : [];
ve.dm.Transaction = function() {
this.operations = [];
this.lengthDifference = 0;
};
/* Static Methods */
/**
* Generates a transaction that inserts data at a given offset.
*
* @static
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {Integer} offset Offset to insert at
* @param {Array} data Data to insert
* @returns {ve.dm.Transaction} Transcation that inserts data
*/
ve.dm.Transaction.newFromInsertion = function( doc, offset, insertion ) {
var tx = new ve.dm.Transaction(),
data = doc.getData();
// Fix up the insertion
insertion = doc.fixupInsertion( insertion, offset );
// Retain up to insertion point, if needed
tx.pushRetain( offset );
// Insert data
tx.pushReplace( [], insertion );
// Retain to end of document, if needed (for completeness)
tx.pushRetain( data.length - offset );
return tx;
};
/**
* Generates a transaction which removes data from a given range.
*
* There are three possible results from a removal:
* 1. Remove content only
* - Occurs when the range starts and ends on elements of different type, depth or ancestry
* 2. Remove entire elements and their content
* - Occurs when the range spans across an entire element
* 3. Merge two elements by removing the end of one and the beginning of another
* - Occurs when the range starts and ends on elements of similar type, depth and ancestry
*
* This function uses the following logic to decide what to actually remove:
* 1. Elements are only removed if range being removed covers the entire element
* 2. Elements can only be merged if ve.dm.Node.canBeMergedWith() returns true
* 3. Merges take place at the highest common ancestor
*
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {ve.Range} range Range of data to remove
* @returns {ve.dm.Transaction} Transcation that removes data
* @throws 'Invalid range, can not remove from {range.start} to {range.end}'
*/
ve.dm.Transaction.newFromRemoval = function( doc, range ) {
var tx = new ve.dm.Transaction(),
data = doc.getData();
// Normalize and validate range
range.normalize();
if ( range.start === range.end ) {
// Empty range, nothing to remove, retain up to the end of the document (for completeness)
tx.pushRetain( data.length );
return tx;
}
// Select nodes and validate selection
var selection = doc.selectNodes( range, 'covered' );
if ( selection.length === 0 ) {
// Empty selection? Something is wrong!
throw 'Invalid range, cannot remove from ' + range.start + ' to ' + range.end;
}
var first, last, offset = 0, removeStart = null, removeEnd = null, nodeStart, nodeEnd;
first = selection[0];
last = selection[selection.length - 1];
// If the first and last node are mergeable, merge them
if ( first.node.canBeMergedWith( last.node ) ) {
if ( !first.range && !last.range ) {
// First and last node are both completely covered, remove them
removeStart = first.nodeOuterRange.start;
removeEnd = last.nodeOuterRange.end;
} else {
// Either the first node or the last node is partially covered, so remove
// the selected content
removeStart = ( first.range || first.nodeRange ).start;
removeEnd = ( last.range || last.nodeRange ).end;
}
tx.pushRetain( removeStart );
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
tx.pushRetain( data.length - removeEnd );
// All done
return tx;
}
// The selection wasn't mergeable, so remove nodes that are completely covered, and strip
// nodes that aren't
for ( i = 0; i < selection.length; i++ ) {
if ( !selection[i].range ) {
// Entire node is covered, remove it
nodeStart = selection[i].nodeOuterRange.start;
nodeEnd = selection[i].nodeOuterRange.end;
} else {
// Part of the node is covered, remove that range
nodeStart = selection[i].range.start;
nodeEnd = selection[i].range.end;
}
// Merge contiguous removals. Only apply a removal when a gap appears, or at the
// end of the loop
if ( removeEnd === null ) {
// First removal
removeStart = nodeStart;
removeEnd = nodeEnd;
} else if ( removeEnd === nodeStart ) {
// Merge this removal into the previous one
removeEnd = nodeEnd;
} else {
// There is a gap between the previous removal and this one
// Push the previous removal first
tx.pushRetain( removeStart - offset );
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
offset = removeEnd;
// Now start this removal
removeStart = nodeStart;
removeEnd = nodeEnd;
}
}
// Apply the last removal, if any
if ( removeEnd !== null ) {
tx.pushRetain( removeStart - offset );
tx.pushReplace( data.slice( removeStart, removeEnd ), [] );
offset = removeEnd;
}
// Retain up to the end of the document
tx.pushRetain( data.length - offset );
return tx;
};
/**
* Generates a transaction that changes an attribute.
*
* @static
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {Integer} offset Offset of element
* @param {String} key Attribute name
* @param {Mixed} value New value
* @returns {ve.dm.Transaction} Transcation that changes an element
* @throws 'Can not set attributes to non-element data'
* @throws 'Can not set attributes on closing element'
*/
ve.dm.Transaction.newFromAttributeChange = function( doc, offset, key, value ) {
var tx = new ve.dm.Transaction(),
data = doc.getData();
// Verify element exists at offset
if ( data[offset].type === undefined ) {
throw 'Can not set attributes to non-element data';
}
// Verify element is not a closing
if ( data[offset].type.charAt( 0 ) === '/' ) {
throw 'Can not set attributes on closing element';
}
// Retain up to element
tx.pushRetain( offset );
// Change attribute
tx.pushReplaceElementAttribute(
key, 'attributes' in data[offset] ? data[offset].attributes[key] : undefined, value
);
// Retain to end of document
tx.pushRetain( data.length - offset );
return tx;
};
/**
* Generates a transaction that annotates content.
*
* @static
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {ve.Range} range Range to annotate
* @param {String} method Annotation mode
* 'set': Adds annotation to all content in range
* 'clear': Removes instances of annotation from content in range
* @param {Object} annotation Annotation to set or clear
* @returns {ve.dm.Transaction} Transcation that annotates content
*/
ve.dm.Transaction.newFromAnnotation = function( doc, range, method, annotation ) {
var tx = new ve.dm.Transaction(),
data = doc.getData(),
hash = ve.getHash( annotation );
// Iterate over all data in range, annotating where appropriate
range.normalize();
var i = range.start,
span = i,
on = false;
while ( i < range.end ) {
if ( data[i].type !== undefined ) {
// Element
if ( on ) {
tx.pushRetain( span );
tx.pushStopAnnotating( method, annotation );
span = 0;
on = false;
}
} else {
// Content
var covered = doc.offsetContainsAnnotation( i, annotation );
if ( ( covered && method === 'set' ) || ( !covered && method === 'clear' ) ) {
// Skip annotated content
if ( on ) {
tx.pushRetain( span );
tx.pushStopAnnotating( method, annotation );
span = 0;
on = false;
}
} else {
// Cover non-annotated content
if ( !on ) {
tx.pushRetain( span );
tx.pushStartAnnotating( method, annotation );
span = 0;
on = true;
}
}
}
span++;
i++;
}
tx.pushRetain( span );
if ( on ) {
tx.pushStopAnnotating( method, annotation );
}
tx.pushRetain( data.length - range.end );
return tx;
};
/**
* Generates a transaction that converts elements that can contain content.
*
* @static
* @method
* @param {ve.dm.Document} doc Document to create transaction for
* @param {ve.Range} range Range to convert
* @param {String} type Symbolic name of element type to convert to
* @param {Object} attr Attributes to initialize element with
* @returns {ve.dm.Transaction} Transaction that annotates content
*/
ve.dm.Transaction.newFromContentBranchConversion = function( doc, range, type, attr ) {
var tx = new ve.dm.Transaction(),
data = doc.getData(),
selection = doc.selectNodes( range, 'leaves' ),
opening = { 'type': type },
closing = { 'type': '/' + type },
previousBranch,
previousBranchOuterRange;
// Add attributes to opening if needed
if ( ve.isPlainObject( attr ) ) {
opening.attributes = attr;
}
// Replace the wrappings of each content branch in the range
for ( var i = 0; i < selection.length; i++ ) {
var selected = selection[i];
if ( selected.node.isContent() ) {
var branch = selected.node.getParent(),
branchOuterRange = branch.getOuterRange();
// Don't convert the same branch twice
if ( branch === previousBranch ) {
continue;
}
// Retain up to this branch, considering where the previous one left off
tx.pushRetain(
branchOuterRange.start - ( previousBranch ? previousBranchOuterRange.end : 0 )
);
// Replace the opening
tx.pushReplace( [data[branchOuterRange.start]], [ve.copyObject( opening )] );
// Retain the contents
tx.pushRetain( branch.getLength() );
// Replace the closing
tx.pushReplace( [data[branchOuterRange.end - 1]], [ve.copyObject( closing )] );
// Remember this branch and its range for next time
previousBranch = branch;
previousBranchOuterRange = branchOuterRange;
}
}
// Retain until the end
tx.pushRetain(
data.length - ( previousBranch ? previousBranchOuterRange.end : 0 )
);
return tx;
};
/**
* Generates a transaction which wraps, unwraps or replaces structure.
*
* The unwrap parameters are checked against the actual model data, and
* an exception is thrown if the type fields don't match. This means you
* can omit attributes from the unwrap parameters, those are automatically
* picked up from the model data instead.
*
* NOTE: This function currently does not fix invalid parent/child relationships, so it will
* happily convert paragraphs to listItems without wrapping them in a list if that's what you
* ask it to do. We'll probably fix this later but for now the caller is responsible for giving
* valid instructions.
*
* @param {ve.dm.Document} doc Document to generate a transaction for
* @param {ve.Range} range Range to wrap/unwrap/replace around
* @param {Array} unwrapOuter Array of opening elements to unwrap. These must be immediately *outside* the range.
* @param {Array} wrapOuter Array of opening elements to wrap around the range.
* @param {Array} unwrapEach Array of opening elements to unwrap from each top-level element in the range.
* @param {Array} wrapEach Array of opening elements to wrap around each top-level element in the range.
* @returns {ve.dm.Transaction}
*
* @example Changing a paragraph to a header:
* Before: [ {'type': 'paragraph'}, 'a', 'b', 'c', {'type': '/paragraph'} ]
* newFromWrap( new ve.Range( 1, 4 ), [ {'type': 'paragraph'} ], [ {'type': 'heading', 'level': 1 } ] );
* After: [ {'type': 'heading', 'level': 1 }, 'a', 'b', 'c', {'type': '/heading'} ]
*
* @example Changing a set of paragraphs to a list:
* Before: [ {'type': 'paragraph'}, 'a', {'type': '/paragraph'}, {'type':'paragraph'}, 'b', {'type':'/paragraph'} ]
* newFromWrap( new ve.Range( 0, 6 ), [], [ {'type': 'list' } ], [], [ {'type': 'listItem', 'attributes': {'styles': ['bullet']}} ] );
* After: [ {'type': 'list'}, {'type': 'listItem', 'attributes': {'styles': ['bullet']}}, {'type':'paragraph'} 'a',
* {'type': '/paragraph'}, {'type': '/listItem'}, {'type': 'listItem', 'attributes': {'styles': ['bullet']}},
* {'type': 'paragraph'}, 'b', {'type': '/paragraph'}, {'type': '/listItem'}, {'type': '/list'} ]
*/
ve.dm.Transaction.newFromWrap = function( doc, range, unwrapOuter, wrapOuter, unwrapEach, wrapEach ) {
// Function to generate arrays of closing elements in reverse order
function closingArray( openings ) {
var closings = [], i, len = openings.length;
for ( i = 0; i < len; i++ ) {
closings[closings.length] = { 'type': '/' + openings[len - i - 1].type };
}
return closings;
}
// TODO: check for and fix nesting validity like fixupInsertion does
var tx = new ve.dm.Transaction(), i, j, unwrapOuterData;
range.normalize();
if ( range.start > unwrapOuter.length ) {
// Retain up to the first thing we're unwrapping
// The outer unwrapping takes place *outside*
// the range, so compensate for that
tx.pushRetain( range.start - unwrapOuter.length );
} else if ( range.start < unwrapOuter.length ) {
throw 'unwrapOuter is longer than the data preceding the range';
}
// Replace the opening elements for the outer unwrap&wrap
if ( wrapOuter.length > 0 || unwrapOuter.length > 0 ) {
// Verify that wrapOuter matches the data at this position
unwrapOuterData = doc.data.slice( range.start - unwrapOuter.length, range.start );
for ( i = 0; i < unwrapOuterData.length; i++ ) {
if ( unwrapOuterData[i].type !== unwrapOuter[i].type ) {
throw 'Element in unwrapOuter does not match: expected ' +
unwrapOuter[i].type + ' but found ' + unwrapOuterData[i].type;
}
}
// Instead of putting in unwrapOuter as given, put it in the
// way it appears in the mode,l so we pick up any attributes
tx.pushReplace( unwrapOuterData, ve.copyArray( wrapOuter ) );
}
if ( wrapEach.length > 0 || unwrapEach.length > 0 ) {
var closingUnwrapEach = closingArray( unwrapEach ),
closingWrapEach = closingArray( wrapEach ),
depth = 0,
startOffset,
unwrapEachData;
// Visit each top-level child and wrap/unwrap it
// TODO figure out if we should use the tree/node functions here
// rather than iterating over offsets, it may or may not be faster
for ( i = range.start; i < range.end; i++ ) {
if ( doc.data[i].type === undefined ) {
// This is a content offset, skip
} else {
// This is a structural offset
if ( doc.data[i].type.charAt( 0 ) != '/' ) {
// This is an opening element
if ( depth === 0 ) {
// We are at the start of a top-level element
// Replace the opening elements
// Verify that unwrapEach matches the data at this position
unwrapEachData = doc.data.slice( i, i + unwrapEach.length );
for ( j = 0; j < unwrapEachData.length; j++ ) {
if ( unwrapEachData[j].type !== unwrapEach[j].type ) {
throw 'Element in unwrapEach does not match: expected ' +
unwrapEach[j].type + ' but found ' +
unwrapEachData[j].type;
}
}
// Instead of putting in unwrapEach as given, put it in the
// way it appears in the model, so we pick up any attributes
tx.pushReplace( ve.copyArray( unwrapEachData ), ve.copyArray( wrapEach ) );
// Store this offset for later
startOffset = i;
}
depth++;
} else {
// This is a closing element
depth--;
if ( depth === 0 ) {
// We are at the end of a top-level element
// Retain the contents of what we're wrapping
tx.pushRetain( i - startOffset + 1 - unwrapEach.length*2 );
// Replace the closing elements
tx.pushReplace( ve.copyArray( closingUnwrapEach ), ve.copyArray( closingWrapEach ) );
}
}
}
}
} else {
// There is no wrapEach/unwrapEach to be done, just retain
// up to the end of the range
tx.pushRetain( range.end - range.start );
}
if ( wrapOuter.length > 0 || unwrapOuter.length > 0 ) {
tx.pushReplace( closingArray( unwrapOuter ), closingArray( wrapOuter ) );
}
// Retain up to the end of the document
if ( range.end < doc.data.length ) {
tx.pushRetain( doc.data.length - range.end - unwrapOuter.length );
}
return tx;
};
/* Methods */
/**
* Gets a list of all operations.
*
*
* @method
* @returns {Object[]} List of operations
*/
@ -24,7 +448,7 @@ ve.dm.Transaction.prototype.getOperations = function() {
/**
* Gets the difference in content length this transaction will cause if applied.
*
*
* @method
* @returns {Integer} Difference in content length
*/
@ -34,80 +458,52 @@ ve.dm.Transaction.prototype.getLengthDifference = function() {
/**
* Adds a retain operation.
*
*
* @method
* @param {Integer} length Length of content data to retain
* @throws 'Invalid retain length, can not retain backwards: {length}'
*/
ve.dm.Transaction.prototype.pushRetain = function( length ) {
var end = this.operations.length - 1;
if ( this.operations.length && this.operations[end].type === 'retain' ) {
this.operations[end].length += length;
} else {
this.operations.push( {
'type': 'retain',
'length': length
} );
if ( length < 0 ) {
throw 'Invalid retain length, can not retain backwards:' + length;
}
};
/**
* Adds an insertion operation.
*
* @method
* @param {Array} data Data to retain
*/
ve.dm.Transaction.prototype.pushInsert = function( data ) {
var end = this.operations.length - 1;
if ( this.operations.length && this.operations[end].type === 'insert' ) {
this.operations[end].data = this.operations[end].data.concat( data );
} else {
this.operations.push( {
'type': 'insert',
'data': data
} );
if ( length ) {
var end = this.operations.length - 1;
if ( this.operations.length && this.operations[end].type === 'retain' ) {
this.operations[end].length += length;
} else {
this.operations.push( {
'type': 'retain',
'length': length
} );
}
}
this.lengthDifference += data.length;
};
/**
* Adds a removal operation.
*
* @method
* @param {Array} data Data to remove
*/
ve.dm.Transaction.prototype.pushRemove = function( data ) {
var end = this.operations.length - 1;
if ( this.operations.length && this.operations[end].type === 'remove' ) {
this.operations[end].data = this.operations[end].data.concat( data );
} else {
this.operations.push( {
'type': 'remove',
'data': data
} );
}
this.lengthDifference -= data.length;
};
/**
* Adds a replace operation
*
*
* @method
* @param {Array} remove Data to remove
* @param {Array] replacement Data to replace 'remove' with
* @param {Array] insert Data to replace 'remove' with
*/
ve.dm.Transaction.prototype.pushReplace = function( remove, replacement ) {
ve.dm.Transaction.prototype.pushReplace = function( remove, insert ) {
if ( remove.length === 0 && insert.length === 0 ) {
// Don't push no-ops
return;
}
this.operations.push( {
'type': 'replace',
'remove': remove,
'replacement': replacement
'insert': insert
} );
this.lengthDifference += insert.length - remove.length;
};
/**
* Adds an element attribute change operation.
*
*
* @method
* @param {String} method Method to use, either "set" or "clear"
* @param {String} key Name of attribute to change
* @param {Mixed} from Value change attribute from
* @param {Mixed} to Value to change attribute to
@ -123,7 +519,7 @@ ve.dm.Transaction.prototype.pushReplaceElementAttribute = function( key, from, t
/**
* Adds a start annotating operation.
*
*
* @method
* @param {String} method Method to use, either "set" or "clear"
* @param {Object} annotation Annotation object to start setting or clearing from content data
@ -139,7 +535,7 @@ ve.dm.Transaction.prototype.pushStartAnnotating = function( method, annotation )
/**
* Adds a stop annotating operation.
*
*
* @method
* @param {String} method Method to use, either "set" or "clear"
* @param {Object} annotation Annotation object to stop setting or clearing from content data

View file

@ -1,462 +1,231 @@
/**
* Creates an ve.dm.TransactionProcessor object.
*
* DataModel transaction processor.
*
* This class reads operations from a transaction and applies them one by one. It's not intended
* to be used directly; use the static functions ve.dm.TransactionProcessor.commit() and .rollback()
* instead.
*
* NOTE: Instances of this class are not recyclable: you can only call .process() on them once.
*
* @class
* @constructor
*/
ve.dm.TransactionProcessor = function( model, transaction ) {
this.model = model;
this.transaction = transaction;
ve.dm.TransactionProcessor = function( doc, transaction, reversed ) {
// Properties
this.document = doc;
this.operations = transaction.getOperations();
this.synchronizer = new ve.dm.DocumentSynchronizer( doc );
this.reversed = reversed;
// Linear model offset that we're currently at. Operations in the transaction are ordered, so
// the cursor only ever moves forward.
this.cursor = 0;
this.set = [];
this.clear = [];
// Adjustment used to convert between linear model offsets in the original linear model and
// in the half-updated linear model.
this.adjustment = 0;
// Set and clear are lists of annotations which should be added or removed to content being
// inserted or retained. The format of these objects is { hash: annotationObjectReference }
// where hash is the result of ve.getHash( annotationObjectReference ).
this.set = {};
this.clear = {};
};
/* Static Members */
ve.dm.TransactionProcessor.operationMap = {
// Retain
'retain': {
'commit': function( op ) {
this.retain( op );
},
'rollback': function( op ) {
this.retain( op );
}
},
// Insert
'insert': {
'commit': function( op ) {
this.insert( op );
},
'rollback': function( op ) {
this.remove( op );
}
},
// Remove
'remove': {
'commit': function( op ) {
this.remove( op );
},
'rollback': function( op ) {
this.insert( op );
}
},
'replace': {
'commit': function( op ) {
this.replace( op );
},
'rollback': function( op ) {
this.replace( op );
}
},
// Change element attributes
'attribute': {
'commit': function( op ) {
this.attribute( op );
},
'rollback': function( op ) {
this.attribute( op );
}
},
// Change content annotations
'annotate': {
'commit': function( op ) {
this.mark( op );
},
'rollback': function( op ) {
this.mark( op );
}
}
};
/**
* Processing methods.
*
* Each method is specific to a type of action. Methods are called in the context of a transaction
* processor, so they work similar to normal methods on the object.
*
* @static
* @member
*/
ve.dm.TransactionProcessor.processors = {};
/* Static Methods */
/* Static methods */
/**
* Commit a transaction to a document.
*
* @static
* @method
* @param {ve.dm.Document} doc Document object to apply the transaction to
* @param {ve.dm.Transaction} transaction Transaction to apply
*/
ve.dm.TransactionProcessor.commit = function( doc, transaction ) {
var tp = new ve.dm.TransactionProcessor( doc, transaction );
tp.process( 'commit' );
};
ve.dm.TransactionProcessor.rollback = function( doc, transaction ) {
var tp = new ve.dm.TransactionProcessor( doc, transaction );
tp.process( 'rollback' );
};
/* Methods */
ve.dm.TransactionProcessor.prototype.nextOperation = function() {
return this.operations[this.operationIndex++] || false;
};
ve.dm.TransactionProcessor.prototype.executeOperation = function( op ) {
if ( op.type in ve.dm.TransactionProcessor.operationMap ) {
ve.dm.TransactionProcessor.operationMap[op.type][this.method].call( this, op );
} else {
throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
}
};
ve.dm.TransactionProcessor.prototype.process = function( method ) {
var op;
this.synchronizer = new ve.dm.DocumentSynchronizer( this.model );
// Store the method (commit or rollback) and the operations array so executeOperation()
// can access them easily
this.method = method;
this.operations = this.transaction.getOperations();
// This loop is factored this way to allow operations to be skipped over or executed
// from within other operations
this.operationIndex = 0;
while ( ( op = this.nextOperation() ) ) {
this.executeOperation( op );
}
this.synchronizer.synchronize();
new ve.dm.TransactionProcessor( doc, transaction, false ).process();
};
/**
* Get the parent node that would be affected by inserting given data into it's child.
*
* This is used when inserting data that closes and reopens one or more parent nodes into a child
* node, which requires rebuilding at a higher level.
*
* Roll back a transaction; this applies the transaction to the document in reverse.
*
* @static
* @method
* @param {ve.Node} node Child node to start from
* @param {Array} data Data to inspect for closings
* @returns {ve.Node} Lowest level parent node being affected
* @param {ve.dm.Document} doc Document object to apply the transaction to
* @param {ve.dm.Transaction} transaction Transaction to apply
*/
ve.dm.TransactionProcessor.prototype.getScope = function( node, data ) {
var i,
length,
level = 0,
max = 0;
for ( i = 0, length = data.length; i < length; i++ ) {
if ( typeof data[i].type === 'string' ) {
level += data[i].type.charAt( 0 ) === '/' ? 1 : -1;
max = Math.max( max, level );
}
}
if ( max > 0 ) {
for ( i = 0; i < max; i++ ) {
node = node.getParent() || node;
}
}
return node;
ve.dm.TransactionProcessor.rollback = function( doc, transaction ) {
new ve.dm.TransactionProcessor( doc, transaction, true ).process();
};
ve.dm.TransactionProcessor.prototype.applyAnnotations = function( to, update ) {
var i,
j,
k,
length,
annotation,
changes = 0,
index;
// Handle annotations
if ( this.set.length ) {
for ( i = 0, length = this.set.length; i < length; i++ ) {
annotation = this.set[i];
// Auto-build annotation hash
if ( annotation.hash === undefined ) {
annotation.hash = ve.dm.DocumentNode.getHash( annotation );
}
for ( j = this.cursor; j < to; j++ ) {
// Auto-convert to array
if ( ve.isArray( this.model.data[j] ) ) {
this.model.data[j].push( annotation );
} else {
this.model.data[j] = [this.model.data[j], annotation];
}
}
}
changes++;
}
if ( this.clear.length ) {
for ( i = 0, length = this.clear.length; i < length; i++ ) {
annotation = this.clear[i];
if ( annotation instanceof RegExp ) {
for ( j = this.cursor; j < to; j++ ) {
var matches = ve.dm.DocumentNode.getMatchingAnnotations(
this.model.data[j], annotation
);
for ( k = 0; k < matches.length; k++ ) {
index = this.model.data[j].indexOf( matches[k] );
if ( index !== -1 ) {
this.model.data[j].splice( index, 1 );
}
}
// Auto-convert to string
if ( this.model.data[j].length === 1 ) {
this.model.data[j] = this.model.data[j][0];
}
}
} else {
// Auto-build annotation hash
if ( annotation.hash === undefined ) {
annotation.hash = ve.dm.DocumentNode.getHash( annotation );
}
for ( j = this.cursor; j < to; j++ ) {
index = ve.dm.DocumentNode.getIndexOfAnnotation(
this.model.data[j], annotation
);
if ( index !== -1 ) {
this.model.data[j].splice( index, 1 );
}
// Auto-convert to string
if ( this.model.data[j].length === 1 ) {
this.model.data[j] = this.model.data[j][0];
}
}
}
}
changes++;
}
if ( update && changes ) {
var fromNode = this.model.getNodeFromOffset( this.cursor ),
toNode = this.model.getNodeFromOffset( to );
this.model.traverseLeafNodes( function( node ) {
node.emit( 'update' );
if ( node === toNode ) {
return false;
}
}, fromNode );
}
};
ve.dm.TransactionProcessor.prototype.retain = function( op ) {
this.applyAnnotations( this.cursor + op.length, true );
/**
* Execute a retain operation.
*
* This method is called within the context of a document synchronizer instance.
*
* This moves the cursor by op.length and applies annotations to the characters that the cursor
* moved over.
*
* @static
* @method
* @param {Object} op Operation object:
* @param {Integer} op.length Number of elements to retain
*/
ve.dm.TransactionProcessor.processors.retain = function( op ) {
this.applyAnnotations( this.cursor + op.length );
this.cursor += op.length;
};
ve.dm.TransactionProcessor.prototype.insert = function( op ) {
var node,
index,
offset,
scope;
node = this.model.getNodeFromOffset( this.cursor );
// Shortcut 1: we're inserting content. We don't need to bother with any structural stuff
if ( !ve.dm.DocumentNode.containsElementData( op.data ) ) {
// TODO should we check whether we're at a structural offset, and throw an exception
// if that's the case? Or can we assume that the transaction is valid at this point?
// Insert data into linear model
ve.insertIntoArray( this.model.data, this.cursor, op.data );
this.applyAnnotations( this.cursor + op.data.length );
// Update the length of the containing node
this.synchronizer.pushResize( node, op.data.length );
// Move the cursor
this.cursor += op.data.length;
// All done
return;
}
// Determine the scope of the inserted data. If the data is an enclosed piece of structure,
// this will return node. Otherwise, the data closes one or more nodes, and this will return
// the first ancestor of node that isn't closed, which is the node that will contain the
// inserted data entirely.
scope = this.getScope( node, op.data );
// Shortcut 2: we're inserting an enclosed piece of structural data at a structural offset
// that isn't the end of the document.
// TODO why can't it be at the end of the document?
if (
ve.dm.DocumentNode.isStructuralOffset( this.model.data, this.cursor ) &&
this.cursor != this.model.data.length &&
scope == node
) {
// We're inserting an enclosed element into something else, so we don't have to rebuild
// the parent node. Just build a node from the inserted data and stick it in
ve.insertIntoArray( this.model.data, this.cursor, op.data );
this.applyAnnotations( this.cursor + op.data.length );
offset = this.model.getOffsetFromNode( node );
index = node.getIndexFromOffset( this.cursor - offset );
this.synchronizer.pushRebuild( new ve.Range( this.cursor, this.cursor ),
new ve.Range( this.cursor, this.cursor + op.data.length ) );
/**
* Execute an annotate operation.
*
* This method is called within the context of a document synchronizer instance.
*
* This will add an annotation to or remove an annotation from {this.set} or {this.clear}.
*
* @static
* @method
* @param {Object} op Operation object
* @param {String} op.method Annotation method, either 'set' to add or 'clear' to remove
* @param {String} op.bias Endpoint of marker, either 'start' to begin or 'stop' to end
* @param {String} op.annotation Annotation object to set or clear from content
* @throws 'Invalid annotation method'
*/
ve.dm.TransactionProcessor.processors.annotate = function( op ) {
var target, hash;
if ( op.method === 'set' ) {
target = this.reversed ? this.clear : this.set;
} else if ( op.method === 'clear' ) {
target = this.reversed ? this.set : this.clear;
} else {
// This is the non-shortcut case
// Rebuild all children of scope, which is the node that encloses everything we might have to rebuild
node = scope.getChildren()[0];
offset = this.model.getOffsetFromNode( node );
if ( offset === -1 ) {
throw 'Invalid offset error. Node is not in model tree';
}
// Perform insert on linear data model
ve.insertIntoArray( this.model.data, this.cursor, op.data );
this.applyAnnotations( this.cursor + op.data.length );
// Synchronize model tree
this.synchronizer.pushRebuild( new ve.Range( offset, offset + scope.getContentLength() ),
new ve.Range( offset, offset + scope.getContentLength() + op.data.length ) );
throw 'Invalid annotation method ' + op.method;
}
this.cursor += op.data.length;
hash = $.toJSON( op.annotation );
if ( op.bias === 'start' ) {
target[hash] = op.annotation;
} else {
delete target[hash];
}
// Tree sync is done by applyAnnotations()
};
ve.dm.TransactionProcessor.prototype.remove = function( op ) {
if ( ve.dm.DocumentNode.containsElementData( op.data ) ) {
// TODO rewrite all this
// Figure out which nodes are covered by the removal
var ranges = this.model.selectNodes(
new ve.Range( this.cursor, this.cursor + op.data.length )
);
// Build the list of nodes to rebuild and the data to keep
var oldNodes = [],
newData = [],
parent = null,
index = null,
firstKeptNode,
lastKeptNode,
i;
for ( i = 0; i < ranges.length; i++ ) {
oldNodes.push( ranges[i].node );
if ( ranges[i].range !== undefined ) {
// We have to keep part of this node
if ( firstKeptNode === undefined ) {
// This is the first node we're keeping
firstKeptNode = ranges[i].node;
}
// Compute the start and end offset of this node
// We could do that with getOffsetFromNode() but
// we already have all the numbers we need so why would we
var startOffset = ranges[i].globalRange.start - ranges[i].range.start,
endOffset = startOffset + ranges[i].node.getContentLength(),
// Get this node's data
nodeData = this.model.data.slice( startOffset, endOffset );
// Remove data covered by the range from nodeData
nodeData.splice(
ranges[i].range.start, ranges[i].range.end - ranges[i].range.start
);
// What remains in nodeData is the data we need to keep
// Append it to newData
newData = newData.concat( nodeData );
lastKeptNode = ranges[i].node;
}
}
// Surround newData with the right openings and closings if needed
if ( firstKeptNode !== undefined ) {
// There are a number of conceptually different cases here,
// but the algorithm for dealing with them is the same.
// 1. Removal within one node: firstKeptNode === lastKeptNode
// 2. Merge of siblings: firstKeptNode.getParent() === lastKeptNode.getParent()
// 3. Merge of arbitrary depth: firstKeptNode and lastKeptNode have a common ancestor
// Because #1 and #2 are special cases of #3 (merges with depth=0 and depth=1,
// respectively), the code below that deals with the general case (#3) and automatically
// covers #1 and #2 that way as well.
// Simultaneously traverse upwards from firstKeptNode and lastKeptNode
// to find the common ancestor. On our way up, keep the element of each
// node we visit and verify that the transaction is a valid merge (i.e. it satisfies
// the merge criteria in prepareRemoval()'s canMerge()).
// FIXME: The code is essentially the same as canMerge(), merge these algorithms
var openings = [],
closings = [],
paths = ve.Node.getCommonAncestorPaths( firstKeptNode, lastKeptNode ),
prevN1,
prevN2;
if ( !paths ) {
throw 'Removal is not a valid merge: ' +
'nodes do not have a common ancestor or are not at the same depth';
}
for ( i = 0; i < paths.node1Path.length; i++ ) {
// Verify the element types are equal
if ( paths.node1Path[i].getElementType() !== paths.node2Path[i].getElementType() ) {
throw 'Removal is not a valid merge: ' +
'corresponding parents have different types ( ' +
paths.node1Path[i].getElementType() + ' vs ' +
paths.node2Path[i].getElementType() + ' )';
}
// Record the opening of n1 and the closing of n2
openings.push( paths.node1Path[i].getElement() );
closings.push( { 'type': '/' + paths.node2Path[i].getElementType() } );
}
// Surround newData with the openings and closings
newData = openings.reverse().concat( newData, closings );
// Rebuild oldNodes' ancestors if needed
// This only happens for merges with depth > 1
prevN1 = paths.node1Path.length ? paths.node1Path[paths.node1Path.length - 1] : null;
prevN2 = paths.node2Path.length ? paths.node2Path[paths.node2Path.length - 1] : null;
if ( prevN1 && prevN1 !== oldNodes[0] ) {
oldNodes = [ prevN1 ];
parent = paths.commonAncestor;
index = parent.indexOf( prevN1 ); // Pass to rebuildNodes() so it's not recomputed
if ( index === -1 ) {
throw "Tree corruption detected: node isn't in its parent's children array";
}
var foundPrevN2 = false;
for ( var j = index + 1; !foundPrevN2 && j < parent.getChildren().length; j++ ) {
oldNodes.push( parent.getChildren()[j] );
foundPrevN2 = parent.getChildren()[j] === prevN2;
}
if ( !foundPrevN2 ) {
throw "Tree corruption detected: node isn't in its parent's children array";
}
}
}
// Update the linear model
this.model.data.splice( this.cursor, op.data.length );
// Perform the rebuild. This updates the model tree
// TODO index of oldNodes[0] in its parent should be computed
// on the go by selectNodes() above
if ( parent == null ) {
parent = oldNodes[0].getParent();
}
if ( index == null ) {
index = parent.indexOf( oldNodes[0] );
}
// TODO better offset computation
// TODO allow direct parameter passing in pushRebuild()
var startOffset = this.model.getOffsetFromNode( oldNodes[0] );
var endOffset = this.model.getOffsetFromNode( oldNodes[oldNodes.length-1] ) +
oldNodes[oldNodes.length-1].getElementLength();
this.synchronizer.pushRebuild( new ve.Range( startOffset, endOffset ),
new ve.Range( startOffset, startOffset + newData.length ) );
} else {
// We're removing content only. Take a shortcut
// Get the node we are removing content from
var node = this.model.getNodeFromOffset( this.cursor );
// Update the linear model
this.model.data.splice( this.cursor, op.data.length );
// Queue a resize
this.synchronizer.pushResize( node, -op.data.length );
/**
* Execute an attribute operation.
*
* This method is called within the context of a document synchronizer instance.
*
* This sets the attribute named op.key on the element at this.cursor to op.to , or unsets it if
* op.to === undefined . op.from is not checked against the old value, but is used instead of op.to
* in reverse mode. So if op.from is incorrect, the transaction will commit fine, but won't roll
* back correctly.
*
* @static
* @method
* @param {Object} op Operation object
* @param {String} op.key: Attribute name
* @param {Mixed} op.from: Old attribute value, or undefined if not previously set
* @param {Mixed} op.to: New attribute value, or undefined to unset
*/
ve.dm.TransactionProcessor.processors.attribute = function( op ) {
var element = this.document.data[this.cursor];
if ( element.type === undefined ) {
throw 'Invalid element error, can not set attributes on non-element data';
}
var to = this.reversed ? op.from : op.to;
var from = this.reversed ? op.to : op.from;
if ( to === undefined ) {
// Clear
if ( element.attributes ) {
delete element.attributes[op.key];
}
} else {
// Automatically initialize attributes object
if ( !element.attributes ) {
element.attributes = {};
}
// Set
element.attributes[op.key] = to;
}
this.synchronizer.pushAttributeChange(
this.document.getNodeFromOffset( this.cursor + 1 ),
op.key,
from, to
);
};
ve.dm.TransactionProcessor.prototype.replace = function( op ) {
var invert = this.method == 'rollback',
remove = invert ? op.replacement : op.remove,
replacement = invert ? op.remove : op.replacement,
removeHasStructure = ve.dm.DocumentNode.containsElementData( remove ),
replacementHasStructure = ve.dm.DocumentNode.containsElementData( replacement ),
node;
// remove is provided only for OT / conflict resolution and for
// reversibility, we don't actually verify it here
// Tree synchronization
// Figure out if this is a structural replacement or a content replacement
if ( !ve.dm.DocumentNode.containsElementData( remove ) && !ve.dm.DocumentNode.containsElementData( replacement ) ) {
/**
* Execute a replace operation.
*
* This method is called within the context of a document synchronizer instance.
*
* This replaces a range of linear model data with another at this.cursor, figures out how the model
* tree needs to be synchronized, and queues this in the DocumentSynchronizer.
*
* op.remove isn't checked against the actual data (instead op.remove.length things are removed
* starting at this.cursor), but it's used instead of op.insert in reverse mode. So if
* op.remove is incorrect but of the right length, the transaction will commit fine, but won't roll
* back correctly.
*
* @static
* @method
* @param {Object} op Operation object
* @param {Array} op.remove Linear model data to remove
* @param {Array} op.insert Linear model data to insert
*/
ve.dm.TransactionProcessor.processors.replace = function( op ) {
var remove = this.reversed ? op.insert : op.remove,
insert = this.reversed ? op.remove : op.insert,
removeIsContent = ve.dm.Document.isContentData( remove ),
insertIsContent = ve.dm.Document.isContentData( insert ),
node, selection;
if ( removeIsContent && insertIsContent ) {
// Content replacement
// Update the linear model
ve.batchedSplice( this.model.data, this.cursor, remove.length, replacement );
this.applyAnnotations( this.cursor + replacement.length );
ve.batchSplice( this.document.data, this.cursor, remove.length, insert );
this.applyAnnotations( this.cursor + insert.length );
// Get the node containing the replaced content
node = this.model.getNodeFromOffset( this.cursor );
// Queue a resize for this node
this.synchronizer.pushResize( node, replacement.length - remove.length );
selection = this.document.selectNodes(
new ve.Range(
this.cursor - this.adjustment,
this.cursor - this.adjustment + remove.length
),
'leaves'
);
var removeHasStructure = ve.dm.Document.containsElementData( remove ),
insertHasStructure = ve.dm.Document.containsElementData( insert );
if ( removeHasStructure || insertHasStructure ) {
// Replacement is not exclusively text
// Rebuild all covered nodes
var range = new ve.Range( selection[0].nodeRange.start,
selection[selection.length - 1].nodeRange.end );
this.synchronizer.pushRebuild( range,
new ve.Range( range.start + this.adjustment,
range.end + this.adjustment + insert.length - remove.length )
);
} else {
// Text-only replacement
// Queue a resize for this node
node = selection[0].node;
this.synchronizer.pushResize( node, insert.length - remove.length );
}
// Advance the cursor
this.cursor += replacement.length;
this.cursor += insert.length;
this.adjustment += insert.length - remove.length;
} else {
// Structural replacement
// TODO generalize for insert/remove
// It's possible that multiple replace operations are needed before the
// model is back in a consistent state. This loop applies the current
// replace operation to the linear model, then keeps applying subsequent
@ -464,22 +233,50 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
// and queue a single rebuild after the loop finishes.
var operation = op,
removeLevel = 0,
replaceLevel = 0,
insertLevel = 0,
startOffset = this.cursor,
adjustment = 0,
i,
type;
type,
prevCursor,
affectedRanges = [],
scope,
minInsertLevel = 0,
coveringRange,
scopeStart,
scopeEnd;
while ( true ) {
if ( operation.type == 'replace' ) {
var opRemove = invert ? operation.replacement : operation.remove,
opReplacement = invert ? operation.remove : operation.replacement;
// Update the linear model for this replacement
ve.batchedSplice( this.model.data, this.cursor, opRemove.length, opReplacement );
this.cursor += opReplacement.length;
adjustment += opReplacement.length - opRemove.length;
// Walk through the remove and replacement data
var opRemove = this.reversed ? operation.insert : operation.remove,
opInsert = this.reversed ? operation.remove : operation.insert;
// Update the linear model for this insert
ve.batchSplice( this.document.data, this.cursor, opRemove.length, opInsert );
affectedRanges.push( new ve.Range(
this.cursor - this.adjustment,
this.cursor - this.adjustment + opRemove.length
) );
prevCursor = this.cursor;
this.cursor += opInsert.length;
// Paint the removed selection, figure out which nodes were
// covered, and add their ranges to the affected ranges list
if ( opRemove.length > 0 ) {
selection = this.document.selectNodes( new ve.Range(
prevCursor - this.adjustment,
prevCursor + opRemove.length - this.adjustment
), 'siblings' );
for ( i = 0; i < selection.length; i++ ) {
// .nodeRange is the inner range, we need the
// outer range (including opening and closing)
if ( selection[i].node.isWrapped() ) {
affectedRanges.push( new ve.Range(
selection[i].nodeRange.start - 1,
selection[i].nodeRange.end + 1
) );
} else {
affectedRanges.push( selection[i].nodeRange );
}
}
}
// Walk through the remove and insert data
// and keep track of the element depth change (level)
// for each of these two separately. The model is
// only consistent if both levels are zero.
@ -495,104 +292,168 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
removeLevel++;
}
}
for ( i = 0; i < opReplacement.length; i++ ) {
type = opReplacement[i].type;
// Keep track of the scope of the insertion
// Normally this is the node we're inserting into, except if the
// insertion closes elements it doesn't open (i.e. splits elements),
// in which case it's the affected ancestor
for ( i = 0; i < opInsert.length; i++ ) {
type = opInsert[i].type;
if ( type === undefined ) {
// This is content, ignore
} else if ( type.charAt( 0 ) === '/' ) {
// Closing element
replaceLevel--;
insertLevel--;
if ( insertLevel < minInsertLevel ) {
// Closing an unopened element at a higher
// (more negative) level than before
// Lazy-initialize scope
scope = scope || this.document.getNodeFromOffset( prevCursor );
// Push the full range of the old scope as an affected range
scopeStart = this.document.getDocumentNode().getOffsetFromNode( scope );
scopeEnd = scopeStart + scope.getOuterLength();
affectedRanges.push( new ve.Range( scopeStart, scopeEnd ) );
// Update scope
scope = scope.getParent() || scope;
}
} else {
// Opening element
replaceLevel++;
insertLevel++;
}
}
// Update adjustment
this.adjustment += opInsert.length - opRemove.length;
} else {
// We're assuming that other operations will not cause
// adjustments.
// TODO actually make this the case by folding insert
// and delete into replace
// We know that other operations won't cause adjustments, so we
// don't have to update adjustment
this.executeOperation( operation );
}
if ( removeLevel === 0 && replaceLevel === 0 ) {
if ( removeLevel === 0 && insertLevel === 0 ) {
// The model is back in a consistent state, so we're done
break;
}
// Get the next operation
operation = this.nextOperation();
if ( !operation ) {
throw 'Unbalanced set of replace operations found';
}
}
// Queue a rebuild for the replaced node
this.synchronizer.pushRebuild( new ve.Range( startOffset, this.cursor - adjustment ),
new ve.Range( startOffset, this.cursor ) );
// From all the affected ranges we have gathered, compute a range that covers all
// of them, and rebuild that
coveringRange = ve.Range.newCoveringRange( affectedRanges );
this.synchronizer.pushRebuild( coveringRange, new ve.Range( coveringRange.start,
coveringRange.end + this.adjustment )
);
}
};
ve.dm.TransactionProcessor.prototype.attribute = function( op ) {
var invert = this.method == 'rollback',
element = this.model.data[this.cursor];
if ( element.type === undefined ) {
throw 'Invalid element error. Can not set attributes on non-element data.';
}
var to = invert ? op.from : op.to;
if ( to === undefined ) {
// Clear
if ( element.attributes ) {
delete element.attributes[op.key];
}
// Automatically clean up attributes object
var empty = true;
for ( var key in element.attributes ) {
empty = false;
break;
}
if ( empty ) {
delete element.attributes;
}
/* Methods */
/**
* Gets the next operation.
*
* @method
*/
ve.dm.TransactionProcessor.prototype.nextOperation = function() {
return this.operations[this.operationIndex++] || false;
};
/**
* Executes an operation.
*
* @method
* @param {Object} op Operation object to execute
* @throws 'Invalid operation error. Operation type is not supported'
*/
ve.dm.TransactionProcessor.prototype.executeOperation = function( op ) {
if ( op.type in ve.dm.TransactionProcessor.processors ) {
ve.dm.TransactionProcessor.processors[op.type].call( this, op );
} else {
// Automatically initialize attributes object
if ( !element.attributes ) {
element.attributes = {};
}
// Set
element.attributes[op.key] = to;
}
var node = this.model.getNodeFromOffset( this.cursor + 1 );
if ( node.hasChildren() ) {
node.traverseLeafNodes( function( leafNode ) {
leafNode.emit( 'update' );
} );
} else {
node.emit( 'update' );
throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
}
};
ve.dm.TransactionProcessor.prototype.mark = function( op ) {
var invert = this.method == 'rollback',
target;
if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
target = this.set;
} else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
target = this.clear;
} else {
throw 'Invalid method error. Can not operate attributes this way: ' + method;
}
if ( op.bias === 'start' ) {
target.push( op.annotation );
} else if ( op.bias === 'stop' ) {
var index;
if ( op.annotation instanceof RegExp ) {
index = target.indexOf( op.annotation );
} else {
index = ve.dm.DocumentNode.getIndexOfAnnotation( target, op.annotation );
}
if ( index === -1 ) {
throw 'Annotation stack error. Annotation is missing.';
}
target.splice( index, 1 );
/**
* Processes all operations.
*
* When all operations are done being processed, the document will be synchronized.
*
* @method
*/
ve.dm.TransactionProcessor.prototype.process = function() {
var op;
// This loop is factored this way to allow operations to be skipped over or executed
// from within other operations
this.operationIndex = 0;
while ( ( op = this.nextOperation() ) ) {
this.executeOperation( op );
}
this.synchronizer.synchronize();
};
/**
* Apply the current annotation stacks. This will set all annotations in this.set and clear all
* annotations in this.clear on the data between the offsets this.cursor and this.cursor + to
*
* @method
* @param {Number} to Offset to stop annotating at. Annotating starts at this.cursor
* @throws 'Invalid transaction, can not annotate a branch element'
* @throws 'Invalid transaction, annotation to be set is already set'
* @throws 'Invalid transaction, annotation to be cleared is not set'
*/
ve.dm.TransactionProcessor.prototype.applyAnnotations = function( to ) {
if ( ve.isEmptyObject( this.set ) && ve.isEmptyObject( this.clear ) ) {
return;
}
var item,
element,
annotated,
annotations,
hash,
empty;
for ( var i = this.cursor; i < to; i++ ) {
item = this.document.data[i];
element = item.type !== undefined;
if ( element ) {
if ( item.type.charAt( 0 ) === '/' ) {
throw 'Invalid transaction, cannot annotate a branch closing element';
} else if ( ve.dm.nodeFactory.canNodeHaveChildren( item.type ) ) {
throw 'Invalid transaction, cannot annotate a branch opening element';
}
}
annotated = element ? 'annotations' in item : ve.isArray( item );
annotations = annotated ? ( element ? item.annotations : item[1] ) : {};
// Set and clear annotations
for ( hash in this.set ) {
if ( hash in annotations ) {
throw 'Invalid transaction, annotation to be set is already set';
}
annotations[hash] = this.set[hash];
}
for ( hash in this.clear ) {
if ( !( hash in annotations ) ) {
throw 'Invalid transaction, annotation to be cleared is not set';
}
delete annotations[hash];
}
// Auto initialize/cleanup
if ( !ve.isEmptyObject( annotations ) && !annotated ) {
if ( element ) {
// Initialize new element annotation
item.annotations = annotations;
} else {
// Initialize new character annotation
this.document.data[i] = [item, annotations];
}
} else if ( ve.isEmptyObject( annotations ) && annotated ) {
if ( element ) {
// Cleanup empty element annotation
delete item.annotations;
} else {
// Cleanup empty character annotation
this.document.data[i] = item[0];
}
}
}
this.synchronizer.pushAnnotation( new ve.Range( this.cursor, to ) );
};

View file

@ -1,6 +1,9 @@
/**
* VisualEditor DataModel namespace.
*
* DataModel namespace.
*
* All classes and functions will be attached to this object to keep the global namespace clean.
*/
ve.dm = {};
ve.dm = {
//'nodeFactory': Initialized in ve.dm.NodeFactory.js
//'converter': Initialized in ve.dm.Converter.js
};

View file

@ -1,71 +0,0 @@
/**
* Creates an ve.es.DocumentNode object.
*
* @class
* @constructor
* @extends {ve.es.BranchNode}
* @param {ve.dm.DocumentNode} documentModel Document model to view
* @param {ve.es.Surface} surfaceView Surface view this view is a child of
*/
ve.es.DocumentNode = function( model, surfaceView ) {
// Inheritance
ve.es.BranchNode.call( this, model );
// Properties
this.surfaceView = surfaceView;
// DOM Changes
this.$.addClass( 'es-documentView' );
};
/* Static Members */
/**
* Mapping of symbolic names and splitting rules.
*
* Each rule is an object with a self and children property. Each of these properties may contain
* one of two possible values:
* Boolean - Whether a split is allowed
* Null - Node is a leaf, so there's nothing to split
*
* @example Paragraph rules
* {
* 'self': true
* 'children': null
* }
* @example List rules
* {
* 'self': false,
* 'children': true
* }
* @example ListItem rules
* {
* 'self': true,
* 'children': false
* }
*/
ve.es.DocumentNode.splitRules = {};
/* Methods */
/**
* Get the document offset of a position created from passed DOM event
*
* @method
* @param e {Event} Event to create ve.Position from
* @returns {Integer} Document offset
*/
ve.es.DocumentNode.prototype.getOffsetFromEvent = function( e ) {
var position = ve.Position.newFromEventPagePosition( e );
return this.getOffsetFromRenderedPosition( position );
};
ve.es.DocumentNode.splitRules.document = {
'self': false,
'children': true
};
/* Inheritance */
ve.extendClass( ve.es.DocumentNode, ve.es.BranchNode );

View file

@ -1,53 +0,0 @@
/**
* Creates an ve.es.HeadingNode object.
*
* @class
* @constructor
* @extends {ve.es.LeafNode}
* @param {ve.dm.HeadingNode} model Heading model to view
*/
ve.es.HeadingNode = function( model ) {
// Inheritance
ve.es.LeafNode.call( this, model );
// Properties
this.currentLevelHash = null;
// DOM Changes
this.$.addClass( 'es-headingView' );
// Events
var _this = this;
this.model.on( 'update', function() {
_this.setClasses();
} );
// Initialization
this.setClasses();
};
/* Methods */
ve.es.HeadingNode.prototype.setClasses = function() {
var level = this.model.getElementAttribute( 'level' );
if ( level !== this.currentLevelHash ) {
this.currentLevelHash = level;
var classes = this.$.attr( 'class' );
this.$
// Remove any existing level classes
.attr( 'class', classes.replace( / ?es-headingView-level[0-9]+/, '' ) )
// Add a new level class
.addClass( 'es-headingView-level' + level );
}
};
/* Registration */
ve.es.DocumentNode.splitRules.heading = {
'self': true,
'children': null
};
/* Inheritance */
ve.extendClass( ve.es.HeadingNode, ve.es.LeafNode );

View file

@ -1,62 +0,0 @@
/**
* Creates an ve.es.ListItemNode object.
*
* @class
* @constructor
* @extends {ve.es.LeafNode}
* @param {ve.dm.ListItemNode} model List item model to view
*/
ve.es.ListItemNode = function( model ) {
// Inheritance
ve.es.BranchNode.call( this, model );
// Properties
this.$icon = $( '<div class="es-listItemView-icon"></div>' ).prependTo( this.$ );
this.currentStylesHash = null;
// DOM Changes
this.$.addClass( 'es-listItemView' );
// Events
var _this = this;
this.model.on( 'update', function() {
_this.setClasses();
} );
// Initialization
this.setClasses();
};
/* Methods */
ve.es.ListItemNode.prototype.setClasses = function() {
var styles = this.model.getElementAttribute( 'styles' ),
stylesHash = styles.join( '|' );
if ( this.currentStylesHash !== stylesHash ) {
this.currentStylesHash = stylesHash;
var classes = this.$.attr( 'class' );
this.$
// Remove any existing level classes
.attr(
'class',
classes
.replace( / ?es-listItemView-level[0-9]+/, '' )
.replace( / ?es-listItemView-(bullet|number|term|definition)/, '' )
)
// Set the list style class from the style on top of the stack
.addClass( 'es-listItemView-' + styles[styles.length - 1] )
// Set the list level class from the length of the stack
.addClass( 'es-listItemView-level' + ( styles.length - 1 ) );
}
};
/* Registration */
ve.es.DocumentNode.splitRules.listItem = {
'self': true,
'children': false
};
/* Inheritance */
ve.extendClass( ve.es.ListItemNode, ve.es.BranchNode );

View file

@ -1,62 +0,0 @@
/**
* Creates an ve.es.ListNode object.
*
* @class
* @constructor
* @extends {ve.es.BranchNode}
* @param {ve.dm.ListNode} model List model to view
*/
ve.es.ListNode = function( model ) {
// Inheritance
ve.es.BranchNode.call( this, model );
// DOM Changes
this.$.addClass( 'es-listView' );
// Events
var _this = this;
this.model.on( 'update', function() {
_this.enumerate();
} );
// Initialization
this.enumerate();
};
/* Methods */
/**
* Set the number labels of all ordered list items.
*
* @method
*/
ve.es.ListNode.prototype.enumerate = function() {
var styles,
levels = [];
for ( var i = 0; i < this.children.length; i++ ) {
styles = this.children[i].model.getElementAttribute( 'styles' );
levels = levels.slice( 0, styles.length );
if ( styles[styles.length - 1] === 'number' ) {
if ( !levels[styles.length - 1] ) {
levels[styles.length - 1] = 0;
}
this.children[i].$icon.text( ++levels[styles.length - 1] + '.' );
} else {
this.children[i].$icon.text( '' );
if ( levels[styles.length - 1] ) {
levels[styles.length - 1] = 0;
}
}
}
};
/* Registration */
ve.es.DocumentNode.splitRules.list = {
'self': false,
'children': true
};
/* Inheritance */
ve.extendClass( ve.es.ListNode, ve.es.BranchNode );

View file

@ -1,26 +0,0 @@
/**
* Creates an ve.es.ParagraphNode object.
*
* @class
* @constructor
* @extends {ve.es.LeafNode}
* @param {ve.dm.ParagraphNode} model Paragraph model to view
*/
ve.es.ParagraphNode = function( model ) {
// Inheritance
ve.es.LeafNode.call( this, model );
// DOM Changes
this.$.addClass( 'es-paragraphView' );
};
/* Registration */
ve.es.DocumentNode.splitRules.paragraph = {
'self': true,
'children': null
};
/* Inheritance */
ve.extendClass( ve.es.ParagraphNode, ve.es.LeafNode );

View file

@ -1,26 +0,0 @@
/**
* Creates an ve.es.PreNode object.
*
* @class
* @constructor
* @extends {ve.es.LeafNode}
* @param {ve.dm.PreNode} model Pre model to view
*/
ve.es.PreNode = function( model ) {
// Inheritance
ve.es.LeafNode.call( this, model, undefined, { 'pre': true } );
// DOM Changes
this.$.addClass( 'es-preView' );
};
/* Registration */
ve.es.DocumentNode.splitRules.pre = {
'self': true,
'children': null
};
/* Inheritance */
ve.extendClass( ve.es.PreNode, ve.es.LeafNode );

View file

@ -1,28 +0,0 @@
/**
* Creates an ve.es.TableCellNode object.
*
* @class
* @constructor
* @extends {ve.es.BranchNode}
* @param {ve.dm.TableCellNode} model Table cell model to view
*/
ve.es.TableCellNode = function( model ) {
// Inheritance
ve.es.BranchNode.call( this, model, $( '<td>' ) );
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 'es-tableCellView' );
};
/* Registration */
ve.es.DocumentNode.splitRules.tableCell = {
'self': false,
'children': true
};
/* Inheritance */
ve.extendClass( ve.es.TableCellNode, ve.es.BranchNode );

View file

@ -1,28 +0,0 @@
/**
* Creates an ve.es.TableNode object.
*
* @class
* @constructor
* @extends {ve.es.BranchNode}
* @param {ve.dm.TableNode} model Table model to view
*/
ve.es.TableNode = function( model ) {
// Inheritance
ve.es.BranchNode.call( this, model, $( '<table>' ) );
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 'es-tableView' );
};
/* Registration */
ve.es.DocumentNode.splitRules.table = {
'self': false,
'children': false
};
/* Inheritance */
ve.extendClass( ve.es.TableNode, ve.es.BranchNode );

View file

@ -1,28 +0,0 @@
/**
* Creates an ve.es.TableRowNode object.
*
* @class
* @constructor
* @extends {ve.es.BranchNode}
* @param {ve.dm.TableRowNode} model Table row model to view
*/
ve.es.TableRowNode = function( model ) {
// Inheritance
ve.es.BranchNode.call( this, model, $( '<tr>' ), true );
// DOM Changes
this.$
.attr( 'style', model.getElementAttribute( 'html/style' ) )
.addClass( 'es-tableRowView' );
};
/* Registration */
ve.es.DocumentNode.splitRules.tableRow = {
'self': false,
'children': false
};
/* Inheritance */
ve.extendClass( ve.es.TableRowNode, ve.es.BranchNode );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

View file

@ -1,97 +0,0 @@
.es-contentView {
position: relative;
z-index: 1;
}
.es-contentView-line,
.es-contentView-ruler-line {
line-height: 1.5em;
cursor: text;
white-space: nowrap;
color: #000000;
}
.es-contentView-ruler-line {
position: absolute;
top: 0;
left: 0;
display: inline-block;
z-index: -1000;
}
.es-contentView-ruler-right {
float: right;
}
.es-contentView-ruler-left {
float: left;
}
.es-contentView-line.empty {
display: block;
width: 0px;
}
.es-contentView-whitespace {
color: #ffffff;
}
.es-contentView-range {
display: none;
position: absolute;
background-color: #b3d6f6;
cursor: text;
z-index: -1;
}
.es-contentView-format-object {
background-color: rgba(0,0,0,0.05);
border-radius: 0.25em;
margin: 1px 0 1px 1px;
padding: 0.25em 0;
cursor: default;
}
.es-contentView-format-object * {
cursor: default !important;
}
.es-contentView-format-object a:link,
.es-contentView-format-object a:visited,
.es-contentView-format-object a:active {
color: #0645AD;
text-decoration: none;
}
.es-contentView-format-textStyle-italic,
.es-contentView-format-textStyle-emphasize {
font-style: italic;
}
.es-contentView-format-textStyle-bold,
.es-contentView-format-textStyle-strong {
font-weight: bold;
}
.es-contentView-format-link {
color: #0645AD;
text-decoration: underline;
}
.es-contentView-format-textStyle-big {
font-size: 1.2em;
}
.es-contentView-format-textStyle-small,
.es-contentView-format-textStyle-subScript,
.es-contentView-format-textStyle-superScript {
font-size: .8em;
}
.es-contentView-format-textStyle-subScript {
vertical-align: sub;
}
.es-contentView-format-textStyle-superScript {
vertical-align: super;
}

View file

@ -1,159 +0,0 @@
.es-documentView {
cursor: text;
margin-top: 1em;
overflow: hidden;
-webkit-user-select: none;
}
.es-headingView,
.es-tableView,
.es-listView,
.es-preView,
.es-paragraphView {
margin: 1em;
margin-top: 0;
position: relative;
min-height: 1.5em;
}
.es-listItemView > .es-paragraphView {
margin-left: 0;
margin-right: 0;
}
.es-listItemView > .es-viewBranchNode-firstChild {
margin: 0;
}
.es-preView {
padding: 1em;
border: 1px dashed #2F6FAB;
}
.es-preView > * {
font-family: monospace,"Courier New";
}
.es-headingView-level1,
.es-headingView-level2 {
border-bottom: 1px solid #AAA;
}
.es-headingView-level1 > * {
font-size: 188%;
font-weight: normal;
}
.es-headingView-level2 > * {
font-size: 150%;
font-weight: normal;
}
.es-headingView-level3 > * {
font-size: 132%;
font-weight: bold;
}
.es-headingView-level4 > * {
font-size: 116%;
font-weight: bold;
}
.es-headingView-level5 > * {
font-size: 100%;
font-weight: bold;
}
.es-headingView-level6 > * {
font-size: 80%;
font-weight: bold;
}
.es-listItemView {
position: relative;
}
.es-listItemView-bullet {
padding-left: 1.2em;
}
.es-listItemView-number {
padding-left: 3.2em;
}
.es-listItemView-icon {
position: absolute;
right: 100%;
height: 1.5em;
line-height: 1.5em;
}
.es-listItemView-bullet .es-listItemView-icon {
background-image: url(images/bullet-icon.png);
background-position: left 0.6em;
background-repeat: no-repeat;
width: 5px;
margin-right: -0.5em;
}
.es-listItemView-number .es-listItemView-icon {
margin-right: -2.8em;
}
.es-listItemView-term {
font-weight: bold;
}
.es-listItemView-definition .es-contentView {
margin-left: 2em;
}
.es-listItemView-level0 {
margin-left: 0;
}
.es-listItemView-level1 {
margin-left: 2em;
}
.es-listItemView-level2 {
margin-left: 4em;
}
.es-listItemView-level3 {
margin-left: 6em;
}
.es-listItemView-level4 {
margin-left: 8em;
}
.es-listItemView-level5 {
margin-left: 10em;
}
.es-listItemView-level6 {
margin-left: 12em;
}
.es-listItemView-level1.es-listItemView-number {
margin-left: 4em;
}
.es-listItemView-level2.es-listItemView-number {
margin-left: 8em;
}
.es-listItemView-level3.es-listItemView-number {
margin-left: 12em;
}
.es-listItemView-level4.es-listItemView-number {
margin-left: 16em;
}
.es-listItemView-level5.es-listItemView-number {
margin-left: 18em;
}
.es-listItemView-level6.es-listItemView-number {
margin-left: 22em;
}

View file

@ -1,34 +0,0 @@
.es-surfaceView {
overflow: hidden;
font-size: 1em; /* to look more like MediaWiki use: 0.8em */;
margin-left: -1em;
margin-right: -1em;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select;
user-select: none;
}
.es-surfaceView-textarea {
position: absolute;
z-index: -1;
opacity: 0;
color: white;
background-color: white;
border: none;
padding: 0;
margin: 0;
width: 1px;
}
.es-surfaceView-textarea:focus {
outline: none;
}
.es-surfaceView-cursor {
position: absolute;
background-color: black;
width: 1px;
display: none;
}

View file

@ -1,272 +0,0 @@
/**
* Creates an ve.es.BranchNode object.
*
* @class
* @abstract
* @constructor
* @extends {ve.BranchNode}
* @extends {ve.es.Node}
* @param model {ve.ModelNode} Model to observe
* @param {jQuery} [$element] Element to use as a container
*/
ve.es.BranchNode = function( model, $element, horizontal ) {
// Inheritance
ve.BranchNode.call( this );
ve.es.Node.call( this, model, $element );
// Properties
this.horizontal = horizontal || false;
if ( model ) {
// Append existing model children
var childModels = model.getChildren();
for ( var i = 0; i < childModels.length; i++ ) {
this.onAfterPush( childModels[i] );
}
// Observe and mimic changes on model
this.model.addListenerMethods( this, {
'afterPush': 'onAfterPush',
'afterUnshift': 'onAfterUnshift',
'afterPop': 'onAfterPop',
'afterShift': 'onAfterShift',
'afterSplice': 'onAfterSplice',
'afterSort': 'onAfterSort',
'afterReverse': 'onAfterReverse'
} );
}
};
/* Methods */
ve.es.BranchNode.prototype.onAfterPush = function( childModel ) {
var childView = childModel.createView();
this.emit( 'beforePush', childView );
childView.attach( this );
childView.on( 'update', this.emitUpdate );
// Update children
this.children.push( childView );
// Update DOM
this.$.append( childView.$ );
// TODO: adding and deleting classes has to be implemented for unshift, shift, splice, sort
// and reverse as well
if ( this.children.length === 1 ) {
childView.$.addClass('es-viewBranchNode-firstChild');
}
this.emit( 'afterPush', childView );
this.emit( 'update' );
};
ve.es.BranchNode.prototype.onAfterUnshift = function( childModel ) {
var childView = childModel.createView();
this.emit( 'beforeUnshift', childView );
childView.attach( this );
childView.on( 'update', this.emitUpdate );
// Update children
this.children.unshift( childView );
// Update DOM
this.$.prepend( childView.$ );
this.emit( 'afterUnshift', childView );
this.emit( 'update' );
};
ve.es.BranchNode.prototype.onAfterPop = function() {
this.emit( 'beforePop' );
// Update children
var childView = this.children.pop();
childView.detach();
childView.removeEventListener( 'update', this.emitUpdate );
// Update DOM
childView.$.detach();
this.emit( 'afterPop' );
this.emit( 'update' );
};
ve.es.BranchNode.prototype.onAfterShift = function() {
this.emit( 'beforeShift' );
// Update children
var childView = this.children.shift();
childView.detach();
childView.removeEventListener( 'update', this.emitUpdate );
// Update DOM
childView.$.detach();
this.emit( 'afterShift' );
this.emit( 'update' );
};
ve.es.BranchNode.prototype.onAfterSplice = function( index, howmany ) {
var i,
length,
args = Array.prototype.slice.call( arguments, 0 );
// Convert models to views and attach them to this node
if ( args.length >= 3 ) {
for ( i = 2, length = args.length; i < length; i++ ) {
args[i] = args[i].createView();
}
}
this.emit.apply( this, ['beforeSplice'].concat( args ) );
var removals = this.children.splice.apply( this.children, args );
for ( i = 0, length = removals.length; i < length; i++ ) {
removals[i].detach();
removals[i].removeListener( 'update', this.emitUpdate );
// Update DOM
removals[i].$.detach();
}
if ( args.length >= 3 ) {
var $target;
if ( index ) {
// Get the element before the insertion point
$anchor = this.$.children().eq( index - 1 );
}
for ( i = args.length - 1; i >= 2; i-- ) {
args[i].attach( this );
args[i].on( 'update', this.emitUpdate );
if ( index ) {
$anchor.after( args[i].$ );
} else {
this.$.prepend( args[i].$ );
}
}
}
this.emit.apply( this, ['afterSplice'].concat( args ) );
if ( args.length >= 3 ) {
for ( i = 2, length = args.length; i < length; i++ ) {
args[i].renderContent();
}
}
this.emit( 'update' );
};
ve.es.BranchNode.prototype.onAfterSort = function() {
this.emit( 'beforeSort' );
var childModels = this.model.getChildren();
for ( var i = 0; i < childModels.length; i++ ) {
for ( var j = 0; j < this.children.length; j++ ) {
if ( this.children[j].getModel() === childModels[i] ) {
var childView = this.children[j];
// Update children
this.children.splice( j, 1 );
this.children.push( childView );
// Update DOM
this.$.append( childView.$ );
}
}
}
this.emit( 'afterSort' );
this.emit( 'update' );
this.renderContent();
};
ve.es.BranchNode.prototype.onAfterReverse = function() {
this.emit( 'beforeReverse' );
// Update children
this.reverse();
// Update DOM
this.$.children().each( function() {
$(this).prependTo( $(this).parent() );
} );
this.emit( 'afterReverse' );
this.emit( 'update' );
this.renderContent();
};
/**
* Render content.
*
* @method
*/
ve.es.BranchNode.prototype.renderContent = function() {
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].renderContent();
}
};
/**
* Draw selection around a given range.
*
* @method
* @param {ve.Range} range Range of content to draw selection around
*/
ve.es.BranchNode.prototype.drawSelection = function( range ) {
var selectedNodes = this.selectNodes( range, true );
for ( var i = 0; i < this.children.length; i++ ) {
if ( selectedNodes.length && this.children[i] === selectedNodes[0].node ) {
for ( var j = 0; j < selectedNodes.length; j++ ) {
selectedNodes[j].node.drawSelection( selectedNodes[j].range );
}
i += selectedNodes.length - 1;
} else {
this.children[i].clearSelection();
}
}
};
/**
* Clear selection.
*
* @method
*/
ve.es.BranchNode.prototype.clearSelection = function() {
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].clearSelection();
}
};
/**
* Gets the nearest offset of a rendered position.
*
* @method
* @param {ve.Position} position Position to get offset for
* @returns {Integer} Offset of position
*/
ve.es.BranchNode.prototype.getOffsetFromRenderedPosition = function( position ) {
if ( this.children.length === 0 ) {
return 0;
}
var node = this.children[0];
for ( var i = 1; i < this.children.length; i++ ) {
if ( this.horizontal && this.children[i].$.offset().left > position.left ) {
break;
} else if ( !this.horizontal && this.children[i].$.offset().top > position.top ) {
break;
}
node = this.children[i];
}
return node.getParent().getOffsetFromNode( node, true ) +
node.getOffsetFromRenderedPosition( position ) + 1;
};
/**
* Gets rendered position of offset within content.
*
* @method
* @param {Integer} offset Offset to get position for
* @returns {ve.Position} Position of offset
*/
ve.es.BranchNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
var node = this.getNodeFromOffset( offset, true );
if ( node !== null ) {
return node.getRenderedPositionFromOffset(
offset - this.getOffsetFromNode( node, true ) - 1,
leftBias
);
}
return null;
};
ve.es.BranchNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
var node = this.getNodeFromOffset( offset, true );
if ( node !== null ) {
var nodeOffset = this.getOffsetFromNode( node, true );
return ve.Range.newFromTranslatedRange(
node.getRenderedLineRangeFromOffset( offset - nodeOffset - 1 ),
nodeOffset + 1
);
}
return null;
};
/* Inheritance */
ve.extendClass( ve.es.BranchNode, ve.BranchNode );
ve.extendClass( ve.es.BranchNode, ve.es.Node );

View file

@ -1,961 +0,0 @@
/**
* Creates an ve.es.Content object.
*
* A content view flows text into a DOM element and provides methods to get information about the
* rendered output. HTML serialized specifically for rendering into and editing surface.
*
* Rendering occurs automatically when content is modified, by responding to "update" events from
* the model. Rendering is iterative and interruptable to reduce user feedback latency.
*
* TODO: Cleanup code and comments
*
* @class
* @constructor
* @param {jQuery} $container Element to render into
* @param {ve.ModelNode} model Model to produce view for
* @param {Object} options List of options
* @param {Boolean} options.pre Line breaks should be respected
* @property {jQuery} $
* @property {ve.ModelNode} model
* @property {Array} boundaries
* @property {Array} lines
* @property {Integer} width
* @property {RegExp} bondaryTest
* @property {Object} widthCache
* @property {Object} renderState
* @property {Object} contentCache
*/
ve.es.Content = function( $container, model, options ) {
// Inheritance
ve.EventEmitter.call( this );
// Properties
this.$ = $container;
this.model = model;
this.boundaries = [];
this.breaks = {};
this.lines = [];
this.width = null;
this.boundaryTest = /([ \-\t\r\n\f])/g;
this.widthCache = {};
this.renderState = {};
this.contentCache = null;
this.options = options || {};
if ( model ) {
// Events
var _this = this;
this.model.on( 'update', function( offset ) {
_this.scanBoundaries();
_this.render( offset || 0 );
} );
// DOM Changes
this.$ranges = $( '<div class="es-contentView-ranges"></div>' );
this.$rangeStart = $( '<div class="es-contentView-range"></div>' );
this.$rangeFill = $( '<div class="es-contentView-range"></div>' );
this.$rangeEnd = $( '<div class="es-contentView-range"></div>' );
this.$.prepend( this.$ranges.append( this.$rangeStart, this.$rangeFill, this.$rangeEnd ) );
this.$rulers = $( '<div class="es-contentView-rulers"></div>' );
this.$rulerLeft = $( '<div class="es-contentView-ruler-left"></div>' );
this.$rulerRight = $( '<div class="es-contentView-ruler-right"></div>' );
this.$rulerLine = $( '<div class="es-contentView-ruler-line"></div>' );
this.$.append( this.$rulers.append( this.$rulerLeft, this.$rulerRight, this.$rulerLine ) );
// Shortcuts to DOM elements
this.rulers = {
'left': this.$rulerLeft[0],
'right': this.$rulerRight[0],
'line': this.$rulerLine[0]
};
// Initialization
this.scanBoundaries();
}
};
/* Static Members */
/**
* List of annotation rendering implementations.
*
* Each supported annotation renderer must have an open and close property, each either a string or
* a function which accepts a data argument.
*
* @static
* @member
*/
ve.es.Content.annotationRenderers = {
'object/template': {
'open': function( data ) {
return '<span class="es-contentView-format-object">' + data.html;
},
'close': '</span>'
},
'object/hook': {
'open': function( data ) {
return '<span class="es-contentView-format-object">' + data.html;
},
'close': '</span>'
},
'textStyle/bold': {
'open': '<span class="es-contentView-format-textStyle-bold">',
'close': '</span>'
},
'textStyle/italic': {
'open': '<span class="es-contentView-format-textStyle-italic">',
'close': '</span>'
},
'textStyle/strong': {
'open': '<span class="es-contentView-format-textStyle-strong">',
'close': '</span>'
},
'textStyle/emphasize': {
'open': '<span class="es-contentView-format-textStyle-emphasize">',
'close': '</span>'
},
'textStyle/big': {
'open': '<span class="es-contentView-format-textStyle-big">',
'close': '</span>'
},
'textStyle/small': {
'open': '<span class="es-contentView-format-textStyle-small">',
'close': '</span>'
},
'textStyle/superScript': {
'open': '<span class="es-contentView-format-textStyle-superScript">',
'close': '</span>'
},
'textStyle/subScript': {
'open': '<span class="es-contentView-format-textStyle-subScript">',
'close': '</span>'
},
'link/external': {
'open': function( data ) {
return '<span class="es-contentView-format-link" data-href="' + data.href + '">';
},
'close': '</span>'
},
'link/internal': {
'open': function( data ) {
return '<span class="es-contentView-format-link" data-title="wiki/' + data.title + '">';
},
'close': '</span>'
}
};
/**
* Mapping of character and HTML entities or renderings.
*
* @static
* @member
*/
ve.es.Content.htmlCharacters = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'\'': '&#039;',
'"': '&quot;',
'\n': '<span class="es-contentView-whitespace">&#182;</span>',
'\t': '<span class="es-contentView-whitespace">&#8702;</span>',
' ': '&nbsp;'
};
/* Static Methods */
/**
* Gets a rendered opening or closing of an annotation.
*
* Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
* argument should be used while rendering content.
*
* @static
* @method
* @param {String} bias Which side of the annotation to render, either "open" or "close"
* @param {Object} annotation Annotation to render
* @param {Array} stack List of currently open annotations
* @returns {String} Rendered annotation
*/
ve.es.Content.renderAnnotation = function( bias, annotation, stack ) {
var renderers = ve.es.Content.annotationRenderers,
type = annotation.type,
out = '';
if ( type in renderers ) {
if ( bias === 'open' ) {
// Add annotation to the top of the stack
stack.push( annotation );
// Open annotation
out += typeof renderers[type].open === 'function' ?
renderers[type].open( annotation.data ) : renderers[type].open;
} else {
if ( stack[stack.length - 1] === annotation ) {
// Remove annotation from top of the stack
stack.pop();
// Close annotation
out += typeof renderers[type].close === 'function' ?
renderers[type].close( annotation.data ) : renderers[type].close;
} else {
// Find the annotation in the stack
var depth = ve.inArray( annotation, stack ),
i;
if ( depth === -1 ) {
throw 'Invalid stack error. An element is missing from the stack.';
}
// Close each already opened annotation
for ( i = stack.length - 1; i >= depth + 1; i-- ) {
out += typeof renderers[stack[i].type].close === 'function' ?
renderers[stack[i].type].close( stack[i].data ) :
renderers[stack[i].type].close;
}
// Close the buried annotation
out += typeof renderers[type].close === 'function' ?
renderers[type].close( annotation.data ) : renderers[type].close;
// Re-open each previously opened annotation
for ( i = depth + 1; i < stack.length; i++ ) {
out += typeof renderers[stack[i].type].open === 'function' ?
renderers[stack[i].type].open( stack[i].data ) :
renderers[stack[i].type].open;
}
// Remove the annotation from the middle of the stack
stack.splice( depth, 1 );
}
}
}
return out;
};
/* Methods */
/**
* Draws selection around a given range of content.
*
* @method
* @param {ve.Range} range Range to draw selection around
*/
ve.es.Content.prototype.drawSelection = function( range ) {
if ( typeof range === 'undefined' ) {
range = new ve.Range( 0, this.model.getContentLength() );
} else {
range.normalize();
}
var fromLineIndex = this.getRenderedLineIndexFromOffset( range.start ),
toLineIndex = this.getRenderedLineIndexFromOffset( range.end ),
fromPosition = this.getRenderedPositionFromOffset( range.start ),
toPosition = this.getRenderedPositionFromOffset( range.end );
if ( fromLineIndex === toLineIndex ) {
// Single line selection
if ( toPosition.left - fromPosition.left ) {
this.$rangeStart.css( {
'top': fromPosition.top,
'left': fromPosition.left,
'width': toPosition.left - fromPosition.left,
'height': fromPosition.bottom - fromPosition.top
} ).show();
}
this.$rangeFill.hide();
this.$rangeEnd.hide();
} else {
// Multiple line selection
var contentWidth = this.$.width();
if ( contentWidth - fromPosition.left ) {
this.$rangeStart.css( {
'top': fromPosition.top,
'left': fromPosition.left,
'width': contentWidth - fromPosition.left,
'height': fromPosition.bottom - fromPosition.top
} ).show();
} else {
this.$rangeStart.hide();
}
if ( toPosition.left ) {
this.$rangeEnd.css( {
'top': toPosition.top,
'left': 0,
'width': toPosition.left,
'height': toPosition.bottom - toPosition.top
} ).show();
} else {
this.$rangeEnd.hide();
}
if ( fromLineIndex + 1 < toLineIndex ) {
this.$rangeFill.css( {
'top': fromPosition.bottom,
'left': 0,
'width': contentWidth,
'height': toPosition.top - fromPosition.bottom
} ).show();
} else {
this.$rangeFill.hide();
}
}
};
/**
* Clears selection if any was drawn.
*
* @method
*/
ve.es.Content.prototype.clearSelection = function() {
this.$rangeStart.hide();
this.$rangeFill.hide();
this.$rangeEnd.hide();
};
/**
* Gets the index of the rendered line a given offset is within.
*
* Offsets that are out of range will always return the index of the last line.
*
* @method
* @param {Integer} offset Offset to get line for
* @returns {Integer} Index of rendered lin offset is within
*/
ve.es.Content.prototype.getRenderedLineIndexFromOffset = function( offset ) {
for ( var i = 0; i < this.lines.length; i++ ) {
if ( this.lines[i].range.containsOffset( offset ) ) {
return i;
}
}
return this.lines.length - 1;
};
/*
* Gets the index of the rendered line closest to a given position.
*
* If the position is above the first line, the offset will always be 0, and if the position is
* below the last line the offset will always be the content length. All other vertical
* positions will fall inside of one of the lines.
*
* @method
* @returns {Integer} Index of rendered line closest to position
*/
ve.es.Content.prototype.getRenderedLineIndexFromPosition = function( position ) {
var lineCount = this.lines.length;
// Positions above the first line always jump to the first offset
if ( !lineCount || position.top < 0 ) {
return 0;
}
// Find which line the position is inside of
var i = 0,
top = 0;
while ( i < lineCount ) {
top += this.lines[i].height;
if ( position.top < top ) {
break;
}
i++;
}
// Positions below the last line always jump to the last offset
if ( i === lineCount ) {
return i - 1;
}
return i;
};
/**
* Gets the range of the rendered line a given offset is within.
*
* Offsets that are out of range will always return the range of the last line.
*
* @method
* @param {Integer} offset Offset to get line for
* @returns {ve.Range} Range of line offset is within
*/
ve.es.Content.prototype.getRenderedLineRangeFromOffset = function( offset ) {
for ( var i = 0; i < this.lines.length; i++ ) {
if ( this.lines[i].range.containsOffset( offset ) ) {
return this.lines[i].range;
}
}
return this.lines[this.lines.length - 1].range;
};
/**
* Gets offset within content model closest to of a given position.
*
* Position is assumed to be local to the container the text is being flowed in.
*
* @method
* @param {Object} position Position to find offset for
* @param {Integer} position.left Horizontal position in pixels
* @param {Integer} position.top Vertical position in pixels
* @returns {Integer} Offset within content model nearest the given coordinates
*/
ve.es.Content.prototype.getOffsetFromRenderedPosition = function( position ) {
// Empty content model shortcut
if ( this.model.getContentLength() === 0 ) {
return 0;
}
// Localize position
position.subtract( ve.Position.newFromElementPagePosition( this.$ ) );
// Get the line object nearest the position
var line = this.lines[this.getRenderedLineIndexFromPosition( position )];
/*
* Offset finding
*
* Now that we know which line we are on, we can just use the "fitCharacters" method to get the
* last offset before "position.left".
*
* TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before
* the cursor.
*/
var lineRuler = this.rulers.line,
fit = this.fitCharacters( line.range, position.left ),
center;
lineRuler.innerHTML = this.getHtml( new ve.Range( line.range.start, fit.end ) );
if ( fit.end < this.model.getContentLength() ) {
var left = lineRuler.clientWidth;
lineRuler.innerHTML = this.getHtml( new ve.Range( line.range.start, fit.end + 1 ) );
center = Math.round( left + ( ( lineRuler.clientWidth - left ) / 2 ) );
} else {
center = lineRuler.clientWidth;
}
// Cleanup ruler contents
lineRuler.innerHTML = '';
// Reset RegExp object's state
this.boundaryTest.lastIndex = 0;
return Math.min(
// If the position is right of the center of the character it's on top of, increment offset
fit.end + ( position.left >= center ? 1 : 0 ),
// Don't allow the value to be higher than the end
line.range.end
);
};
/**
* Gets position coordinates of a given offset.
*
* Offsets are boundaries between plain or annotated characters within content model. Results are
* given in left, top and bottom positions, which could be used to draw a cursor, highlighting, etc.
*
* @method
* @param {Integer} offset Offset within content model
* @returns {Object} Object containing left, top and bottom properties, each positions in pixels as
* well as a line index
*/
ve.es.Content.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
/*
* Range validation
*
* Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is
* less than 0 or greater than the length of the content model.
*/
if ( offset < 0 ) {
throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
} else if ( offset > this.model.getContentLength() ) {
throw 'Out of range error. Offset is expected to be less than or equal to text length.';
}
/*
* Line finding
*
* It's possible that a more efficient method could be used here, but the number of lines to be
* iterated through will rarely be over 100, so it's unlikely that any significant gains will be
* had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom
* positions, which is a nice benefit of this method.
*/
var line,
lineCount = this.lines.length,
lineIndex = 0,
position = new ve.Position();
while ( lineIndex < lineCount ) {
line = this.lines[lineIndex];
if ( line.range.containsOffset( offset ) || ( leftBias && line.range.end === offset ) ) {
position.bottom = position.top + line.height;
break;
}
position.top += line.height;
lineIndex++;
}
/*
* Virtual n+1 position
*
* To allow access to position information of the right side of the last character on the last
* line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause
* an exception to be thrown.
*/
if ( lineIndex === lineCount ) {
position.bottom = position.top;
position.top -= line.height;
}
/*
* Offset measuring
*
* Since the left position will be zero for the first character in the line, so we can skip
* measuring for those cases.
*/
if ( line.range.start < offset ) {
var lineRuler = this.rulers.line;
lineRuler.innerHTML = this.getHtml( new ve.Range( line.range.start, offset ) );
position.left = lineRuler.clientWidth;
// Cleanup ruler contents
lineRuler.innerHTML = '';
}
return position;
};
/**
* Updates the word boundary cache, which is used for word fitting.
*
* @method
*/
ve.es.Content.prototype.scanBoundaries = function() {
/*
* Word boundary scan
*
* To perform binary-search on words, rather than characters, we need to collect word boundary
* offsets into an array. The offset of the right side of the breaking character is stored, so
* the gaps between stored offsets always include the breaking character at the end.
*
* To avoid encoding the same words as HTML over and over while fitting text to lines, we also
* build a list of HTML escaped strings for each gap between the offsets stored in the
* "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
* the words.
*/
// Get and cache a copy of all content, the make a plain-text version of the cached content
var data = this.contentCache = this.model.getContentData(),
text = '';
for ( var i = 0, length = data.length; i < length; i++ ) {
text += typeof data[i] === 'string' ? data[i] : data[i][0];
}
// Reset boundaries
this.boundaries = [0];
this.boundaryTest.lastIndex = 0;
// Reset breaks
if ( this.options.pre ) {
this.breaks = {};
}
// Iterate over each word+boundary sequence, capturing offsets in this.boundaries
var match,
end;
while ( ( match = this.boundaryTest.exec( text ) ) ) {
// Include the boundary character in the range
end = match.index + 1;
// Store the boundary offset
this.boundaries.push( end );
// Check for break at boundary and store it
if ( this.options.pre && text[match.index] === '\n' ) {
this.breaks[end] = true;
}
}
// If the last character is not a boundary character, we need to append the final range to the
// "boundaries" and "words" arrays
if ( end < text.length || this.boundaries.length === 1 ) {
this.boundaries.push( text.length );
}
};
/**
* Renders a batch of lines and then yields execution before rendering another batch.
*
* In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
* causing them to be fragmented. Word fragments are rendered on their own lines, except for their
* remainder, which is combined with whatever proceeding words can fit on the same line.
*
* @method
* @param {Integer} limit Maximum number of iterations to render before yeilding
*/
ve.es.Content.prototype.renderIteration = function( limit ) {
var rs = this.renderState,
iteration = 0,
fractional = false,
lineStart = this.boundaries[rs.wordOffset],
lineEnd,
wordFit = null,
charOffset = 0,
charFit = null,
wordCount = this.boundaries.length;
while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) {
// Get the width from the edges of left and right floated DIV elements in the container
rs.width = rs.rulers.right.offsetLeft - rs.rulers.left.offsetLeft;
// Fit words on the line
wordFit = this.fitWords( new ve.Range( rs.wordOffset, wordCount - 1 ), rs.width );
fractional = false;
if ( wordFit.width > rs.width ) {
// The first word didn't fit, we need to split it up
charOffset = lineStart;
var lineOffset = rs.wordOffset;
rs.wordOffset++;
lineEnd = this.boundaries[rs.wordOffset];
do {
charFit = this.fitCharacters( new ve.Range( charOffset, lineEnd ), rs.width );
// If we were able to get the rest of the characters on the line OK
if ( charFit.end === lineEnd) {
// Try to fit more words on the line
wordFit = this.fitWords(
new ve.Range( rs.wordOffset, wordCount - 1 ),
rs.width - charFit.width
);
if ( wordFit.end > rs.wordOffset ) {
lineOffset = rs.wordOffset;
rs.wordOffset = wordFit.end;
charFit.end = lineEnd = this.boundaries[rs.wordOffset];
}
}
this.appendLine( new ve.Range( charOffset, charFit.end ), lineOffset, fractional );
// Move on to another line
charOffset = charFit.end;
// Mark the next line as fractional
fractional = true;
} while ( charOffset < lineEnd );
} else {
lineEnd = this.boundaries[wordFit.end];
this.appendLine( new ve.Range( lineStart, lineEnd ), rs.wordOffset, fractional );
rs.wordOffset = wordFit.end;
}
lineStart = lineEnd;
}
// Only perform on actual last iteration
if ( rs.wordOffset >= wordCount - 1 ) {
// Cleanup ruler contents
rs.rulers.line.innerHTML = '';
// Cleanup line meta data
if ( rs.line < this.lines.length ) {
this.lines.splice( rs.line, this.lines.length - rs.line );
}
// Cleanup unused lines in the DOM
this.$.find( '.es-contentView-line[line-index=' + ( this.lines.length - 1 ) + ']' )
.nextAll( '.es-contentView-line' )
.remove();
rs.timeout = undefined;
this.emit( 'update' );
} else {
rs.rulers.line.innerHTML = '';
var that = this;
rs.timeout = setTimeout( function() {
that.renderIteration( 3 );
}, 0 );
}
};
/**
* Renders text into a series of HTML elements, each a single line of wrapped text.
*
* The offset parameter can be used to reduce the amount of work involved in re-rendering the same
* text, but will be automatically ignored if the text or width of the container has changed.
*
* Rendering happens asynchronously, and yields execution between iterations. Iterative rendering
* provides the JavaScript engine an ability to process events between rendering batches of lines,
* allowing rendering to be interrupted and restarted if changes to content model are happening before
* rendering of all lines is complete.
*
* @method
* @param {Integer} [offset] Offset to re-render from, if possible
*/
ve.es.Content.prototype.render = function( offset ) {
var rs = this.renderState;
// Check if rendering is currently underway
if ( rs.timeout !== undefined ) {
// Cancel the active rendering process
clearTimeout( rs.timeout );
}
// Clear caches that were specific to the previous render
this.widthCache = {};
// In case of empty content model we still want to display empty with non-breaking space inside
// This is very important for lists
if(this.model.getContentLength() === 0) {
// Remove all lines
this.$.children().remove( '.es-contentView-line' );
// Create a new line
var $line = $( '<div class="es-contentView-line" line-index="0">&nbsp;</div>' );
// Insert the new line between ranges and rulers
this.$rulers.before( $line );
// Store meta data about the line for later
this.lines = [{
'text': ' ',
'range': new ve.Range( 0,0 ),
'width': 0,
'height': $line.outerHeight(),
'wordOffset': 0,
'fractional': false
}];
this.emit( 'update' );
return;
}
/*
* Container measurement
*
* To get an accurate measurement of the inside of the container, without having to deal with
* inconsistencies between browsers and box models, we can just create elements inside the
* container and measure them. There are three rulers, a line ruler used for checking if a line
* of text fits or not, and a set of left and right boundary rulers which are floated left and
* right respectively and get the effective width of a line after the browser has done it's
* job with laying out floating content.
*/
rs.rulers = this.rulers;
// TODO: Re-implement render-from offset in a way that will take into consideration that not all
// lines are the same width. This is a very important optimization.
/*
// Ignore offset optimization if the width has changed or the text has never been flowed before
if (this.width !== rs.width) {
offset = undefined;
}
this.width = rs.width;
// Reset the render state
if ( offset ) {
var gap,
currentLine = this.lines.length - 1;
for ( var i = this.lines.length - 1; i >= 0; i-- ) {
var line = this.lines[i];
if ( line.range.start < offset && line.range.end > offset ) {
currentLine = i;
}
if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) {
rs.line = i;
rs.wordOffset = line.wordOffset;
gap = currentLine - i;
break;
}
}
this.renderIteration( 2 + gap );
} else {
*/
rs.line = 0;
rs.wordOffset = 0;
this.renderIteration( 3 );
//}
};
/**
* Adds a line containing a given range of text to the end of the DOM and the "lines" array.
*
* @method
* @param {ve.Range} range Range of data within content model to append
* @param {Integer} start Beginning of text range for line
* @param {Integer} end Ending of text range for line
* @param {Integer} wordOffset Index within this.words which the line begins with
* @param {Boolean} fractional If the line begins in the middle of a word
*/
ve.es.Content.prototype.appendLine = function( range, wordOffset, fractional ) {
var rs = this.renderState,
$line = this.$.children( '[line-index=' + rs.line + ']' );
if ( !$line.length ) {
$line = $( '<div class="es-contentView-line" line-index="' + rs.line + '"></div>' );
this.$rulers.before( $line );
}
$line[0].innerHTML = this.getHtml( range );
// Overwrite/append line information
this.lines[rs.line] = {
'text': this.model.getContentText( range ),
'range': range,
'width': $line.outerWidth(),
'height': $line.outerHeight(),
'wordOffset': wordOffset,
'fractional': fractional
};
// Disable links within rendered content
$line.find( '.es-contentView-format-object a' )
.mousedown( function( e ) {
e.preventDefault();
} )
.click( function( e ) {
e.preventDefault();
} );
rs.line++;
};
/**
* Gets the index of the boundary of last word that fits inside the line
*
* The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
* areas within the text. Using these, we can perform a binary-search for the best fit of words
* within a line, just as we would with characters.
*
* Results are given as an object containing both an index and a width, the later of which can be
* used to detect when the first word was too long to fit on a line. In such cases the result will
* contain the index of the boundary of the first word and it's width.
*
* TODO: Because limit is most likely given as "words.length", it may be possible to improve the
* efficiency of this code by making a best guess and working from there, rather than always
* starting with [offset .. limit], which usually results in reducing the end position in all but
* the last line, and in most cases more than 3 times, before changing directions.
*
* @method
* @param {ve.Range} range Range of data within content model to try to fit
* @param {Integer} width Maximum width to allow the line to extend to
* @returns {Integer} Last index within "words" that contains a word that fits
*/
ve.es.Content.prototype.fitWords = function( range, width ) {
var offset = range.start,
start = range.start,
end = range.end,
charOffset = this.boundaries[offset],
middle,
charMiddle,
lineWidth,
cacheKey;
// Look ahead for line breaks and adjust end accordingly
if ( this.options.pre ) {
for ( var i = start + 1; i < end; i++ ) {
if ( this.boundaries[i] in this.breaks ) {
end = i;
break;
}
}
}
do {
// Place "middle" directly in the center of "start" and "end"
middle = Math.ceil( ( start + end ) / 2 );
charMiddle = this.boundaries[middle];
// Measure and cache width of substring
cacheKey = charOffset + ':' + charMiddle;
// Prepare the line for measurement using pre-escaped HTML
this.rulers.line.innerHTML = this.getHtml( new ve.Range( charOffset, charMiddle ) );
// Test for over/under using width of the rendered line
this.widthCache[cacheKey] = lineWidth = this.rulers.line.clientWidth;
// Test for over/under using width of the rendered line
if ( lineWidth > width ) {
// Detect impossible fit (the first word won't fit by itself)
if (middle - offset === 1) {
start = middle;
break;
}
// Words after "middle" won't fit
end = middle - 1;
} else {
// Words before "middle" will fit
start = middle;
}
} while ( start < end );
// Check if we ended by moving end to the left of middle
if ( end === middle - 1 ) {
// A final measurement is required
var charStart = this.boundaries[start];
this.rulers.line.innerHTML = this.getHtml( new ve.Range( charOffset, charStart ) );
lineWidth = this.widthCache[charOffset + ':' + charStart] = this.rulers.line.clientWidth;
}
return { 'end': start, 'width': lineWidth };
};
/**
* Gets the index of the boundary of the last character that fits inside the line
*
* Results are given as an object containing both an index and a width, the later of which can be
* used to detect when the first character was too long to fit on a line. In such cases the result
* will contain the index of the first character and it's width.
*
* @method
* @param {ve.Range} range Range of data within content model to try to fit
* @param {Integer} width Maximum width to allow the line to extend to
* @returns {Integer} Last index within "text" that contains a character that fits
*/
ve.es.Content.prototype.fitCharacters = function( range, width ) {
var offset = range.start,
start = range.start,
end = range.end,
middle,
lineWidth,
cacheKey;
do {
// Place "middle" directly in the center of "start" and "end"
middle = Math.ceil( ( start + end ) / 2 );
// Measure and cache width of substring
cacheKey = offset + ':' + middle;
if ( cacheKey in this.widthCache ) {
lineWidth = this.widthCache[cacheKey];
} else {
// Fill the line with a portion of the text, escaped as HTML
this.rulers.line.innerHTML = this.getHtml( new ve.Range( offset, middle ) );
// Test for over/under using width of the rendered line
this.widthCache[cacheKey] = lineWidth = this.rulers.line.clientWidth;
}
if ( lineWidth > width ) {
// Detect impossible fit (the first character won't fit by itself)
if (middle - offset === 1) {
start = middle - 1;
break;
}
// Words after "middle" won't fit
end = middle - 1;
} else {
// Words before "middle" will fit
start = middle;
}
} while ( start < end );
// Check if we ended by moving end to the left of middle
if ( end === middle - 1 ) {
// Try for cache hit
cacheKey = offset + ':' + start;
if ( cacheKey in this.widthCache ) {
lineWidth = this.widthCache[cacheKey];
} else {
// A final measurement is required
this.rulers.line.innerHTML = this.getHtml( new ve.Range( offset, start ) );
lineWidth = this.widthCache[cacheKey] = this.rulers.line.clientWidth;
}
}
return { 'end': start, 'width': lineWidth };
};
/**
* Gets an HTML rendering of a range of data within content model.
*
* @method
* @param {ve.Range} range Range of content to render
* @param {String} Rendered HTML of data within content model
*/
ve.es.Content.prototype.getHtml = function( range, options ) {
if ( range ) {
range.normalize();
} else {
range = { 'start': 0, 'end': undefined };
}
var data = this.contentCache.slice( range.start, range.end ),
render = ve.es.Content.renderAnnotation,
htmlChars = ve.es.Content.htmlCharacters;
var out = '',
left = '',
right,
leftPlain,
rightPlain,
stack = [],
chr,
i,
j;
for ( i = 0; i < data.length; i++ ) {
right = data[i];
leftPlain = typeof left === 'string';
rightPlain = typeof right === 'string';
if ( !leftPlain && rightPlain ) {
// [formatted][plain] pair, close any annotations for left
for ( j = 1; j < left.length; j++ ) {
out += render( 'close', left[j], stack );
}
} else if ( leftPlain && !rightPlain ) {
// [plain][formatted] pair, open any annotations for right
for ( j = 1; j < right.length; j++ ) {
out += render( 'open', right[j], stack );
}
} else if ( !leftPlain && !rightPlain ) {
// [formatted][formatted] pair, open/close any differences
for ( j = 1; j < left.length; j++ ) {
if ( ve.inArray( left[j], right ) === -1 ) {
out += render( 'close', left[j], stack );
}
}
for ( j = 1; j < right.length; j++ ) {
if ( ve.inArray( right[j], left ) === -1 ) {
out += render( 'open', right[j], stack );
}
}
}
chr = rightPlain ? right : right[0];
out += chr in htmlChars ? htmlChars[chr] : chr;
left = right;
}
// Close all remaining tags at the end of the content
if ( !rightPlain && right ) {
for ( j = 1; j < right.length; j++ ) {
out += render( 'close', right[j], stack );
}
}
return out;
};
/* Inheritance */
ve.extendClass( ve.es.Content, ve.EventEmitter );

View file

@ -1,89 +0,0 @@
/**
* Creates an ve.es.LeafNode object.
*
* @class
* @abstract
* @constructor
* @extends {ve.LeafNode}
* @extends {ve.es.Node}
* @param model {ve.ModelNode} Model to observe
* @param {jQuery} [$element] Element to use as a container
*/
ve.es.LeafNode = function( model, $element, options ) {
// Inheritance
ve.LeafNode.call( this );
ve.es.Node.call( this, model, $element );
// Properties
this.$content = $( '<div class="es-contentView"></div>' ).appendTo( this.$ );
this.contentView = new ve.es.Content( this.$content, model, options );
// Events
this.contentView.on( 'update', this.emitUpdate );
};
/* Methods */
/**
* Render content.
*
* @method
*/
ve.es.LeafNode.prototype.renderContent = function() {
this.contentView.render();
};
/**
* Draw selection around a given range.
*
* @method
* @param {ve.Range} range Range of content to draw selection around
*/
ve.es.LeafNode.prototype.drawSelection = function( range ) {
this.contentView.drawSelection( range );
};
/**
* Clear selection.
*
* @method
*/
ve.es.LeafNode.prototype.clearSelection = function() {
this.contentView.clearSelection();
};
/**
* Gets the nearest offset of a rendered position.
*
* @method
* @param {ve.Position} position Position to get offset for
* @returns {Integer} Offset of position
*/
ve.es.LeafNode.prototype.getOffsetFromRenderedPosition = function( position ) {
return this.contentView.getOffsetFromRenderedPosition( position );
};
/**
* Gets rendered position of offset within content.
*
* @method
* @param {Integer} offset Offset to get position for
* @returns {ve.Position} Position of offset
*/
ve.es.LeafNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
var position = this.contentView.getRenderedPositionFromOffset( offset, leftBias ),
contentPosition = this.$content.offset();
position.top += contentPosition.top;
position.left += contentPosition.left;
position.bottom += contentPosition.top;
return position;
};
ve.es.LeafNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
return this.contentView.getRenderedLineRangeFromOffset( offset );
};
/* Inheritance */
ve.extendClass( ve.es.LeafNode, ve.LeafNode );
ve.extendClass( ve.es.LeafNode, ve.es.Node );

View file

@ -1,107 +0,0 @@
/**
* Creates an ve.es.Node object.
*
* @class
* @abstract
* @constructor
* @extends {ve.Node}
* @param {ve.dm.Node} model Model to observe
* @param {jQuery} [$element=$( '<div></div>' )] Element to use as a container
*/
ve.es.Node = function( model, $element ) {
// Inheritance
ve.Node.call( this );
// Properties
this.model = model;
this.parent = null;
this.$ = $element || $( '<div/>' );
};
/* Methods */
/**
* Gets the length of the element in the model.
*
* @method
* @see {ve.Node.prototype.getElementLength}
* @returns {Integer} Length of content
*/
ve.es.Node.prototype.getElementLength = function() {
return this.model.getElementLength();
};
/**
* Gets the length of the content in the model.
*
* @method
* @see {ve.Node.prototype.getContentLength}
* @returns {Integer} Length of content
*/
ve.es.Node.prototype.getContentLength = function() {
return this.model.getContentLength();
};
/**
* Attaches node as a child to another node.
*
* @method
* @param {ve.es.Node} parent Node to attach to
* @emits attach (parent)
*/
ve.es.Node.prototype.attach = function( parent ) {
this.parent = parent;
this.emit( 'attach', parent );
};
/**
* Detaches node from it's parent.
*
* @method
* @emits detach (parent)
*/
ve.es.Node.prototype.detach = function() {
var parent = this.parent;
this.parent = null;
this.emit( 'detach', parent );
};
/**
* Gets a reference to this node's parent.
*
* @method
* @returns {ve.es.Node} Reference to this node's parent
*/
ve.es.Node.prototype.getParent = function() {
return this.parent;
};
/**
* Gets a reference to the model this node observes.
*
* @method
* @returns {ve.dm.Node} Reference to the model this node observes
*/
ve.es.Node.prototype.getModel = function() {
return this.model;
};
ve.es.Node.getSplitableNode = function( node ) {
var splitableNode = null;
ve.Node.traverseUpstream( node, function( node ) {
var elementType = node.model.getElementType();
if (
splitableNode !== null &&
ve.es.DocumentNode.splitRules[ elementType ].children === true
) {
return false;
}
splitableNode = ve.es.DocumentNode.splitRules[ elementType ].self ? node : null;
} );
return splitableNode;
};
/* Inheritance */
ve.extendClass( ve.es.Node, ve.Node );

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
/**
* VisualEditor EditSurface namespace.
*
* All classes and functions will be attached to this object to keep the global namespace clean.
*/
ve.es = {
};

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,6 +1,6 @@
/**
* Creates an ve.ui.LinkInspector object.
*
*
* @class
* @constructor
* @param {ve.ui.Toolbar} toolbar
@ -11,8 +11,13 @@ ve.ui.LinkInspector = function( toolbar, context ) {
// Properties
this.$clearButton = $( '<div class="es-inspector-button es-inspector-clearButton"></div>' )
.prependTo( this.$ );
this.$.prepend( '<div class="es-inspector-title">Edit link</div>' );
this.$locationLabel = $( '<label>Page title</label>' ).appendTo( this.$form );
this.$.prepend(
$( '<div class="es-inspector-title"></div>' )
.text( ve.msg( 'visualeditor-linkinspector-title' ) )
);
this.$locationLabel = $( '<label></label>' )
.text( ve.msg( 'visualeditor-linkinspector-label-pagetitle' ) )
.appendTo( this.$form );
this.$locationInput = $( '<input type="text">' ).appendTo( this.$form );
this.initialValue = null;
@ -23,8 +28,12 @@ ve.ui.LinkInspector = function( toolbar, context ) {
return;
}
var surfaceView = _this.context.getSurfaceView();
surfaceView.annotate( 'clear', /link\/.*/ );
var surfaceModel = _this.context.getSurfaceView().getModel(),
annotations = _this.getSelectedLinkAnnotations();
// If link annotation exists, clear it.
for ( var hash in annotations ) {
surfaceModel.annotate( 'clear', annotations[hash] );
}
_this.$locationInput.val( '' );
_this.context.closeInspector();
@ -42,32 +51,46 @@ ve.ui.LinkInspector = function( toolbar, context ) {
/* Methods */
ve.ui.LinkInspector.prototype.getTitleFromSelection = function() {
ve.ui.LinkInspector.prototype.getSelectedLinkAnnotations = function(){
var surfaceView = this.context.getSurfaceView(),
surfaceModel = surfaceView.getModel(),
documentModel = surfaceModel.getDocument(),
data = documentModel.getData( surfaceView.getSelectionRange() );
data = documentModel.getData( surfaceModel.getSelection() );
if ( data.length ) {
var annotation = ve.dm.DocumentNode.getMatchingAnnotations( data[0], /link\/.*/ );
if ( annotation.length ) {
annotation = annotation[0];
if ( ve.isPlainObject( data[0][1] ) ) {
return ve.dm.Document.getMatchingAnnotations( data[0][1], /link\/.*/ );
}
if ( annotation && annotation.data && annotation.data.title ) {
return annotation.data.title;
}
return {};
};
ve.ui.LinkInspector.prototype.getAnnotationFromSelection = function() {
var annotations = this.getSelectedLinkAnnotations();
for ( var hash in annotations ) {
// Use the first one with a recognized type (there should only be one, but this is just in case)
if ( annotations[hash].type === 'link/wikiLink' || annotations[hash].type === 'link/extLink' ) {
return annotations[hash];
}
}
return null;
};
ve.ui.LinkInspector.prototype.onOpen = function() {
var title = this.getTitleFromSelection();
if ( title !== null ) {
this.$locationInput.val( title );
this.$clearButton.removeClass( 'es-inspector-button-disabled' );
} else {
var annotation = this.getAnnotationFromSelection();
if ( annotation === null ) {
this.$locationInput.val( '' );
this.$clearButton.addClass( 'es-inspector-button-disabled' );
} else if ( annotation.type === 'link/wikiLink' ) {
// Internal link
this.$locationInput.val( annotation.data.title || '' );
this.$clearButton.removeClass( 'es-inspector-button-disabled' );
} else {
// External link
this.$locationInput.val( annotation.data.href || '' );
this.$clearButton.removeClass( 'es-inspector-button-disabled' );
}
this.$acceptButton.addClass( 'es-inspector-button-disabled' );
this.initialValue = this.$locationInput.val();
var _this = this;
@ -78,13 +101,35 @@ ve.ui.LinkInspector.prototype.onOpen = function() {
ve.ui.LinkInspector.prototype.onClose = function( accept ) {
if ( accept ) {
var title = this.$locationInput.val();
if ( title === this.getTitleFromSelection() || !title ) {
var target = this.$locationInput.val();
if ( target === this.initialValue || !target ) {
return;
}
var surfaceView = this.context.getSurfaceView();
surfaceView.annotate( 'clear', /link\/.*/ );
surfaceView.annotate( 'set', { 'type': 'link/internal', 'data': { 'title': title } } );
var surfaceModel = this.context.getSurfaceView().getModel(),
annotations = this.getSelectedLinkAnnotations();
// Clear link annotation if it exists
for ( var hash in annotations ) {
surfaceModel.annotate( 'clear', annotations[hash] );
}
var annotation;
// Figure out if this is an internal or external link
// TODO better logic
if ( target.match( /^https?:\/\// ) ) {
// External link
annotation = {
'type': 'link/extLink',
'data': { 'href': target }
};
} else {
// Internal link
annotation = {
'type': 'link/wikiLink',
'data': { 'title': target }
};
}
surfaceModel.annotate( 'set', annotation );
}
};

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -47,7 +47,7 @@
}
.es-contextView-position-above .es-menuView {
bottom: -4px;
bottom: 2px;
}
.es-contextView-position-below .es-menuView {
@ -55,14 +55,14 @@
}
.es-contextView-inspectors {
position: absolute;
position: absolute;
}
.es-contextView-position-above .es-inspector {
bottom: -4px;
.es-contextView-position-above .es-contextView-inspectors {
bottom: -8px;
}
.es-contextView-position-below .es-inspector {
.es-contextView-position-below .es-contextView-inspectors {
top: 2px;
}

View file

@ -1,5 +1,5 @@
.es-inspector {
display: none;
font-family: sans-serif;
position: absolute;
border: solid 1px #cccccc;
-webkit-border-radius: 0.25em;

Some files were not shown because too many files have changed in this diff Show more