mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 08:34:54 +00:00
4e64187beb
* Document them consistently between secetions Inheritance and Static Properties under their own (new) section Events. * Removed any quotes or brackets around the event name in existing @emit annotations Search: @emit.*['"{}(){}] * For every call to this.emit() anywhere, added @emits. * Fixed all warnings for references to undefined events (introduced as a result of the previous point). Event handler parameter documented based on the emit() call and actual handlers using the event. Usually the latter is more elaborate. * Extend coverage of jQuery as needed (copied from mwcore/maintenance/jsduck/external.js written by me, hereby implicitly and explicitly released under MIT). Specifics * ve.ce.Surface#onContentChange: Fixed type of range from Object to ve.Range. * ve.ce.SurfaceObserver#poll: Fix syntax for code from {} to `backticks`. * ve.ui.Toolbar#onContextChange: Doesn't actually emit "clearState" event. Removed #onClearState in Tool, ButtonTool and DropdownTool. Bug: 45872 Change-Id: Id879aa769b2c72d86a0322e75dddeb868211ce28
855 lines
25 KiB
JavaScript
855 lines
25 KiB
JavaScript
/*!
|
|
* VisualEditor DataModel Fragment class.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* DataModel surface fragment.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {ve.dm.Surface} surface Target surface
|
|
* @param {ve.Range} [range] Range within target document, current selection used by default
|
|
* @param {boolean} [noAutoSelect] Update the surface's selection when making changes
|
|
*/
|
|
ve.dm.SurfaceFragment = function VeDmSurfaceFragment( surface, range, noAutoSelect ) {
|
|
// Short-circuit for missing-surface null fragment
|
|
if ( !surface ) {
|
|
return this;
|
|
}
|
|
|
|
// Properties
|
|
this.range = range && range instanceof ve.Range ? range : surface.getSelection();
|
|
// Short-circuit for invalid range null fragment
|
|
if ( !this.range ) {
|
|
return this;
|
|
}
|
|
this.surface = surface;
|
|
this.document = surface.getDocument();
|
|
this.noAutoSelect = !!noAutoSelect;
|
|
|
|
// Events
|
|
surface.on( 'transact', ve.bind( this.onTransact, this ) );
|
|
|
|
// Initialization
|
|
var length = this.document.getLength();
|
|
this.range = new ve.Range(
|
|
// Clamp range to valid document offsets
|
|
Math.min( Math.max( this.range.from, 0 ), length ),
|
|
Math.min( Math.max( this.range.to, 0 ), length )
|
|
);
|
|
};
|
|
|
|
/* Static Properties */
|
|
|
|
/**
|
|
* @static
|
|
* @property
|
|
* @inheritable
|
|
*/
|
|
ve.dm.SurfaceFragment.static = {};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Handle transactions being processed on the document.
|
|
*
|
|
* This keeps the range of the fragment valid, even while other transactions are being processed
|
|
*
|
|
* @method
|
|
* @param {ve.dm.Transaction[]} txs Transactions that have just been processed
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.onTransact = function ( txs ) {
|
|
for ( var i = 0; i < txs.length; i++ ) {
|
|
this.range = txs[i].translateRange( this.range );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the surface the fragment is a part of.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.Surface} Surface of fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getSurface = function () {
|
|
return this.surface;
|
|
};
|
|
|
|
/**
|
|
* Get the document of the surface the fragment is a part of.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.Document} Document of surface of fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getDocument = function () {
|
|
return this.document;
|
|
};
|
|
|
|
/**
|
|
* Get the range of the fragment within the surface.
|
|
*
|
|
* @method
|
|
* @returns {ve.Range} Surface range
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getRange = function () {
|
|
return this.range.clone();
|
|
};
|
|
|
|
/**
|
|
* Check if the fragment is null.
|
|
*
|
|
* @method
|
|
* @returns {boolean} Fragment is a null fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.isNull = function () {
|
|
return this.surface === undefined;
|
|
};
|
|
|
|
/**
|
|
* Get a new fragment with an adjusted position
|
|
*
|
|
* @method
|
|
* @param {number} [start] Adjustment for start position
|
|
* @param {number} [end] Adjustment for end position
|
|
* @returns {ve.dm.SurfaceFragment} Adjusted fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.adjustRange = function ( start, end ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
return new ve.dm.SurfaceFragment(
|
|
this.surface,
|
|
new ve.Range( this.range.start + ( start || 0 ), this.range.end + ( end || 0 ) ),
|
|
this.noAutoSelect
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get a new fragment with a truncated length.
|
|
*
|
|
* @method
|
|
* @param {number} limit Maximum length of range (negative for left-side truncation)
|
|
* @returns {ve.dm.SurfaceFragment} Truncated fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.truncateRange = function ( limit ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
return new ve.dm.SurfaceFragment(
|
|
this.surface,
|
|
this.range.truncate( limit ),
|
|
this.noAutoSelect
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get a new fragment with a zero-length selection at the start offset.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.SurfaceFragment} Collapsed fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.collapseRange = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
return new ve.dm.SurfaceFragment(
|
|
this.surface, new ve.Range( this.range.start ), this.noAutoSelect
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get a new fragment with a range that no longer includes leading and trailing whitespace.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.SurfaceFragment} Trimmed fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.trimRange = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
// If range is only whitespace
|
|
if ( this.document.getText( this.range ).trim().length === 0 ) {
|
|
// Collapse range
|
|
return new ve.dm.SurfaceFragment(
|
|
this.surface, new ve.Range( this.range.start ), this.noAutoSelect
|
|
);
|
|
}
|
|
return new ve.dm.SurfaceFragment(
|
|
this.surface, this.document.trimOuterSpaceFromRange( this.range ), this.noAutoSelect
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get a new fragment that covers an expanded range of the document.
|
|
*
|
|
* @method
|
|
* @param {string} [scope='parent'] Method of expansion:
|
|
* - `word`: Expands to cover the nearest word by looking for word boundary characters
|
|
* - `annotation`: Expands to cover a given annotation (argument) within the current range
|
|
* - `root`: Expands to cover the entire document
|
|
* - `siblings`: Expands to cover all sibling nodes
|
|
* - `closest`: Expands to cover the closest common ancestor node of a give type (argument)
|
|
* - `parent`: Expands to cover the closest common parent node
|
|
* @param {Mixed} [type] Parameter to use with scope method if needed
|
|
* @returns {ve.dm.SurfaceFragment} Expanded fragment
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.expandRange = function ( scope, type ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var range, node, nodes, parent;
|
|
switch ( scope || 'parent' ) {
|
|
case 'word':
|
|
range = new ve.Range(
|
|
this.document.getNearestWordBoundary( this.range.start, -1 ),
|
|
this.document.getNearestWordBoundary( this.range.end, 1 )
|
|
);
|
|
break;
|
|
case 'annotation':
|
|
range = this.document.getAnnotatedRangeFromSelection( this.range, type );
|
|
// Adjust selection if it does not contain the annotated range
|
|
if ( this.range.start > range.start || this.range.end < range.end ) {
|
|
// Maintain range direction
|
|
if ( this.range.from > this.range.to ) {
|
|
range = range.flip();
|
|
}
|
|
} else {
|
|
// Otherwise just keep the range as is
|
|
range = this.range.clone();
|
|
}
|
|
break;
|
|
case 'root':
|
|
range = new ve.Range( 0, this.document.getData().length );
|
|
break;
|
|
case 'siblings':
|
|
// Grow range to cover all siblings
|
|
nodes = this.document.selectNodes( this.range, 'siblings' );
|
|
if ( nodes.length === 1 ) {
|
|
range = nodes[0].node.getOuterRange();
|
|
} else {
|
|
range = new ve.Range(
|
|
nodes[0].node.getOuterRange().start,
|
|
nodes[nodes.length - 1].node.getOuterRange().end
|
|
);
|
|
}
|
|
break;
|
|
case 'closest':
|
|
// Grow range to cover closest common ancestor node of given type
|
|
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
|
parent = node.getParent();
|
|
while ( parent && parent.getType() !== type ) {
|
|
node = parent;
|
|
parent = parent.getParent();
|
|
}
|
|
if ( !parent ) {
|
|
return new ve.dm.SurfaceFragment( null );
|
|
}
|
|
range = parent.getOuterRange();
|
|
break;
|
|
case 'parent':
|
|
// Grow range to cover the closest common parent node
|
|
node = this.document.selectNodes( this.range, 'siblings' )[0].node;
|
|
parent = node.getParent();
|
|
if ( !parent ) {
|
|
return new ve.dm.SurfaceFragment( null );
|
|
}
|
|
range = parent.getOuterRange();
|
|
break;
|
|
default:
|
|
throw new Error( 'Invalid scope argument: ' + scope );
|
|
}
|
|
return new ve.dm.SurfaceFragment( this.surface, range, this.noAutoSelect );
|
|
};
|
|
|
|
/**
|
|
* Check if the surface's selection will be updated automatically when changes are made.
|
|
*
|
|
* @method
|
|
* @returns {boolean} Will automatically update surface selection
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.willAutoSelect = function () {
|
|
return !this.noAutoSelect;
|
|
};
|
|
|
|
/**
|
|
* Get data for the fragment.
|
|
*
|
|
* @method
|
|
* @param {boolean} [deep] Get a deep copy of the data
|
|
* @returns {Array} Fragment data
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getData = function ( deep ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return [];
|
|
}
|
|
return this.document.getData( this.range, deep );
|
|
};
|
|
|
|
/**
|
|
* Get plain text for the fragment.
|
|
*
|
|
* @method
|
|
* @returns {Array} Fragment text
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getText = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return '';
|
|
}
|
|
var i, length,
|
|
text = '',
|
|
data = this.document.getData( this.range );
|
|
for ( i = 0, length = data.length; i < length; i++ ) {
|
|
if ( data[i].type === undefined ) {
|
|
// Annotated characters have a string at index 0, plain characters are 1-char strings
|
|
text += typeof data[i] === 'string' ? data[i] : data[i][0];
|
|
}
|
|
}
|
|
return text;
|
|
};
|
|
|
|
/**
|
|
* Get annotations in fragment.
|
|
*
|
|
* By default, this will only get annotations that completely cover the fragment. Use the {all}
|
|
* argument to get all annotations that occur within the fragment.
|
|
*
|
|
* @method
|
|
* @param {boolean} [all] Get annotations cover some of the fragment
|
|
* @returns {ve.AnnotationSet} All annotation objects range is covered by
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getAnnotations = function ( all ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return new ve.AnnotationSet();
|
|
}
|
|
if ( this.range.getLength() ) {
|
|
return this.document.getAnnotationsFromRange( this.range, all );
|
|
} else {
|
|
return this.surface.getInsertionAnnotations();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get all leaf nodes covered by the fragment.
|
|
*
|
|
* @see ve.Document#selectNodes Used to get the return value
|
|
*
|
|
* @method
|
|
* @returns {Array} List of nodes and related information
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getLeafNodes = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return [];
|
|
}
|
|
return this.document.selectNodes( this.range, 'leaves' );
|
|
};
|
|
|
|
/**
|
|
* Get nodes covered by the fragment.
|
|
*
|
|
* Does not descend into nodes that are entirely covered by the range. The result is
|
|
* similar to that of {ve.dm.SurfaceFragment.prototype.getLeafNodes} except that if a node is
|
|
* entirely covered, its children aren't returned separately.
|
|
*
|
|
* @see ve.Document#selectNodes for more information about the return value
|
|
*
|
|
* @method
|
|
* @returns {Array} List of nodes and related information
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getCoveredNodes = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return [];
|
|
}
|
|
return this.document.selectNodes( this.range, 'coveredNodes' );
|
|
};
|
|
|
|
/**
|
|
* Get nodes covered by the fragment.
|
|
*
|
|
* Includes adjacent siblings covered by the range, descending if the range is in a single node.
|
|
*
|
|
* @see ve.Document#selectNodes for more information about the return value.
|
|
*
|
|
* @method
|
|
* @returns {Array} List of nodes and related information
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.getSiblingNodes = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return [];
|
|
}
|
|
return this.document.selectNodes( this.range, 'siblings' );
|
|
};
|
|
|
|
/**
|
|
* Change whether to automatically update the surface selection when making changes.
|
|
*
|
|
* @method
|
|
* @param {boolean} [value=true] Automatically update surface selection
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.setAutoSelect = function ( value ) {
|
|
this.noAutoSelect = !value;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Apply the fragment's range to the surface as a selection.
|
|
*
|
|
* @method
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.select = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
this.surface.change( null, this.range );
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Apply an annotation to content in the fragment.
|
|
*
|
|
* To avoid problems identified in bug 33108, use the {ve.dm.SurfaceFragment.trimRange} method.
|
|
*
|
|
* TODO: Optionally take an annotation set instead of name and data arguments and set/clear multiple
|
|
* annotations in a single transaction.
|
|
*
|
|
* @method
|
|
* @param {string} method Mode of annotation, either 'set' or 'clear'
|
|
* @param {string|ve.dm.Annotation} name Annotation name, for example: 'textStyle/bold' or
|
|
* Annotation object
|
|
* @param {Object} [data] Additional annotation data (not used if annotation object is given)
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.annotateContent = function ( method, name, data ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
// Extract annotation information
|
|
if ( name instanceof ve.dm.Annotation ) {
|
|
data = name.data;
|
|
name = name.name;
|
|
}
|
|
var tx,
|
|
annotation = ve.dm.annotationFactory.create( name, data );
|
|
if ( this.range.getLength() ) {
|
|
// Apply to selection
|
|
tx = ve.dm.Transaction.newFromAnnotation( this.document, this.range, method, annotation );
|
|
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
|
} else {
|
|
// Apply annotation to stack
|
|
if ( method === 'set' ) {
|
|
this.surface.addInsertionAnnotation( annotation );
|
|
} else if ( method === 'clear' ) {
|
|
this.surface.removeInsertionAnnotation( annotation );
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove content in the fragment and insert content before it.
|
|
*
|
|
* This will move the fragment's range to the end of the insertion and make it zero-length.
|
|
*
|
|
* @method
|
|
* @param {string|Array} content Content to insert, can be either a string or array of data
|
|
* @param {boolean} annotate Content should be automatically annotated to match surrounding content
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.insertContent = function ( content, annotate ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var tx, annotations;
|
|
if ( this.range.getLength() ) {
|
|
this.removeContent();
|
|
}
|
|
// Auto-convert content to array of plain text characters
|
|
if ( typeof content === 'string' ) {
|
|
content = content.split( '' );
|
|
}
|
|
if ( content.length ) {
|
|
if ( annotate ) {
|
|
annotations = this.document.getAnnotationsFromOffset( this.range.start - 1 );
|
|
if ( annotations.getLength() > 0 ) {
|
|
ve.dm.Document.addAnnotationsToData( content, annotations );
|
|
}
|
|
}
|
|
tx = ve.dm.Transaction.newFromInsertion( this.document, this.range.start, content );
|
|
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove content in the fragment.
|
|
*
|
|
* @method
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.removeContent = function () {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var tx;
|
|
if ( this.range.getLength() ) {
|
|
tx = ve.dm.Transaction.newFromRemoval( this.document, this.range );
|
|
// this.range will be translated via the onTransact event handler
|
|
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
|
// Check if the range didn't get collapsed automatically - this will occur when removing
|
|
// content across un-mergable nodes because the delete only strips out content leaving
|
|
// structure at the beginning and end of the range in place
|
|
if ( this.range.getLength() ) {
|
|
// Collapse the range manually
|
|
this.range = new ve.Range( this.range.start );
|
|
if ( !this.noAutoSelect ) {
|
|
// Update the surface selection
|
|
this.surface.change( null, this.range );
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Convert each content branch in the fragment from one type to another.
|
|
*
|
|
* @method
|
|
* @param {string} type Element type to convert to
|
|
* @param {Object} [attr] Initial attributes for new element
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.convertNodes = function ( type, attr ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var tx =
|
|
ve.dm.Transaction.newFromContentBranchConversion( this.document, this.range, type, attr );
|
|
this.surface.change( tx, !this.noAutoSelect && tx.translateRange( this.range ) );
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Wrap each node in the fragment with one or more elements.
|
|
*
|
|
* A wrapper object is a linear model element; a plain object containing a type property and an
|
|
* optional attributes property.
|
|
*
|
|
* Example:
|
|
* // fragment is a selection of: <p>a</p><p>b</p>
|
|
* fragment.wrapNodes(
|
|
* [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type': 'listItem' }]
|
|
* )
|
|
* // fragment is now a selection of: <ul><li><p>a</p></li></ul><ul><li><p>b</p></li></ul>
|
|
*
|
|
* @method
|
|
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper objects (see above)
|
|
* @param {string} wrapper.type Node type of wrapper
|
|
* @param {Object} [wrapper.attributes] Attributes of wrapper
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.wrapNodes = function ( wrapper ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
if ( !ve.isArray( wrapper ) ) {
|
|
wrapper = [wrapper];
|
|
}
|
|
var tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], [], [], wrapper ),
|
|
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
|
this.surface.change( tx, !this.noAutoSelect && newRange );
|
|
this.range = newRange;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Unwrap nodes in the fragment out of one or more elements.
|
|
*
|
|
* Example:
|
|
* // fragment is a selection of: <ul>「<li><p>a</p></li><li><p>b</p></li>」</ul>
|
|
* fragment.unwrapNodes( 1, 1 )
|
|
* // fragment is now a selection of: 「<p>a</p><p>b</p>」
|
|
*
|
|
* @method
|
|
* @param {number} outerDepth Number of nodes outside the selection to unwrap
|
|
* @param {number} innerDepth Number of nodes inside the selection to unwrap
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.unwrapNodes = function ( outerDepth, innerDepth ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var i, tx, newRange, innerUnwrapper = [], outerUnwrapper = [];
|
|
|
|
if ( this.range.end - this.range.start < innerDepth * 2 ) {
|
|
throw new Error( 'cannot unwrap by greater depth than maximum theoretical depth of selection' );
|
|
}
|
|
|
|
for ( i = 0; i < innerDepth; i++ ) {
|
|
innerUnwrapper.push( this.surface.getDocument().data[this.range.start + i] );
|
|
}
|
|
for ( i = outerDepth; i > 0; i-- ) {
|
|
outerUnwrapper.push( this.surface.getDocument().data[this.range.start - i] );
|
|
}
|
|
|
|
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, outerUnwrapper, [], innerUnwrapper, [] );
|
|
newRange = new ve.Range( this.range.start - outerDepth, this.range.end + outerDepth + tx.getLengthDifference() );
|
|
this.surface.change( tx, !this.noAutoSelect && newRange );
|
|
|
|
this.range = newRange;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Change the wrapping of each node in the fragment from one type to another.
|
|
*
|
|
* A wrapper object is a linear model element; a plain object containing a type property and an
|
|
* optional attributes property.
|
|
*
|
|
* Example:
|
|
* // fragment is a selection of: <dl><dt><p>a</p></dt></dl><dl><dt><p>b</p></dt></dl>
|
|
* fragment.rewrapNodes(
|
|
* 2,
|
|
* [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type': 'listItem' }]
|
|
* )
|
|
* // fragment is now a selection of: <ul><li><p>a</p></li></ul><ul><li><p>b</p></li></ul>
|
|
*
|
|
* @method
|
|
* @param {number} depth Number of nodes to unwrap
|
|
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper objects (see above)
|
|
* @param {string} wrapper.type Node type of wrapper
|
|
* @param {Object} [wrapper.attributes] Attributes of wrapper
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.rewrapNodes = function ( depth, wrapper ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var i, tx, newRange, unwrapper = [];
|
|
|
|
if ( !ve.isArray( wrapper ) ) {
|
|
wrapper = [wrapper];
|
|
}
|
|
|
|
if ( this.range.end - this.range.start < depth * 2 ) {
|
|
throw new Error( 'cannot unwrap by greater depth than maximum theoretical depth of selection' );
|
|
}
|
|
|
|
for ( i = 0; i < depth; i++ ) {
|
|
unwrapper.push( this.surface.getDocument().data[this.range.start + i] );
|
|
}
|
|
|
|
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], [], unwrapper, wrapper );
|
|
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
|
this.surface.change( tx, !this.noAutoSelect && newRange );
|
|
|
|
this.range = newRange;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Wrap nodes in the fragment with one or more elements.
|
|
*
|
|
* A wrapper object is a linear model element; a plain object containing a type property and an
|
|
* optional attributes property.
|
|
*
|
|
* Example:
|
|
* // fragment is a selection of: <p>a</p><p>b</p>
|
|
* fragment.wrapAllNodes(
|
|
* [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type': 'listItem' }]
|
|
* )
|
|
* // fragment is now a selection of: <ul><li><p>a</p><p>b</p></li></ul>
|
|
*
|
|
* @method
|
|
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper objects (see above)
|
|
* @param {string} wrapper.type Node type of wrapper
|
|
* @param {Object} [wrapper.attributes] Attributes of wrapper
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.wrapAllNodes = function ( wrapper ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
|
|
var tx, newRange;
|
|
|
|
if ( !ve.isArray( wrapper ) ) {
|
|
wrapper = [wrapper];
|
|
}
|
|
|
|
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], wrapper, [], [] );
|
|
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
|
this.surface.change( tx, !this.noAutoSelect && newRange );
|
|
|
|
this.range = newRange;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Change the wrapping of nodes in the fragment from one type to another.
|
|
*
|
|
* A wrapper object is a linear model element; a plain object containing a type property and an
|
|
* optional attributes property.
|
|
*
|
|
* Example:
|
|
* // fragment is a selection of: <h1><p>a</p><p>b</p></h1>
|
|
* fragment.rewrapAllNodes( 1, { 'type': 'heading', 'attributes' : { 'level' : 2 } } );
|
|
* // fragment is now a selection of: <h2><p>a</p><p>b</p></h2>
|
|
*
|
|
* @method
|
|
* @param {number} depth Number of nodes to unwrap
|
|
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper objects (see above)
|
|
* @param {string} wrapper.type Node type of wrapper
|
|
* @param {Object} [wrapper.attributes] Attributes of wrapper
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.rewrapAllNodes = function ( depth, wrapper ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var i, tx, newRange, unwrapper = [],
|
|
innerRange = new ve.Range( this.range.start + depth, this.range.end - depth );
|
|
|
|
if ( !ve.isArray( wrapper ) ) {
|
|
wrapper = [wrapper];
|
|
}
|
|
|
|
if ( this.range.end - this.range.start < depth * 2 ) {
|
|
throw new Error( 'cannot unwrap by greater depth than maximum theoretical depth of selection' );
|
|
}
|
|
|
|
for ( i = 0; i < depth; i++ ) {
|
|
unwrapper.push( this.surface.getDocument().data[this.range.start + i] );
|
|
}
|
|
|
|
tx = ve.dm.Transaction.newFromWrap( this.document, innerRange, unwrapper, wrapper, [], [] );
|
|
newRange = new ve.Range( this.range.start, this.range.end + tx.getLengthDifference() );
|
|
this.surface.change( tx, !this.noAutoSelect && newRange );
|
|
|
|
this.range = newRange;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Isolates the nodes in a fragment then unwraps them.
|
|
*
|
|
* The node selection is expanded to siblings. These are isolated such that they are the
|
|
* sole children of the nearest parent element which can 'type' can exist in.
|
|
*
|
|
* The new isolated selection is then safely unwrapped.
|
|
*
|
|
* @method
|
|
* @param {string} type Node type to isolate for
|
|
* @chainable
|
|
*/
|
|
ve.dm.SurfaceFragment.prototype.isolateAndUnwrap = function ( isolateForType ) {
|
|
// Handle null fragment
|
|
if ( !this.surface ) {
|
|
return this;
|
|
}
|
|
var nodes, startSplitNode, endSplitNode, tx,
|
|
startOffset, endOffset,
|
|
outerDepth = 0,
|
|
factory = ve.dm.nodeFactory,
|
|
allowedParents = factory.getSuggestedParentNodeTypes( isolateForType ),
|
|
startSplitRequired = false,
|
|
endSplitRequired = false,
|
|
startSplitNodes = [],
|
|
endSplitNodes = [],
|
|
fragment = this,
|
|
newRange = new ve.Range( this.range.start, this.range.end );
|
|
|
|
function createSplits( splitNodes, insertBefore ) {
|
|
var i, length,
|
|
startOffsetChange = 0, endOffsetChange = 0, data = [];
|
|
for ( i = 0, length = splitNodes.length; i < length; i++ ) {
|
|
data.unshift( { 'type': '/' + splitNodes[i].type } );
|
|
data.push( splitNodes[i].getClonedElement() );
|
|
|
|
if ( insertBefore ) {
|
|
startOffsetChange += 2;
|
|
endOffsetChange += 2;
|
|
}
|
|
}
|
|
|
|
tx = ve.dm.Transaction.newFromInsertion( fragment.getDocument(), insertBefore ? startOffset : endOffset, data );
|
|
newRange = new ve.Range( newRange.start, newRange.end + tx.getLengthDifference() );
|
|
fragment.surface.change( tx, !fragment.noAutoSelect && newRange );
|
|
|
|
startOffset += startOffsetChange;
|
|
endOffset += endOffsetChange;
|
|
}
|
|
|
|
nodes = this.getDocument().selectNodes( this.range, 'siblings' );
|
|
|
|
// Find start split point, if required
|
|
startSplitNode = nodes[0].node;
|
|
startOffset = startSplitNode.getOuterRange().start;
|
|
while( allowedParents !== null && ve.indexOf( startSplitNode.getParent().type, allowedParents ) === -1 ) {
|
|
if ( startSplitNode.getParent().indexOf( startSplitNode ) > 0 ) {
|
|
startSplitRequired = true;
|
|
}
|
|
startSplitNode = startSplitNode.getParent();
|
|
if ( startSplitRequired ) {
|
|
startSplitNodes.unshift(startSplitNode);
|
|
} else {
|
|
startOffset = startSplitNode.getOuterRange().start;
|
|
}
|
|
outerDepth++;
|
|
}
|
|
|
|
// Find end split point, if required
|
|
endSplitNode = nodes[nodes.length - 1].node;
|
|
endOffset = endSplitNode.getOuterRange().end;
|
|
while( allowedParents !== null && ve.indexOf( endSplitNode.getParent().type, allowedParents ) === -1 ) {
|
|
if ( endSplitNode.getParent().indexOf( endSplitNode ) < endSplitNode.getParent().getChildren().length - 1 ) {
|
|
endSplitRequired = true;
|
|
}
|
|
endSplitNode = endSplitNode.getParent();
|
|
if ( endSplitRequired ) {
|
|
endSplitNodes.unshift(endSplitNode);
|
|
} else {
|
|
endOffset = endSplitNode.getOuterRange().end;
|
|
}
|
|
}
|
|
|
|
if ( startSplitRequired ) {
|
|
createSplits( startSplitNodes, true );
|
|
}
|
|
|
|
if ( endSplitRequired ) {
|
|
createSplits( endSplitNodes, false );
|
|
}
|
|
|
|
this.unwrapNodes( outerDepth, 0 );
|
|
|
|
return this;
|
|
};
|