mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-13 14:58:43 +00:00
cf7f2b141d
Let's experiment with this via our local Gruntfile. If it works fine we can install it in Jenkins (similar to node-csslint). Verify through $ npm install && npm test; Fixed all outstanding violations. Also: * Added syntaxhighight to ignore. * Added imetests (which contain unformatted JSON) to ignore. * In ve.dm.ModelRegistry#matchTypeRegExps, removed redundant !! cast from the [+!!withFunc] statement which was hitting a bug in node-jscs. All callers to this local private function pass a literal boolean true/false so no need to cast it. * Removed "/* key .. , value */" from ve.setProp, though this wasn't caught by node-jscs, found it when searching for " , ". * Made npm.devDependencies fixed instead of using tilde-ranges. This too often leads to strange bugs or sudden changes. Fixed them at the version they were currently ranging to. Bug: 54218 Change-Id: Ib2630806f3946874c8b01e58cf171df83a28da29
306 lines
9 KiB
JavaScript
306 lines
9 KiB
JavaScript
/*!
|
|
* VisualEditor ContentEditable namespace.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Namespace for all VisualEditor ContentEditable classes, static methods and static properties.
|
|
* @class
|
|
* @singleton
|
|
*/
|
|
ve.ce = {
|
|
//'nodeFactory': Initialized in ve.ce.NodeFactory.js
|
|
};
|
|
|
|
/* Static Properties */
|
|
|
|
/**
|
|
* RegExp pattern for matching all whitespaces in HTML text.
|
|
*
|
|
* \u0020 (32) space
|
|
* \u00A0 (160) non-breaking space
|
|
*
|
|
* @property
|
|
*/
|
|
ve.ce.whitespacePattern = /[\u0020\u00A0]/g;
|
|
|
|
/* Static Methods */
|
|
|
|
/**
|
|
* Gets the plain text of a DOM element (that is a node canContainContent === true)
|
|
*
|
|
* In the returned string only the contents of text nodes are included, and the contents of
|
|
* non-editable elements are excluded (but replaced with the appropriate number of characters
|
|
* so the offsets match up with the linear model).
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} 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 = '',
|
|
numChars,
|
|
$element = $( element );
|
|
|
|
if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
|
|
if ( $element.hasClass( 've-ce-branchNode-slug' ) ) {
|
|
// Slugs are not represented in the model at all, but they do
|
|
// contain a single nbsp/FEFF character in the DOM, so make sure
|
|
// that character isn't counted
|
|
return '';
|
|
} else if ( $element.hasClass( 've-ce-leafNode' ) ) {
|
|
// For leaf nodes, don't return the content, but return
|
|
// the right amount of characters so the offsets match up
|
|
numChars = $element.data( 'view' ).getOuterLength();
|
|
return new Array( numChars + 1 ).join( '\u2603' );
|
|
} else {
|
|
// Traverse its children
|
|
for ( element = element.firstChild; element; element = element.nextSibling ) {
|
|
text += func( element );
|
|
}
|
|
}
|
|
} else if ( nodeType === 3 || nodeType === 4 ) {
|
|
return element.nodeValue;
|
|
}
|
|
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 represented 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 observe structural
|
|
* changes.
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} 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 its children
|
|
for ( element = element.firstChild; element; element = element.nextSibling ) {
|
|
hash += ve.ce.getDomHash( element );
|
|
}
|
|
hash += '</' + nodeName + '>';
|
|
}
|
|
return hash;
|
|
};
|
|
|
|
/**
|
|
* Gets the linear offset from a given DOM node and offset within it.
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} domNode DOM node
|
|
* @param {number} domOffset DOM offset within the DOM node
|
|
* @returns {number} Linear model offset
|
|
*/
|
|
ve.ce.getOffset = function ( domNode, domOffset ) {
|
|
if ( domNode.nodeType === Node.TEXT_NODE ) {
|
|
return ve.ce.getOffsetFromTextNode( domNode, domOffset );
|
|
} else {
|
|
return ve.ce.getOffsetFromElementNode( domNode, domOffset );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets the linear offset from a given text node and offset within it.
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} domNode DOM node
|
|
* @param {number} domOffset DOM offset within the DOM Element
|
|
* @returns {number} Linear model offset
|
|
*/
|
|
ve.ce.getOffsetFromTextNode = function ( domNode, domOffset ) {
|
|
var $node, nodeModel, current, stack, item, offset, $item;
|
|
|
|
$node = $( domNode ).closest(
|
|
'.ve-ce-branchNode, .ve-ce-leafNode'
|
|
);
|
|
nodeModel = $node.data( 'view' ).getModel();
|
|
|
|
// IE sometimes puts the cursor in a text node inside ce="false". BAD!
|
|
if ( $node[0].contentEditable === 'false' ) {
|
|
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
|
}
|
|
|
|
if ( !$node.hasClass( 've-ce-branchNode' ) ) {
|
|
return nodeModel.getOffset();
|
|
}
|
|
|
|
current = [$node.contents(), 0];
|
|
stack = [current];
|
|
offset = 0;
|
|
|
|
while ( stack.length > 0 ) {
|
|
if ( current[1] >= current[0].length ) {
|
|
stack.pop();
|
|
current = stack[ stack.length - 1 ];
|
|
continue;
|
|
}
|
|
item = current[0][current[1]];
|
|
if ( item.nodeType === Node.TEXT_NODE ) {
|
|
if ( item === domNode ) {
|
|
offset += domOffset;
|
|
break;
|
|
} else {
|
|
offset += item.textContent.length;
|
|
}
|
|
} else if ( item.nodeType === Node.ELEMENT_NODE ) {
|
|
$item = current[0].eq( current[1] );
|
|
if ( $item.hasClass( 've-ce-branchNode-slug' ) ) {
|
|
if ( $item.contents()[0] === domNode ) {
|
|
break;
|
|
}
|
|
} else if ( $item.hasClass( 've-ce-leafNode' ) ) {
|
|
offset += 2;
|
|
} else if ( $item.hasClass( 've-ce-branchNode' ) ) {
|
|
offset += $item.data( 'view' ).getOuterLength();
|
|
} else {
|
|
stack.push( [ $item.contents(), 0 ] );
|
|
current[1]++;
|
|
current = stack[ stack.length - 1 ];
|
|
continue;
|
|
}
|
|
}
|
|
current[1]++;
|
|
}
|
|
return offset + nodeModel.getOffset() + ( nodeModel.isWrapped() ? 1 : 0 );
|
|
};
|
|
|
|
/**
|
|
* Gets the linear offset from a given element node and offset within it.
|
|
*
|
|
* @method
|
|
* @param {HTMLElement} domNode DOM node
|
|
* @param {number} domOffset DOM offset within the DOM Element
|
|
* @param {number} [firstRecursionDirection] Which direction the first recursive call went in (+/-1)
|
|
* @returns {number} Linear model offset
|
|
*/
|
|
ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, firstRecursionDirection ) {
|
|
var direction, nodeModel, node,
|
|
$domNode = $( domNode );
|
|
|
|
if ( $domNode.hasClass( 've-ce-branchNode-slug' ) ) {
|
|
if ( $domNode.prev().length ) {
|
|
nodeModel = $domNode.prev().data( 'view' ).getModel();
|
|
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
|
}
|
|
if ( $domNode.next().length ) {
|
|
nodeModel = $domNode.next().data( 'view' ).getModel();
|
|
return nodeModel.getOffset();
|
|
}
|
|
}
|
|
|
|
// IE sometimes puts the cursor in a text node inside ce="false". BAD!
|
|
if ( !firstRecursionDirection && !domNode.isContentEditable ) {
|
|
nodeModel = $domNode.closest( '.ve-ce-branchNode, .ve-ce-leafNode' ).data( 'view' ).getModel();
|
|
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
|
}
|
|
|
|
if ( domOffset === 0 ) {
|
|
node = $domNode.data( 'view' );
|
|
if ( node && node instanceof ve.ce.Node ) {
|
|
nodeModel = $domNode.data( 'view' ).getModel();
|
|
if ( firstRecursionDirection === -1 ) {
|
|
return nodeModel.getOffset() + nodeModel.getOuterLength();
|
|
} else if ( firstRecursionDirection === 1 ) {
|
|
return nodeModel.getOffset();
|
|
} else {
|
|
return nodeModel.getOffset() + ( nodeModel.isWrapped() ? 1 : 0 );
|
|
}
|
|
} else {
|
|
node = $domNode.contents().last()[0];
|
|
if ( !firstRecursionDirection ) {
|
|
direction = 1;
|
|
}
|
|
}
|
|
} else {
|
|
node = $domNode.contents()[ domOffset - 1 ];
|
|
if ( !firstRecursionDirection ) {
|
|
direction = -1;
|
|
}
|
|
}
|
|
|
|
if ( node.nodeType === Node.TEXT_NODE ) {
|
|
return ve.ce.getOffsetFromTextNode( node, node.length );
|
|
} else {
|
|
return ve.ce.getOffsetFromElementNode( node, 0, direction );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets the linear offset of a given slug
|
|
*
|
|
* @method
|
|
* @param {jQuery} $node jQuery slug selection
|
|
* @returns {number} Linear model offset
|
|
* @throws {Error}
|
|
*/
|
|
ve.ce.getOffsetOfSlug = function ( $node ) {
|
|
var model;
|
|
if ( $node.index() === 0 ) {
|
|
model = $node.parent().data( 'view' ).getModel();
|
|
return model.getOffset() + ( model.isWrapped() ? 1 : 0 );
|
|
} else if ( $node.prev().length ) {
|
|
model = $node.prev().data( 'view' ).getModel();
|
|
return model.getOffset() + model.getOuterLength();
|
|
} else {
|
|
throw new Error( 'Incorrect slug location' );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if the key code represents a left or right arrow key
|
|
* @param {number} keyCode Key code
|
|
* @returns {boolean} Key code represents a left or right arrow key
|
|
*/
|
|
ve.ce.isLeftOrRightArrowKey = function ( keyCode ) {
|
|
return keyCode === OO.ui.Keys.LEFT || keyCode === OO.ui.Keys.RIGHT;
|
|
};
|
|
|
|
/**
|
|
* Check if the key code represents an up or down arrow key
|
|
* @param {number} keyCode Key code
|
|
* @returns {boolean} Key code represents an up or down arrow key
|
|
*/
|
|
ve.ce.isUpOrDownArrowKey = function ( keyCode ) {
|
|
return keyCode === OO.ui.Keys.UP || keyCode === OO.ui.Keys.DOWN;
|
|
};
|
|
|
|
/**
|
|
* Check if the key code represents an arrow key
|
|
* @param {number} keyCode Key code
|
|
* @returns {boolean} Key code represents an arrow key
|
|
*/
|
|
ve.ce.isArrowKey = function ( keyCode ) {
|
|
return ve.ce.isLeftOrRightArrowKey( keyCode ) || ve.ce.isUpOrDownArrowKey( keyCode );
|
|
};
|
|
|
|
/**
|
|
* Check if keyboard shortcut modifier key is pressed.
|
|
*
|
|
* @method
|
|
* @param {jQuery.Event} e Key press event
|
|
* @returns {boolean} Modifier key is pressed
|
|
*/
|
|
ve.ce.isShortcutKey = function ( e ) {
|
|
return !!( e.ctrlKey || e.metaKey );
|
|
};
|