mediawiki-extensions-Visual.../modules/ve/ce/ve.ce.Node.js
Catrope 9372ca60ad Kill getOffsetFromNode() with fire
The way it operated was evil. It did a depth-first search from the root,
finding the node using reference equality. For documents with deep
structures, this could take a long time. Inez did some profiling and
found it was called tens of millions of times on a complex document.

Kill getOffsetFromNode() and move its functionality to getOffset().
The logic has been completely rewritten: getOffset() now traverses
up from the node rather than down from the root, and pretty much does
the reverse of what getNodeFromOffset() does. This should be much more
efficient even without offset caching in the node objects (which we may
still implement later).

Change-Id: I125f9fa423c40db6472e2c4a7c94214218ba3bc7
2013-05-09 17:26:22 -07:00

239 lines
5.2 KiB
JavaScript

/*!
* VisualEditor ContentEditable Node class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Generic ContentEditable node.
*
* @abstract
* @extends ve.ce.View
* @mixins ve.Node
*
* @constructor
* @param {ve.dm.Node} model Model to observe
* @param {jQuery} [$element] Element to use as a container
*/
ve.ce.Node = function VeCeNode( model, $element ) {
// Parent constructor
ve.ce.View.call( this, model, $element );
// Mixin constructor
ve.Node.call( this );
// Properties
this.parent = null;
// Events
this.model.connect( this, { 'attributeChange': 'onAttributeChange' } );
};
/* Inheritance */
ve.inheritClass( ve.ce.Node, ve.ce.View );
ve.mixinClass( ve.ce.Node, ve.Node );
/* Static Members */
/**
* Whether this node type can be split.
*
* When the user presses Enter, we split the node they're in (if splittable), then split its parent
* if splittable, and continue traversing up the tree and stop at the first non-splittable node.
*
* @static
* @property static.canBeSplit
* @inheritable
*/
ve.ce.Node.static.canBeSplit = false;
/* Methods */
/**
* Handle attribute change events.
*
* Whitelisted attributes will be added or removed in sync with the DOM. They are initially set in
* the constructor.
*
* @method
* @param {string} key Attribute key
* @param {string} from Old value
* @param {string} to New value
*/
ve.ce.Node.prototype.onAttributeChange = function ( key, from, to ) {
this.renderAttributes( { key: to } );
};
/**
* Get 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();
};
/**
* Get allowed parent node types.
*
* This method passes through to the model.
*
* @method
* @returns {string[]|null} List of node types allowed as parents or null if any type is allowed
*/
ve.ce.Node.prototype.getParentNodeTypes = function () {
return this.model.getParentNodeTypes();
};
/**
* Check if the node 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();
};
/**
* Check if the node can have children but not content nor be content.
*
* This method passes through to the model.
*
* @method
* @returns {boolean} Model node can have children but not content nor be content
*/
ve.ce.Node.prototype.canHaveChildrenNotContent = function () {
return this.model.canHaveChildrenNotContent();
};
/**
* Check if the node has a wrapped element in the document data.
*
* 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();
};
/**
* Check if the node can contain content.
*
* This method passes through to the model.
*
* @method
* @returns {boolean} Node can contain content
*/
ve.ce.Node.prototype.canContainContent = function () {
return this.model.canContainContent();
};
/**
* Check if the node is content.
*
* This method passes through to the model.
*
* @method
* @returns {boolean} Node is content
*/
ve.ce.Node.prototype.isContent = function () {
return this.model.isContent();
};
/**
* Check if the node can have a slug before it.
*
* TODO: Figure out a way to remove the hard-coding for text nodes here.
*
* @static
* @method
* @returns {boolean} Whether the node can have a slug before it
*/
ve.ce.Node.prototype.canHaveSlugBefore = function () {
return !this.canContainContent() && this.getParentNodeTypes() === null && this.type !== 'text';
};
/**
* Check if the node can have a slug after it.
*
* @static
* @method
* @returns {boolean} Whether the node can have a slug after it
*/
ve.ce.Node.prototype.canHaveSlugAfter = ve.ce.Node.prototype.canHaveSlugBefore;
/**
* Get the length of the node.
*
* This method passes through to the model.
*
* @method
* @returns {number} Model length
*/
ve.ce.Node.prototype.getLength = function () {
return this.model.getLength();
};
/**
* Get the outer length of the node, which includes wrappers if present.
*
* This method passes through to the model.
*
* @method
* @returns {number} Model outer length
*/
ve.ce.Node.prototype.getOuterLength = function () {
return this.model.getOuterLength();
};
/**
* Get the offset of the node.
*
* @see ve.dm.Node#getOffset
* @returns {number} Offset
*/
ve.ce.Node.prototype.getOffset = function () {
return this.model.getOffset();
};
/**
* Check if the node can be split.
*
* @method
* @returns {boolean} Node can be split
*/
ve.ce.Node.prototype.canBeSplit = function () {
return this.constructor.static.canBeSplit;
};
/**
* Get the closest splittable node upstream.
*
* @method
* @returns {ve.ce.Node} Closest splittable node
*/
ve.ce.Node.getSplitableNode = function ( node ) {
var splitableNode = null;
node.traverseUpstream( function ( node ) {
if ( node.canBeSplit() ) {
splitableNode = node;
return true;
} else {
return false;
}
} );
return splitableNode;
};