Move ve2/ back to ve/
Change-Id: Ie51d8e48171fb1f84045d1560ee603cee62b91f6
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"></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 );
|
||||
|
|
|
@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'\'': ''',
|
||||
'"': '"',
|
||||
'\n': '<span class="ve-ce-content-whitespace">¶</span>',
|
||||
'\t': '<span class="ve-ce-content-whitespace">⇾</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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 */
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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 );
|
||||
|
|
|
@ -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 = [];
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ) );
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 );
|
Before Width: | Height: | Size: 111 B |
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 );
|
|
@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'\'': ''',
|
||||
'"': '"',
|
||||
'\n': '<span class="es-contentView-whitespace">¶</span>',
|
||||
'\t': '<span class="es-contentView-whitespace">⇾</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.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"> </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 );
|
|
@ -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 );
|
|
@ -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 );
|
|
@ -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 = {
|
||||
|
||||
};
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
@ -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 );
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.es-inspector {
|
||||
display: none;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
border: solid 1px #cccccc;
|
||||
-webkit-border-radius: 0.25em;
|
||||
|
|