mediawiki-extensions-Visual.../modules/ve/ce/ve.ce.BranchNode.js

271 lines
8.2 KiB
JavaScript
Raw Normal View History

/**
* VisualEditor content editable BranchNode class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
2012-03-08 12:27:02 +00:00
/**
* ContentEditable node that can have branch or leaf children.
*
2012-03-08 12:27:02 +00:00
* @class
* @abstract
* @constructor
* @extends {ve.ce.Node}
* @param {String} type Symbolic name of node type
* @param model {ve.dm.BranchNode} Model to observe
2012-03-08 12:27:02 +00:00
* @param {jQuery} [$element] Element to use as a container
*/
ve.ce.BranchNode = function VeCeBranchNode( type, model, $element ) {
2012-03-08 12:27:02 +00:00
ve.BranchNode.call( this );
Object management: Object create/inherit/clone utilities * For the most common case: - replace ve.extendClass with ve.inheritClass (chose slightly different names to detect usage of the old/new one, and I like 'inherit' better). - move it up to below the constructor, see doc block for why. * Cases where more than 2 arguments were passed to ve.extendClass are handled differently depending on the case. In case of a longer inheritance tree, the other arguments could be omitted (like in "ve.ce.FooBar, ve.FooBar, ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar, because ve.ce.FooBar inherits from ve.Bar). In the case of where it previously had two mixins with ve.extendClass(), either one becomes inheritClass and one a mixin, both to mixinClass(). No visible changes should come from this commit as the instances still all have the same visible properties in the end. No more or less than before. * Misc.: - Be consistent in calling parent constructors in the same order as the inheritance. - Add missing @extends and @param documentation. - Replace invalid {Integer} type hint with {Number}. - Consistent doc comments order: @class, @abstract, @constructor, @extends, @params. - Fix indentation errors A fairly common mistake was a superfluous space before the identifier on the assignment line directly below the documentation comment. $ ack "^ [^*]" --js modules/ve - Typo "Inhertiance" -> "Inheritance". - Replacing the other confusing comment "Inheritance" (inside the constructor) with "Parent constructor". - Add missing @abstract for ve.ui.Tool. - Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js - Add function names to all @constructor functions. Now that we have inheritance it is important and useful to have these functions not be anonymous. Example of debug shot: http://cl.ly/image/1j3c160w3D45 Makes the difference between < documentNode; > ve_dm_DocumentNode ... : ve_dm_BranchNode ... : ve_dm_Node ... : ve_dm_Node ... : Object ... without names (current situation): < documentNode; > Object ... : Object ... : Object ... : Object ... : Object ... though before this commit, it really looks like this (flattened since ve.extendClass really did a mixin): < documentNode; > Object ... ... ... Pattern in Sublime (case-sensitive) to find nameless constructor functions: "^ve\..*\.([A-Z])([^\.]+) = function \(" Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
// Parent constructor
ve.ce.Node.call( this, type, model, $element );
2012-03-08 12:27:02 +00:00
// Properties
Object management: Object create/inherit/clone utilities * For the most common case: - replace ve.extendClass with ve.inheritClass (chose slightly different names to detect usage of the old/new one, and I like 'inherit' better). - move it up to below the constructor, see doc block for why. * Cases where more than 2 arguments were passed to ve.extendClass are handled differently depending on the case. In case of a longer inheritance tree, the other arguments could be omitted (like in "ve.ce.FooBar, ve.FooBar, ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar, because ve.ce.FooBar inherits from ve.Bar). In the case of where it previously had two mixins with ve.extendClass(), either one becomes inheritClass and one a mixin, both to mixinClass(). No visible changes should come from this commit as the instances still all have the same visible properties in the end. No more or less than before. * Misc.: - Be consistent in calling parent constructors in the same order as the inheritance. - Add missing @extends and @param documentation. - Replace invalid {Integer} type hint with {Number}. - Consistent doc comments order: @class, @abstract, @constructor, @extends, @params. - Fix indentation errors A fairly common mistake was a superfluous space before the identifier on the assignment line directly below the documentation comment. $ ack "^ [^*]" --js modules/ve - Typo "Inhertiance" -> "Inheritance". - Replacing the other confusing comment "Inheritance" (inside the constructor) with "Parent constructor". - Add missing @abstract for ve.ui.Tool. - Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js - Add function names to all @constructor functions. Now that we have inheritance it is important and useful to have these functions not be anonymous. Example of debug shot: http://cl.ly/image/1j3c160w3D45 Makes the difference between < documentNode; > ve_dm_DocumentNode ... : ve_dm_BranchNode ... : ve_dm_Node ... : ve_dm_Node ... : Object ... without names (current situation): < documentNode; > Object ... : Object ... : Object ... : Object ... : Object ... though before this commit, it really looks like this (flattened since ve.extendClass really did a mixin): < documentNode; > Object ... ... ... Pattern in Sublime (case-sensitive) to find nameless constructor functions: "^ve\..*\.([A-Z])([^\.]+) = function \(" Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
this.domWrapperElementType = this.$.get( 0 ).nodeName.toLowerCase();
this.$slugs = $();
2012-03-08 12:27:02 +00:00
// Events
this.model.addListenerMethod( this, 'splice', 'onSplice' );
// DOM Changes
this.$.addClass( 've-ce-branchNode' );
2012-03-08 12:27:02 +00:00
// Initialization
if ( model.getChildren().length ) {
this.onSplice.apply( this, [0, 0].concat( model.getChildren() ) );
2012-03-08 12:27:02 +00:00
}
};
Object management: Object create/inherit/clone utilities * For the most common case: - replace ve.extendClass with ve.inheritClass (chose slightly different names to detect usage of the old/new one, and I like 'inherit' better). - move it up to below the constructor, see doc block for why. * Cases where more than 2 arguments were passed to ve.extendClass are handled differently depending on the case. In case of a longer inheritance tree, the other arguments could be omitted (like in "ve.ce.FooBar, ve.FooBar, ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar, because ve.ce.FooBar inherits from ve.Bar). In the case of where it previously had two mixins with ve.extendClass(), either one becomes inheritClass and one a mixin, both to mixinClass(). No visible changes should come from this commit as the instances still all have the same visible properties in the end. No more or less than before. * Misc.: - Be consistent in calling parent constructors in the same order as the inheritance. - Add missing @extends and @param documentation. - Replace invalid {Integer} type hint with {Number}. - Consistent doc comments order: @class, @abstract, @constructor, @extends, @params. - Fix indentation errors A fairly common mistake was a superfluous space before the identifier on the assignment line directly below the documentation comment. $ ack "^ [^*]" --js modules/ve - Typo "Inhertiance" -> "Inheritance". - Replacing the other confusing comment "Inheritance" (inside the constructor) with "Parent constructor". - Add missing @abstract for ve.ui.Tool. - Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js - Add function names to all @constructor functions. Now that we have inheritance it is important and useful to have these functions not be anonymous. Example of debug shot: http://cl.ly/image/1j3c160w3D45 Makes the difference between < documentNode; > ve_dm_DocumentNode ... : ve_dm_BranchNode ... : ve_dm_Node ... : ve_dm_Node ... : Object ... without names (current situation): < documentNode; > Object ... : Object ... : Object ... : Object ... : Object ... though before this commit, it really looks like this (flattened since ve.extendClass really did a mixin): < documentNode; > Object ... ... ... Pattern in Sublime (case-sensitive) to find nameless constructor functions: "^ve\..*\.([A-Z])([^\.]+) = function \(" Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
/* Inheritance */
ve.inheritClass( ve.ce.BranchNode, ve.ce.Node );
ve.mixinClass( ve.ce.BranchNode, ve.BranchNode );
/* Static Members */
if ( $.browser.msie ) {
ve.ce.BranchNode.$slugTemplate = $( '<span class="ve-ce-slug">&nbsp;</span>' );
} else {
ve.ce.BranchNode.$slugTemplate = $( '<span class="ve-ce-slug">&#xFEFF;</span>' );
}
2012-03-08 12:27:02 +00:00
/* 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 types,
value = model.getAttribute( key );
if ( value === undefined ) {
throw new Error( 'Undefined attribute: ' + key );
2012-03-08 12:27:02 +00:00
}
types = ve.ce.nodeFactory.lookup( model.getType() ).domWrapperElementTypes;
if ( types[value] === undefined ) {
throw new Error( 'Invalid attribute value: ' + value );
}
return types[value];
2012-03-08 12:27:02 +00:00
};
/**
* 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 $( document.createElement( type ) );
2012-03-08 12:27:02 +00:00
};
/* Methods */
2012-03-08 12:27:02 +00:00
ve.ce.BranchNode.prototype.addSlugs = function () {
var i, $slug, $slugTemplate;
// Remove all slugs in this branch
this.$slugs.remove();
$slugTemplate = ve.ce.BranchNode.$slugTemplate.clone();
if ( this.canHaveGrandchildren() ) {
$slugTemplate.css( 'display', 'block');
}
// Iterate over all children of this branch and add slugs in appropriate places
if ( this.getLength() === 0 ) {
this.$.empty();
$slug = $slugTemplate.clone();
this.$slugs = this.$slugs.add( $slug );
this.$.append( $slug );
}
// TODO the before/after logic below is duplicated from hasSlugAtOffset(), refactor this
for ( i = 0; i < this.children.length; i++ ) {
if ( this.children[i].canHaveSlugAfter() ) {
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].canHaveSlugBefore() )
) {
$slug = $slugTemplate.clone();
this.$slugs = this.$slugs.add( $slug );
this.children[i].$.after( $slug );
}
}
// First sluggable child (left side)
if ( i === 0 && this.children[i].canHaveSlugBefore() ) {
$slug = $slugTemplate.clone();
this.$slugs = this.$slugs.add( $slug );
this.children[i].$.before( $slug );
}
}
};
/**
* 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 $element,
type = ve.ce.BranchNode.getDomWrapperType( this.model, key );
if ( type !== this.domWrapperElementType ) {
$element = $( document.createElement( 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;
// Remember which type we are using now
this.domWrapperElementType = type;
}
2012-03-08 12:27:02 +00:00
};
/**
* 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 its model.
*
* @method
Object management: Object create/inherit/clone utilities * For the most common case: - replace ve.extendClass with ve.inheritClass (chose slightly different names to detect usage of the old/new one, and I like 'inherit' better). - move it up to below the constructor, see doc block for why. * Cases where more than 2 arguments were passed to ve.extendClass are handled differently depending on the case. In case of a longer inheritance tree, the other arguments could be omitted (like in "ve.ce.FooBar, ve.FooBar, ve.Bar". ve.ce.FooBar only needs to inherit from ve.FooBar, because ve.ce.FooBar inherits from ve.Bar). In the case of where it previously had two mixins with ve.extendClass(), either one becomes inheritClass and one a mixin, both to mixinClass(). No visible changes should come from this commit as the instances still all have the same visible properties in the end. No more or less than before. * Misc.: - Be consistent in calling parent constructors in the same order as the inheritance. - Add missing @extends and @param documentation. - Replace invalid {Integer} type hint with {Number}. - Consistent doc comments order: @class, @abstract, @constructor, @extends, @params. - Fix indentation errors A fairly common mistake was a superfluous space before the identifier on the assignment line directly below the documentation comment. $ ack "^ [^*]" --js modules/ve - Typo "Inhertiance" -> "Inheritance". - Replacing the other confusing comment "Inheritance" (inside the constructor) with "Parent constructor". - Add missing @abstract for ve.ui.Tool. - Corrected ve.FormatDropdownTool to ve.ui.FormatDropdownTool.js - Add function names to all @constructor functions. Now that we have inheritance it is important and useful to have these functions not be anonymous. Example of debug shot: http://cl.ly/image/1j3c160w3D45 Makes the difference between < documentNode; > ve_dm_DocumentNode ... : ve_dm_BranchNode ... : ve_dm_Node ... : ve_dm_Node ... : Object ... without names (current situation): < documentNode; > Object ... : Object ... : Object ... : Object ... : Object ... though before this commit, it really looks like this (flattened since ve.extendClass really did a mixin): < documentNode; > Object ... ... ... Pattern in Sublime (case-sensitive) to find nameless constructor functions: "^ve\..*\.([A-Z])([^\.]+) = function \(" Change-Id: Iab763954fb8cf375900d7a9a92dec1c755d5407e
2012-09-05 06:07:47 +00:00
* @param {Number} index Index to remove and or insert nodes at
* @param {Number} howmany Number of nodes to remove
* @param {ve.dm.BranchNode} [...] Variadic list of nodes to insert
*/
Make use of new jshint options * Restricting "camelcase": No changes, we were passing all of these already * Explicitly unrestricting "forin" and "plusplus" These are off by default in node-jshint, but some distro of jshint and editors that use their own wrapper around jshint instead of node-jshint (Eclipse?) may have different defaults. Therefor setting them to false explicitly. This also serves as a reminder for the future so we'll always know we don't pass that, in case we would want to change that. * Fix order ("quotemark" before "regexp") * Restricting "unused" We're not passing all of this, which is why I've set it to false for now. But I did put it in .jshintrc as placeholder. I've fixed most of them, there's some left where there is no clean solution. * While at it fix a few issues: - Unused variables ($target, $window) - Bad practices (using jQuery context for find instead of creation) - Redundant /*global */ comments - Parameters that are not used and don't have documentation either - Lines longer than 100 chars @ 4 spaces/tab * Note: - ve.ce.Surface.prototype.onChange takes two arguments but never uses the former. And even the second one can be null/undefined. Aside from that, the .change() function emits another event for the transaction already. Looks like this should be refactored a bit, two more separated events probably or one that is actually used better. - Also cleaned up a lot of comments, some of which were missing, others were incorrect - Reworked the contentChange event so we are no longer using the word new as an object key; expanded a complex object into multiple arguments being passed through the event to make it easier to work with and document Change-Id: I8490815a508c6c379d5f9a743bb4aefd14576aa6
2012-08-07 06:02:18 +00:00
ve.ce.BranchNode.prototype.onSplice = function ( index ) {
2012-03-08 12:27:02 +00:00
var i,
length,
Make use of new jshint options * Restricting "camelcase": No changes, we were passing all of these already * Explicitly unrestricting "forin" and "plusplus" These are off by default in node-jshint, but some distro of jshint and editors that use their own wrapper around jshint instead of node-jshint (Eclipse?) may have different defaults. Therefor setting them to false explicitly. This also serves as a reminder for the future so we'll always know we don't pass that, in case we would want to change that. * Fix order ("quotemark" before "regexp") * Restricting "unused" We're not passing all of this, which is why I've set it to false for now. But I did put it in .jshintrc as placeholder. I've fixed most of them, there's some left where there is no clean solution. * While at it fix a few issues: - Unused variables ($target, $window) - Bad practices (using jQuery context for find instead of creation) - Redundant /*global */ comments - Parameters that are not used and don't have documentation either - Lines longer than 100 chars @ 4 spaces/tab * Note: - ve.ce.Surface.prototype.onChange takes two arguments but never uses the former. And even the second one can be null/undefined. Aside from that, the .change() function emits another event for the transaction already. Looks like this should be refactored a bit, two more separated events probably or one that is actually used better. - Also cleaned up a lot of comments, some of which were missing, others were incorrect - Reworked the contentChange event so we are no longer using the word new as an object key; expanded a complex object into multiple arguments being passed through the event to make it easier to work with and document Change-Id: I8490815a508c6c379d5f9a743bb4aefd14576aa6
2012-08-07 06:02:18 +00:00
args = Array.prototype.slice.call( arguments ),
$anchor,
Make use of new jshint options * Restricting "camelcase": No changes, we were passing all of these already * Explicitly unrestricting "forin" and "plusplus" These are off by default in node-jshint, but some distro of jshint and editors that use their own wrapper around jshint instead of node-jshint (Eclipse?) may have different defaults. Therefor setting them to false explicitly. This also serves as a reminder for the future so we'll always know we don't pass that, in case we would want to change that. * Fix order ("quotemark" before "regexp") * Restricting "unused" We're not passing all of this, which is why I've set it to false for now. But I did put it in .jshintrc as placeholder. I've fixed most of them, there's some left where there is no clean solution. * While at it fix a few issues: - Unused variables ($target, $window) - Bad practices (using jQuery context for find instead of creation) - Redundant /*global */ comments - Parameters that are not used and don't have documentation either - Lines longer than 100 chars @ 4 spaces/tab * Note: - ve.ce.Surface.prototype.onChange takes two arguments but never uses the former. And even the second one can be null/undefined. Aside from that, the .change() function emits another event for the transaction already. Looks like this should be refactored a bit, two more separated events probably or one that is actually used better. - Also cleaned up a lot of comments, some of which were missing, others were incorrect - Reworked the contentChange event so we are no longer using the word new as an object key; expanded a complex object into multiple arguments being passed through the event to make it easier to work with and document Change-Id: I8490815a508c6c379d5f9a743bb4aefd14576aa6
2012-08-07 06:02:18 +00:00
removals;
2012-03-08 12:27:02 +00:00
// 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] = ve.ce.nodeFactory.create( args[i].getType(), args[i] );
2012-03-08 12:27:02 +00:00
}
}
removals = this.children.splice.apply( this.children, args );
2012-03-08 12:27:02 +00:00
for ( i = 0, length = removals.length; i < length; i++ ) {
removals[i].detach();
// Update DOM
removals[i].$.detach();
}
if ( args.length >= 3 ) {
if ( index ) {
// Get the element before the insertion point
$anchor = this.children[ index - 1 ].$.last();
2012-03-08 12:27:02 +00:00
}
for ( i = args.length - 1; i >= 2; i-- ) {
args[i].attach( this );
if ( index ) {
$anchor.after( args[i].$ );
} else {
this.$.prepend( args[i].$ );
}
}
}
this.addSlugs();
2012-03-08 12:27:02 +00:00
};
ve.ce.BranchNode.prototype.hasSlugAtOffset = function ( offset ) {
var i, nodeOffset, nodeLength;
if ( this.getLength() === 0 ) {
return true;
}
for ( i = 0; i < this.children.length; i++ ) {
if ( this.children[i].canHaveSlugAfter() || this.children[i].canHaveSlugBefore() ) {
nodeOffset = this.children[i].model.getRoot().getOffsetFromNode( this.children[i].model );
if ( this.children[i].canHaveSlugAfter() ) {
nodeLength = this.children[i].model.getOuterLength();
if (
// Offset is after this child
offset === nodeOffset + nodeLength &&
(
// 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].canHaveSlugBefore() )
)
) {
return true;
}
}
// First sluggable child (left side)
if ( i === 0 && offset === nodeOffset && this.children[i].canHaveSlugBefore() ) {
return true;
}
2012-03-08 12:27:02 +00:00
}
}
return false;
2012-03-08 12:27:02 +00:00
};
ve.ce.BranchNode.prototype.clean = function () {
// Detach all child nodes from this.$
// We can't use this.$.empty() because that destroys .data() and event handlers
this.$.contents().each( function () {
$(this).detach();
} );
// Reattach the child nodes we're supposed to have
for ( var i = 0; i < this.children.length; i++ ) {
this.$.append( this.children[i].$ );
2012-03-08 12:27:02 +00:00
}
this.addSlugs();
2012-03-08 12:27:02 +00:00
};