r101684 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r101683‎ | r101684 | r101685 >
Date:21:00, 2 November 2011
Author:brion
Status:ok
Tags:
Comment:
Copy wikidom lib/hype into VisualEditor/modules
Modified paths:
  • /trunk/extensions/VisualEditor/modules (added) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/modules/es.js
@@ -0,0 +1,150 @@
 2+/**
 3+ * EditSurface namespace.
 4+ *
 5+ * All classes and functions will be attached to this object to keep the global namespace clean.
 6+ */
 7+var es = {};
 8+
 9+/* Functions */
 10+
 11+/**
 12+ * Extends a constructor with the prototype of another.
 13+ *
 14+ * When using this, it's required to include a call to the constructor of the parent class as the
 15+ * first code in the child class's constructor.
 16+ *
 17+ * @example
 18+ * // Define parent class
 19+ * function Foo() {
 20+ * // code here
 21+ * }
 22+ * // Define child class
 23+ * function Bar() {
 24+ * // Call parent constructor
 25+ * Foo.call( this );
 26+ * }
 27+ * // Extend prototype
 28+ * extend( Bar, Foo );
 29+ *
 30+ * @static
 31+ * @method
 32+ * @param {Function} dst Class to extend
 33+ * @param {Function} src Base class to use methods from
 34+ */
 35+es.extendClass = function( dst, src ) {
 36+ var base = new src();
 37+ for ( var method in base ) {
 38+ if ( typeof base[method] === 'function' && !( method in dst.prototype ) ) {
 39+ dst.prototype[method] = base[method];
 40+ }
 41+ }
 42+};
 43+
 44+es.extendObject = $.extend;
 45+
 46+es.isPlainObject = $.isPlainObject;
 47+
 48+es.isArray = $.isArray;
 49+
 50+/**
 51+ * Recursively compares string and number property between two objects.
 52+ *
 53+ * A false result may be caused by property inequality or by properties in one object missing from
 54+ * the other. An asymmetrical test may also be performed, which checks only that properties in the
 55+ * first object are present in the second object, but not the inverse.
 56+ *
 57+ * @static
 58+ * @method
 59+ * @param {Object} a First object to compare
 60+ * @param {Object} b Second object to compare
 61+ * @param {Boolean} [asymmetrical] Whether to check only that b contains values from a
 62+ * @returns {Boolean} If the objects contain the same values as each other
 63+ */
 64+es.compareObjects = function( a, b, asymmetrical ) {
 65+ var aValue, bValue, aType, bType;
 66+ var k;
 67+ for ( k in a ) {
 68+ aValue = a[k];
 69+ bValue = b[k];
 70+ aType = typeof aValue;
 71+ bType = typeof bValue;
 72+ if ( aType !== bType ||
 73+ ( ( aType === 'string' || aType === 'number' ) && aValue !== bValue ) ||
 74+ ( es.isPlainObject( aValue ) && !es.compareObjects( aValue, bValue ) ) ) {
 75+ return false;
 76+ }
 77+ }
 78+ // If the check is not asymmetrical, recursing with the arguments swapped will verify our result
 79+ return asymmetrical ? true : es.compareObjects( b, a, true );
 80+};
 81+
 82+/**
 83+ * Gets a deep copy of an array's string, number, array and plain-object contents.
 84+ *
 85+ * @static
 86+ * @method
 87+ * @param {Array} source Array to copy
 88+ * @returns {Array} Copy of source array
 89+ */
 90+es.copyArray = function( source ) {
 91+ var destination = [];
 92+ for ( var i = 0; i < source.length; i++ ) {
 93+ sourceValue = source[i];
 94+ sourceType = typeof sourceValue;
 95+ if ( sourceType === 'string' || sourceType === 'number' ) {
 96+ destination.push( sourceValue );
 97+ } else if ( es.isPlainObject( sourceValue ) ) {
 98+ destination.push( es.copyObject( sourceValue ) );
 99+ } else if ( es.isArray( sourceValue ) ) {
 100+ destination.push( es.copyArray( sourceValue ) );
 101+ }
 102+ }
 103+ return destination;
 104+};
 105+
 106+/**
 107+ * Gets a deep copy of an object's string, number, array and plain-object properties.
 108+ *
 109+ * @static
 110+ * @method
 111+ * @param {Object} source Object to copy
 112+ * @returns {Object} Copy of source object
 113+ */
 114+es.copyObject = function( source ) {
 115+ var destination = {};
 116+ for ( var key in source ) {
 117+ sourceValue = source[key];
 118+ sourceType = typeof sourceValue;
 119+ if ( sourceType === 'string' || sourceType === 'number' ) {
 120+ destination[key] = sourceValue;
 121+ } else if ( es.isPlainObject( sourceValue ) ) {
 122+ destination[key] = es.copyObject( sourceValue );
 123+ } else if ( es.isArray( sourceValue ) ) {
 124+ destination[key] = es.copyArray( sourceValue );
 125+ }
 126+ }
 127+ return destination;
 128+};
 129+
 130+/**
 131+ * Splice one array into another. This is the equivalent of arr.splice( offset, 0, i1, i2, i3, ... )
 132+ * except that i1, i2, i3, ... are specified as an array rather than separate parameters.
 133+ *
 134+ * @static
 135+ * @method
 136+ * @param {Array} dst Array to splice insertion into. Will be modified
 137+ * @param {Number} offset Offset in arr to splice insertion in at. May be negative; see the 'index' parameter for Array.prototype.splice()
 138+ * @param {Array} src Array of items to insert
 139+ */
 140+es.insertIntoArray = function( dst, offset, src ) {
 141+ // We need to splice insertion in in batches, because of parameter list length limits which vary cross-browser.
 142+ // 1024 seems to be a safe batch size on all browsers.
 143+ var index = 0, batchSize = 1024;
 144+ while ( index < src.length ) {
 145+ // Call arr.splice( offset, 0, i0, i1, i2, ..., i1023 );
 146+ dst.splice.apply(
 147+ dst, [index + offset, 0].concat( src.slice( index, index + batchSize ) )
 148+ );
 149+ index += batchSize;
 150+ }
 151+};
Index: trunk/extensions/VisualEditor/modules/models/es.DocumentModel.js
@@ -0,0 +1,1168 @@
 2+/**
 3+ * Creates an es.DocumentModel object.
 4+ *
 5+ * es.DocumentModel objects extend the native Array object, so it's contents are directly accessible
 6+ * through the typical methods.
 7+ *
 8+ * @class
 9+ * @constructor
 10+ * @extends {es.DocumentModelNode}
 11+ * @param {Array} data Model data to initialize with, such as data from es.DocumentModel.getData()
 12+ * @param {Object} attributes Document attributes
 13+ */
 14+es.DocumentModel = function( data, attributes ) {
 15+ // Inheritance
 16+ es.DocumentModelNode.call( this, null, data ? data.length : 0 );
 17+
 18+ // Properties
 19+ this.data = es.isArray( data ) ? data : [];
 20+ this.attributes = es.isPlainObject( attributes ) ? attributes : {};
 21+
 22+ // Auto-generate model tree
 23+ var nodes = es.DocumentModel.createNodesFromData( this.data );
 24+ for ( var i = 0; i < nodes.length; i++ ) {
 25+ this.push( nodes[i] );
 26+ }
 27+};
 28+
 29+/* Static Members */
 30+
 31+/**
 32+ * Mapping of symbolic names and node model constructors.
 33+ */
 34+es.DocumentModel.nodeModels = {};
 35+
 36+/**
 37+ * Mapping of symbolic names and nesting rules.
 38+ *
 39+ * Each rule is an object with a parents and children property. Each of these properties may contain
 40+ * one of two possible values:
 41+ * Array - List of allowed element types (if empty, no elements will be allowed)
 42+ * Null - Any element type is allowed (as long as the other element also allows it)
 43+ *
 44+ * @example Paragraph rules
 45+ * {
 46+ * 'parents': null,
 47+ * 'children': []
 48+ * }
 49+ * @example List rules
 50+ * {
 51+ * 'parents': null,
 52+ * 'children': ['listItem']
 53+ * }
 54+ * @example ListItem rules
 55+ * {
 56+ * 'parents': ['list'],
 57+ * 'children': []
 58+ * }
 59+ */
 60+es.DocumentModel.nodeRules = {};
 61+
 62+/**
 63+ * Mapping of operation types to pure functions.
 64+ *
 65+ * Each function is called in the context of a state, and takes an operation object as a parameter.
 66+ */
 67+es.DocumentModel.operations = ( function() {
 68+ function invalidate( from, to ) {
 69+ this.rebuild.push( { 'from': from, 'to': to } );
 70+ }
 71+
 72+ function retain( op ) {
 73+ annotate.call( this, this.cursor + op.length );
 74+ this.cursor += op.length;
 75+ }
 76+
 77+ function insert( op ) {
 78+ if ( es.DocumentModel.isStructuralOffset( this.data, this.cursor ) ) {
 79+ // TODO: Support tree updates when inserting between elements
 80+ } else {
 81+ // Get the node we are about to insert into
 82+ var node = this.tree.getNodeFromOffset( this.cursor );
 83+ if ( es.DocumentModel.containsElementData( op.data ) ) {
 84+ var nodeParent = node.getParent();
 85+ if ( !nodeParent ) {
 86+ throw 'Missing parent error. Node does not have a parent node.';
 87+ }
 88+ var offset = this.tree.getOffsetFromNode( node ),
 89+ length = node.getElementLength() + op.data.length,
 90+ index = nodeParent.indexOf( node );
 91+ if ( index === -1 ) {
 92+ throw 'Missing child error. Node could not be found in its parent node.';
 93+ }
 94+ // Remove the node we are about to insert into from the model tree
 95+ nodeParent.splice( index, 1 );
 96+ // Perform insert on linear data model
 97+ es.insertIntoArray( this.data, this.cursor, op.data );
 98+ annotate.call( this, this.cursor + op.data.length );
 99+ // Regenerate nodes for the data we've affected
 100+ var nodes = es.DocumentModel.createNodesFromData(
 101+ this.data.slice( offset, length )
 102+ );
 103+ // Insert new elements into the tree where the old one used to be
 104+ for ( var i = nodes.length; i >= 0; i-- ) {
 105+ this.tree.splice( index, nodes[i] );
 106+ }
 107+ } else {
 108+ // Perform insert on linear data model
 109+ es.insertIntoArray( this.data, this.cursor, op.data );
 110+ annotate.call( this, this.cursor + op.data.length );
 111+ // Update model tree
 112+ node.adjustContentLength( op.data.length );
 113+ }
 114+ }
 115+ this.cursor += op.data.length;
 116+ }
 117+
 118+ function remove( op ) {
 119+ var elementLeft = es.DocumentModel.isElementData( this.data, this.cursor ),
 120+ elementRight = es.DocumentModel.isElementData( this.cursor + op.data.length );
 121+ if ( elementLeft && elementRight ) {
 122+ // TODO: Support tree updates when removing whole elements
 123+ } else {
 124+ if ( es.DocumentModel.containsElementData( op.data ) ) {
 125+ // TODO: Support tree updates when removing partial elements
 126+ } else {
 127+ // Get the node we are removing content from
 128+ var node = this.tree.getNodeFromOffset( this.cursor );
 129+ // Remove content from linear data model
 130+ this.data.splice( this.cursor, op.data.length );
 131+ // Update model tree
 132+ node.adjustContentLength( -op.data.length );
 133+ }
 134+ }
 135+ }
 136+
 137+ function attribute( op, invert ) {
 138+ var element = this.data[this.cursor];
 139+ if ( element.type === undefined ) {
 140+ throw 'Invalid element error. Can not set attributes on non-element data.';
 141+ }
 142+ if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
 143+ // Automatically initialize attributes object
 144+ if ( !element.attributes ) {
 145+ element.attributes = {};
 146+ }
 147+ element.attributes[op.key] = op.value;
 148+ } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
 149+ if ( element.attributes ) {
 150+ delete element.attributes[op.key];
 151+ }
 152+ // Automatically clean up attributes object
 153+ var empty = true;
 154+ for ( var key in element.attributes ) {
 155+ empty = false;
 156+ break;
 157+ }
 158+ if ( empty ) {
 159+ delete element.attributes;
 160+ }
 161+ } else {
 162+ throw 'Invalid method error. Can not operate attributes this way: ' + method;
 163+ }
 164+ }
 165+
 166+ function annotate( to ) {
 167+ var i,
 168+ j,
 169+ length,
 170+ annotation;
 171+ // Handle annotations
 172+ if ( this.set.length ) {
 173+ for ( i = 0, length = this.set.length; i < length; i++ ) {
 174+ annotation = this.set[i];
 175+ // Auto-build annotation hash
 176+ if ( annotation.hash === undefined ) {
 177+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 178+ }
 179+ for ( j = this.cursor; j < to; j++ ) {
 180+ // Auto-convert to array
 181+ if ( es.isArray( this.data[j] ) ) {
 182+ this.data[j].push( annotation );
 183+ } else {
 184+ this.data[j] = [this.data[j], annotation];
 185+ }
 186+ }
 187+ }
 188+ }
 189+ if ( this.clear.length ) {
 190+ for ( i = 0, length = this.clear.length; i < length; i++ ) {
 191+ annotation = this.clear[i];
 192+ // Auto-build annotation hash
 193+ if ( annotation.hash === undefined ) {
 194+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 195+ }
 196+ for ( j = this.cursor; j < to; j++ ) {
 197+ var index = es.DocumentModel.getIndexOfAnnotation( this.data[j], annotation );
 198+ if ( index !== -1 ) {
 199+ this.data[j].splice( index, 1 );
 200+ }
 201+ // Auto-convert to string
 202+ if ( this.data[j].length === 1 ) {
 203+ this.data[j] = this.data[j][0];
 204+ }
 205+ }
 206+ }
 207+ }
 208+ }
 209+
 210+ function mark( op, invert ) {
 211+ var target;
 212+ if ( ( op.method === 'set' && !invert ) || ( op.method === 'clear' && invert ) ) {
 213+ target = this.set;
 214+ } else if ( ( op.method === 'clear' && !invert ) || ( op.method === 'set' && invert ) ) {
 215+ target = this.clear;
 216+ } else {
 217+ throw 'Invalid method error. Can not operate attributes this way: ' + method;
 218+ }
 219+ if ( op.bias === 'start' ) {
 220+ target.push( op.annotation );
 221+ } else if ( op.bias === 'stop' ) {
 222+ var index = es.DocumentModel.getIndexOfAnnotation( target, op.annotation );
 223+ if ( index === -1 ) {
 224+ throw 'Annotation stack error. Annotation is missing.';
 225+ }
 226+ target.splice( index, 1 );
 227+ }
 228+ }
 229+
 230+ return {
 231+ // Retain
 232+ 'retain': {
 233+ 'commit': retain,
 234+ 'rollback': retain
 235+ },
 236+ // Insert
 237+ 'insert': {
 238+ 'commit': insert,
 239+ 'rollback': remove
 240+ },
 241+ // Remove
 242+ 'remove': {
 243+ 'commit': remove,
 244+ 'rollback': insert
 245+ },
 246+ // Change element attributes
 247+ 'attribute': {
 248+ 'commit': function( op ) {
 249+ attribute.call( this, op, false );
 250+ },
 251+ 'rollback': function( op ) {
 252+ attribute.call( this, op, true );
 253+ }
 254+ },
 255+ // Change content annotations
 256+ 'annotate': {
 257+ 'commit': function( op ) {
 258+ mark.call( this, op, false );
 259+ },
 260+ 'rollback': function( op ) {
 261+ mark.call( this, op, true );
 262+ }
 263+ }
 264+ };
 265+} )();
 266+
 267+/* Static Methods */
 268+
 269+/*
 270+ * Create child nodes from an array of data.
 271+ *
 272+ * These child nodes are used for the model tree, which is a space partitioning data structure in
 273+ * which each node contains the length of itself (1 for opening, 1 for closing) and the lengths of
 274+ * it's child nodes.
 275+ */
 276+es.DocumentModel.createNodesFromData = function( data ) {
 277+ var currentNode = new es.DocumentModelNode();
 278+ for ( var i = 0, length = data.length; i < length; i++ ) {
 279+ if ( data[i].type !== undefined ) {
 280+ // It's an element, figure out it's type
 281+ var element = data[i],
 282+ type = element.type,
 283+ open = type[0] !== '/';
 284+ // Trim the "/" off the beginning of closing tag types
 285+ if ( !open ) {
 286+ type = type.substr( 1 );
 287+ }
 288+ if ( open ) {
 289+ // Validate the element type
 290+ if ( !( type in es.DocumentModel.nodeModels ) ) {
 291+ throw 'Unsuported element error. No class registered for element type: ' + type;
 292+ }
 293+ // Create a model node for the element
 294+ var newNode = new es.DocumentModel.nodeModels[element.type]( element );
 295+ // Add the new model node as a child
 296+ currentNode.push( newNode );
 297+ // Descend into the new model node
 298+ currentNode = newNode;
 299+ } else {
 300+ // Return to the parent node
 301+ currentNode = currentNode.getParent();
 302+ }
 303+ } else {
 304+ // It's content, let's start tracking the length
 305+ var start = i;
 306+ // Move forward to the next object, tracking the length as we go
 307+ while ( data[i].type === undefined && i < length ) {
 308+ i++;
 309+ }
 310+ // Now we know how long the current node is
 311+ currentNode.setContentLength( i - start );
 312+ // The while loop left us 1 element to far
 313+ i--;
 314+ }
 315+ }
 316+ return currentNode.getChildren().slice( 0 );
 317+};
 318+
 319+/**
 320+ * Creates a document model from a plain object.
 321+ *
 322+ * @static
 323+ * @method
 324+ * @param {Object} obj Object to create new document model from
 325+ * @returns {es.DocumentModel} Document model created from obj
 326+ */
 327+es.DocumentModel.newFromPlainObject = function( obj ) {
 328+ if ( obj.type === 'document' ) {
 329+ var data = [],
 330+ attributes = es.isPlainObject( obj.attributes ) ? es.copyObject( obj.attributes ) : {};
 331+ for ( var i = 0; i < obj.children.length; i++ ) {
 332+ data = data.concat( es.DocumentModel.flattenPlainObjectElementNode( obj.children[i] ) );
 333+ }
 334+ return new es.DocumentModel( data, attributes );
 335+ }
 336+ throw 'Invalid object error. Object is not a valid document object.';
 337+};
 338+
 339+/**
 340+ * Generates a hash of an annotation object based on it's name and data.
 341+ *
 342+ * TODO: Add support for deep hashing of array and object properties of annotation data.
 343+ *
 344+ * @static
 345+ * @method
 346+ * @param {Object} annotation Annotation object to generate hash for
 347+ * @returns {String} Hash of annotation
 348+ */
 349+es.DocumentModel.getAnnotationHash = function( annotation ) {
 350+ var hash = '#' + annotation.type;
 351+ if ( annotation.data ) {
 352+ var keys = [];
 353+ for ( var key in annotation.data ) {
 354+ keys.push( key + ':' + annotation.data );
 355+ }
 356+ keys.sort();
 357+ hash += '|' + keys.join( '|' );
 358+ }
 359+ return hash;
 360+};
 361+
 362+es.DocumentModel.getIndexOfAnnotation = function( annotations, annotation ) {
 363+ if ( annotation === undefined || annotation.type === undefined ) {
 364+ throw 'Invalid annotation error. Can not find non-annotation data in character.';
 365+ }
 366+ if ( es.isArray( annotations ) ) {
 367+ // Find the index of a comparable annotation (checking for same value, not reference)
 368+ for ( var i = 0; i < annotations.length; i++ ) {
 369+ // Skip over character data - used when this is called on a content data item
 370+ if ( typeof annotations[i] === 'string' ) {
 371+ continue;
 372+ }
 373+ if ( annotations[i].hash === annotation.hash ) {
 374+ return i;
 375+ }
 376+ }
 377+ }
 378+ return -1;
 379+};
 380+
 381+/**
 382+ * Creates an es.ContentModel object from a plain content object.
 383+ *
 384+ * A plain content object contains plain text and a series of annotations to be applied to ranges of
 385+ * the text.
 386+ *
 387+ * @example
 388+ * {
 389+ * 'text': '1234',
 390+ * 'annotations': [
 391+ * // Makes "23" bold
 392+ * {
 393+ * 'type': 'bold',
 394+ * 'range': {
 395+ * 'start': 1,
 396+ * 'end': 3
 397+ * }
 398+ * }
 399+ * ]
 400+ * }
 401+ *
 402+ * @static
 403+ * @method
 404+ * @param {Object} obj Plain content object, containing a "text" property and optionally
 405+ * an "annotations" property, the latter of which being an array of annotation objects including
 406+ * range information
 407+ * @returns {Array}
 408+ */
 409+es.DocumentModel.flattenPlainObjectContentNode = function( obj ) {
 410+ if ( !es.isPlainObject( obj ) ) {
 411+ // Use empty content
 412+ return [];
 413+ } else {
 414+ // Convert string to array of characters
 415+ var data = obj.text.split('');
 416+ // Render annotations
 417+ if ( es.isArray( obj.annotations ) ) {
 418+ for ( var i = 0, length = obj.annotations.length; i < length; i++ ) {
 419+ var src = obj.annotations[i];
 420+ // Build simplified annotation object
 421+ var dst = { 'type': src.type };
 422+ if ( 'data' in src ) {
 423+ dst.data = es.copyObject( src.data );
 424+ }
 425+ // Add a hash to the annotation for faster comparison
 426+ dst.hash = es.DocumentModel.getAnnotationHash( dst );
 427+ // Apply annotation to range
 428+ if ( src.start < 0 ) {
 429+ // TODO: The start can not be lower than 0! Throw error?
 430+ // Clamp start value
 431+ src.start = 0;
 432+ }
 433+ if ( src.end > data.length ) {
 434+ // TODO: The end can not be higher than the length! Throw error?
 435+ // Clamp end value
 436+ src.end = data.length;
 437+ }
 438+ for ( var j = src.start; j < src.end; j++ ) {
 439+ // Auto-convert to array
 440+ if ( typeof data[j] === 'string' ) {
 441+ data[j] = [data[j]];
 442+ }
 443+ // Append
 444+ data[j].push( dst );
 445+ }
 446+ }
 447+ }
 448+ return data;
 449+ }
 450+};
 451+
 452+/**
 453+ * Flatten a plain node object into a data array, recursively.
 454+ *
 455+ * TODO: where do we document this whole structure - aka "WikiDom"?
 456+ *
 457+ * @static
 458+ * @method
 459+ * @param {Object} obj Plain node object to flatten
 460+ * @returns {Array} Flattened version of obj
 461+ */
 462+es.DocumentModel.flattenPlainObjectElementNode = function( obj ) {
 463+ var i,
 464+ data = [],
 465+ element = { 'type': obj.type };
 466+ if ( es.isPlainObject( obj.attributes ) ) {
 467+ element.attributes = es.copyObject( obj.attributes );
 468+ }
 469+ // Open element
 470+ data.push( element );
 471+ if ( es.isPlainObject( obj.content ) ) {
 472+ // Add content
 473+ data = data.concat( es.DocumentModel.flattenPlainObjectContentNode( obj.content ) );
 474+ } else if ( es.isArray( obj.children ) ) {
 475+ // Add children - only do this if there is no content property
 476+ for ( i = 0; i < obj.children.length; i++ ) {
 477+ // TODO: Figure out if all this concatenating is inefficient. I think it is
 478+ data = data.concat( es.DocumentModel.flattenPlainObjectElementNode( obj.children[i] ) );
 479+ }
 480+ }
 481+ // Close element - TODO: Do we need attributes here or not?
 482+ data.push( { 'type': '/' + obj.type } );
 483+ return data;
 484+};
 485+
 486+/**
 487+ * Checks if a data at a given offset is content.
 488+ *
 489+ * @example Content data:
 490+ * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list>
 491+ * ^ ^ ^ ^ ^ ^
 492+ *
 493+ * @static
 494+ * @method
 495+ * @param {Array} data Data to evaluate offset within
 496+ * @param {Integer} offset Offset in data to check
 497+ * @returns {Boolean} If data at offset is content
 498+ */
 499+es.DocumentModel.isContentData = function( data, offset ) {
 500+ // Shortcut: if there's already content there, we will trust it's supposed to be there
 501+ return typeof data[offset] === 'string' || es.isArray( data[offset] );
 502+};
 503+
 504+/**
 505+ * Checks if a data at a given offset is an element.
 506+ *
 507+ * @example Element data:
 508+ * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list>
 509+ * ^ ^ ^ ^ ^ ^
 510+ *
 511+ * @static
 512+ * @method
 513+ * @param {Array} data Data to evaluate offset within
 514+ * @param {Integer} offset Offset in data to check
 515+ * @returns {Boolean} If data at offset is an element
 516+ */
 517+es.DocumentModel.isElementData = function( data, offset ) {
 518+ // TODO: Is there a safer way to check if it's a plain object without sacrificing speed?
 519+ return offset >= 0 && offset < data.length && data[offset].type !== undefined;
 520+};
 521+
 522+/**
 523+ * Checks if an offset within given data is structural.
 524+ *
 525+ * Structural offsets are those at the beginning, end or surrounded by elements. This differs
 526+ * from a location at which an element is present in that elements can be safely inserted at a
 527+ * structural location, but not nessecarily where an element is present.
 528+ *
 529+ * @example Structural offsets:
 530+ * <paragraph> a b c </paragraph> <list> <listItem> d e f </listItem> </list>
 531+ * ^ ^ ^ ^ ^
 532+ *
 533+ * @static
 534+ * @method
 535+ * @param {Array} data Data to evaluate offset within
 536+ * @param {Integer} offset Offset to check
 537+ * @returns {Boolean} Whether offset is structural or not
 538+ */
 539+es.DocumentModel.isStructuralOffset = function( data, offset ) {
 540+ // Edges are always structural
 541+ if ( offset === 0 || offset === data.length ) {
 542+ return true;
 543+ }
 544+ // Structual offsets will have elements on each side
 545+ if ( data[offset - 1].type !== undefined && data[offset].type !== undefined ) {
 546+ return true;
 547+ }
 548+ return false;
 549+};
 550+
 551+/**
 552+ * Checks if elements are present within data.
 553+ *
 554+ * @static
 555+ * @method
 556+ * @param {Array} data Data to look for elements within
 557+ * @returns {Boolean} If elements exist in data
 558+ */
 559+es.DocumentModel.containsElementData = function( data ) {
 560+ for ( var i = 0, length = data.length; i < length; i++ ) {
 561+ if ( data[i].type !== undefined ) {
 562+ return true;
 563+ }
 564+ }
 565+ return false;
 566+};
 567+
 568+/* Methods */
 569+
 570+/**
 571+ * Creates a document view for this model.
 572+ *
 573+ * @method
 574+ * @returns {es.DocumentView}
 575+ */
 576+es.DocumentModel.prototype.createView = function() {
 577+ return new es.DocumentView( this );
 578+};
 579+
 580+/**
 581+ * Gets copy of the document data.
 582+ *
 583+ * @method
 584+ * @param {es.Range} [range] Range of data to get, all data will be given by default
 585+ * @param {Boolean} [deep=false] Whether to return a deep copy (WARNING! This may be very slow)
 586+ * @returns {Array} Copy of document data
 587+ */
 588+es.DocumentModel.prototype.getData = function( range, deep ) {
 589+ var start = 0,
 590+ end;
 591+ if ( range !== undefined ) {
 592+ range.normalize();
 593+ start = Math.max( 0, Math.min( this.data.length, range.start ) );
 594+ end = Math.max( 0, Math.min( this.data.length, range.end ) );
 595+ }
 596+ var data = this.data.slice( start, end );
 597+ return deep ? es.copyArray( data ) : data;
 598+};
 599+
 600+/**
 601+ * Gets the element object of a node.
 602+ *
 603+ * @method
 604+ * @param {es.DocumentModelNode} node Node to get element object for
 605+ * @returns {Object|null} Element object
 606+ */
 607+es.DocumentModel.prototype.getElementFromNode = function( node ) {
 608+ var offset = this.getOffsetFromNode( node );
 609+ if ( offset !== false ) {
 610+ return this.data[offset];
 611+ }
 612+ return null;
 613+};
 614+
 615+/**
 616+ * Gets the content data of a node.
 617+ *
 618+ * @method
 619+ * @param {es.DocumentModelNode} node Node to get content data for
 620+ * @returns {Array|null} List of content and elements inside node or null if node is not found
 621+ */
 622+es.DocumentModel.prototype.getContentFromNode = function( node, range ) {
 623+ var length = node.getContentLength();
 624+ if ( range ) {
 625+ range.normalize();
 626+ if ( range.start < 0 ) {
 627+ throw 'Invalid range error. Range can not start before node start: ' + range.start;
 628+ }
 629+ if ( range.end > length ) {
 630+ throw 'Invalid range error. Range can not end after node end: ' + range.end;
 631+ }
 632+ } else {
 633+ range = {
 634+ 'start': 0,
 635+ 'end': length
 636+ };
 637+ }
 638+ var offset = this.getOffsetFromNode( node );
 639+ if ( offset !== -1 ) {
 640+ offset++;
 641+ return this.data.slice( offset + range.start, offset + range.end );
 642+ }
 643+ return null;
 644+};
 645+
 646+/**
 647+ * Gets the range of content surrounding a given offset that's covered by a given annotation.
 648+ *
 649+ * @method
 650+ * @param {Integer} offset Offset to begin looking forward and backward from
 651+ * @param {Object} annotation Annotation to test for coverage with
 652+ * @returns {es.Range|null} Range of content covered by annotation, or null if offset is not covered
 653+ */
 654+es.DocumentModel.prototype.getAnnotationBoundaries = function( offset, annotation ) {
 655+ if ( annotation.hash === undefined ) {
 656+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 657+ }
 658+ if ( es.DocumentModel.getIndexOfAnnotation( this.data[offset], annotation ) === -1 ) {
 659+ return null;
 660+ }
 661+ var start = offset,
 662+ end = offset,
 663+ item;
 664+ while ( start > 0 ) {
 665+ start--;
 666+ if ( es.DocumentModel.getIndexOfAnnotation( this.data[start], annotation ) === -1 ) {
 667+ start++;
 668+ break;
 669+ }
 670+ }
 671+ while ( end < this.data.length ) {
 672+ if ( es.DocumentModel.getIndexOfAnnotation( this.data[end], annotation ) === -1 ) {
 673+ break;
 674+ }
 675+ end++;
 676+ }
 677+ return new es.Range( start, end );
 678+};
 679+
 680+/**
 681+ * Gets a list of annotations that a given offset is covered by.
 682+ *
 683+ * @method
 684+ * @param {Integer} offset Offset to get annotations for
 685+ * @returns {Object[]} A copy of all annotation objects offset is covered by
 686+ */
 687+es.DocumentModel.prototype.getAnnotationsFromOffset = function( offset ) {
 688+ if ( es.isArray( this.data[offset] ) ) {
 689+ return es.copyArray( this.data[offset].slice( 1 ) );
 690+ }
 691+ return [];
 692+};
 693+
 694+/**
 695+ * Gets the range of content surrounding a given offset that makes up a whole word.
 696+ *
 697+ * @method
 698+ * @param {Integer} offset Offset to begin looking forward and backward from
 699+ * @returns {es.Range|null} Range of content making up a whole word or null if offset is not content
 700+ */
 701+es.DocumentModel.prototype.getWordBoundaries = function( offset ) {
 702+ if ( es.DocumentModel.isStructuralOffset( this.data, offset ) ) {
 703+ return null;
 704+ }
 705+ var start = offset,
 706+ end = offset,
 707+ item;
 708+ while ( start > 0 ) {
 709+ start--;
 710+ if ( typeof this.data[start] !== 'string' && !es.isArray( this.data[start] ) ) {
 711+ start++;
 712+ break;
 713+ }
 714+ item = typeof this.data[start] === 'string' ? this.data[start] : this.data[start][0];
 715+ if ( item.match( /\B/ ) ) {
 716+ start++;
 717+ break;
 718+ }
 719+ }
 720+ while ( end < this.data.length ) {
 721+ if ( typeof this.data[end] !== 'string' && !es.isArray( this.data[end] ) ) {
 722+ break;
 723+ }
 724+ item = typeof this.data[end] === 'string' ? this.data[end] : this.data[end][0];
 725+ if ( item.match( /\B/ ) ) {
 726+ break;
 727+ }
 728+ end++;
 729+ }
 730+ return new es.Range( start, end );
 731+};
 732+
 733+/**
 734+ * Gets a content offset a given distance forwards or backwards from another.
 735+ *
 736+ * @method
 737+ * @param {Integer} offset Offset to start from
 738+ * @param {Integer} distance Number of content offsets to move
 739+ * @param {Integer} Offset a given distance from the given offset
 740+ */
 741+es.DocumentModel.prototype.getRelativeContentOffset = function( offset, distance ) {
 742+ if ( es.DocumentModel.isStructuralOffset( this.data, offset ) ) {
 743+ throw 'Invalid offset error. Can not get relative content offset from non-content offset.';
 744+ }
 745+ if ( distance === 0 ) {
 746+ return offset;
 747+ }
 748+ var direction = distance > 0 ? 1 : -1,
 749+ i = offset + direction,
 750+ steps = 0;
 751+ distance = Math.abs( distance );
 752+ while ( i > 0 && i < this.data.length - 1 ) {
 753+ if ( !es.DocumentModel.isStructuralOffset( this.data, i ) ) {
 754+ steps++;
 755+ offset = i;
 756+ if ( distance === steps ) {
 757+ return offset;
 758+ }
 759+ }
 760+ i += direction;
 761+ }
 762+ return offset;
 763+};
 764+
 765+/**
 766+ * Generates a transaction which inserts data at a given offset.
 767+ *
 768+ * @method
 769+ * @param {Integer} offset
 770+ * @param {Array} data
 771+ * @returns {es.Transaction}
 772+ */
 773+es.DocumentModel.prototype.prepareInsertion = function( offset, data ) {
 774+ /**
 775+ * Balances mismatched openings/closings in data
 776+ * @return data itself if nothing was changed, or a clone of data with balancing changes made.
 777+ * data itself is never touched
 778+ */
 779+ function balance( data ) {
 780+ var i, stack = [], element, workingData = null;
 781+
 782+ for ( i = 0; i < data.length; i++ ) {
 783+ if ( data[i].type === undefined ) {
 784+ // Not an opening or a closing, skip
 785+ } else if ( data[i].type.charAt( 0 ) != '/' ) {
 786+ // Opening
 787+ stack.push( data[i].type );
 788+ } else {
 789+ // Closing
 790+ if ( stack.length === 0 ) {
 791+ // The stack is empty, so this is an unopened closing
 792+ // Remove it
 793+ if ( workingData === null ) {
 794+ workingData = data.slice( 0 );
 795+ }
 796+ workingData.splice( i, 1 );
 797+ } else {
 798+ element = stack.pop();
 799+ if ( element != data[i].type.substr( 1 ) ) {
 800+ // Closing doesn't match what's expected
 801+ // This means the input is malformed and cannot possibly
 802+ // have been a fragment taken from well-formed data
 803+ throw 'Input is malformed: expected /' + element + ' but got ' + data[i].type +
 804+ ' at index ' + i;
 805+ }
 806+ }
 807+ }
 808+ }
 809+
 810+ // Check whether there are any unclosed tags and close them
 811+ if ( stack.length > 0 && workingData === null ) {
 812+ workingData = data.slice( 0 );
 813+ }
 814+ while ( stack.length > 0 ) {
 815+ element = stack.pop();
 816+ workingData.push( { 'type': '/' + element } );
 817+ }
 818+
 819+ // TODO
 820+ // Check whether there is any raw unenclosed content and deal with that somehow
 821+
 822+ return workingData || data;
 823+ }
 824+
 825+ var tx = new es.Transaction(),
 826+ insertedData = data, // may be cloned and modified
 827+ isStructuralLoc,
 828+ wrappingElementType;
 829+
 830+ if ( offset < 0 || offset > this.data.length ) {
 831+ throw 'Offset ' + offset + ' out of bounds [0..' + this.data.length + ']';
 832+ }
 833+
 834+ // Has to be after the bounds check, because isStructuralOffset doesn't like out-of-bounds offsets
 835+ isStructuralLoc = es.DocumentModel.isStructuralOffset( this.data, offset );
 836+
 837+ if ( offset > 0 ) {
 838+ tx.pushRetain( offset );
 839+ }
 840+
 841+ if ( es.DocumentModel.containsElementData( insertedData ) ) {
 842+ if ( insertedData[0].type !== undefined && insertedData[0].type.charAt( 0 ) != '/' ) {
 843+ // insertedData starts with an opening, so this is really intended to insert structure
 844+ // Balance it to make it sane, if it's not already
 845+ // TODO we need an actual validator and check that the insertion is really valid
 846+ insertedData = balance( insertedData );
 847+ if ( !isStructuralLoc ) {
 848+ // We're inserting structure at a content location,
 849+ // so we need to split up the wrapping element
 850+ wrappingElementType = this.getNodeFromOffset( offset ).getElementType();
 851+ var arr = [ { 'type': '/' + wrappingElementType }, { 'type': wrappingElementType } ];
 852+ es.insertIntoArray( arr, 1, insertedData );
 853+ insertedData = arr;
 854+ }
 855+ // else we're inserting structure at a structural location, which is fine
 856+ } else {
 857+ // insertedData starts with content but contains structure
 858+ // TODO balance and validate, will be different for this case
 859+ }
 860+ } else {
 861+ if ( isStructuralLoc ) {
 862+ // We're inserting content into a structural location,
 863+ // so we need to wrap the inserted content in a paragraph.
 864+ insertedData = [ { 'type': 'paragraph' }, { 'type': '/paragraph' } ];
 865+ es.insertIntoArray( insertedData, 1, data );
 866+ } else {
 867+ // Content being inserted in content is fine, do nothing
 868+ }
 869+ }
 870+
 871+ tx.pushInsert( insertedData );
 872+ if ( offset < this.data.length ) {
 873+ tx.pushRetain( this.data.length - offset );
 874+ }
 875+
 876+ return tx;
 877+
 878+ /*
 879+ * // Structural changes
 880+ * There are 2 basic types of locations the insertion point can be:
 881+ * Structural locations
 882+ * |<p>a</p><p>b</p> - Beginning of the document
 883+ * <p>a</p>|<p>b</p> - Between elements (like in a document or list)
 884+ * <p>a</p><p>b</p>| - End of the document
 885+ * Content locations
 886+ * <p>|a</p><p>b</p> - Inside an element (like in a paragraph or listItem)
 887+ * <p>a|</p><p>b</p> - May also be inside an element but right before/after an
 888+ * open/close
 889+ *
 890+ * if ( Incoming data contains structural elements ) {
 891+ // We're assuming the incoming data is balanced, is that OK?
 892+ * if ( Insertion point is a structural location ) {
 893+ * if ( Incoming data is not a complete structural element ) {
 894+ * Incoming data must be balanced
 895+ * }
 896+ * } else {
 897+ * Closing and opening elements for insertion point must be added to incoming data
 898+ * }
 899+ * } else {
 900+ * if ( Insertion point is a structural location ) {
 901+ * Incoming data must be balanced //how? Should this even be allowed?
 902+ * } else {
 903+ * Content being inserted into content is OK, do nothing
 904+ * }
 905+ * }
 906+ */
 907+};
 908+
 909+/**
 910+ * Generates a transaction which removes data from a given range.
 911+ *
 912+ * When removing data inside an element, the data is simply discarded and the node's length is
 913+ * adjusted accordingly. When removing data across elements, there are two situations that can cause
 914+ * added complexity:
 915+ * 1. A range spans between nodes of different levels or types
 916+ * 2. A range only partially covers one or two nodes
 917+ *
 918+ * To resolve these issues in a predictable way the following rules must be obeyed:
 919+ * 1. Structural elements are retained unless the range being removed covers the entire element
 920+ * 2. Elements can only be merged if they are of the same time and share a common parent
 921+ *
 922+ * @method
 923+ * @param {es.Range} range
 924+ * @returns {es.Transaction}
 925+ */
 926+es.DocumentModel.prototype.prepareRemoval = function( range ) {
 927+ var doc = this;
 928+ //debugger;
 929+
 930+ /**
 931+ * Return true if can merge the remaining contents of the elements after a selection is deleted
 932+ * across them. For instance, if a selection is painted across two paragraphs, and then the text
 933+ * is deleted, the two paragraphs can become one paragraph. However, if the selection crosses
 934+ * into a table, those cannot be merged.
 935+ * @param {Number} integer offset
 936+ * @param {Number} integer offset
 937+ * @return {Boolean}
 938+ */
 939+ function canMerge( range ) {
 940+ var node1 = doc.getNodeFromOffset( range.start );
 941+ var node2 = doc.getNodeFromOffset( range.end - 1 );
 942+ // This is the simple rule we are following for now -- same type & same parent = can merge.
 943+ // So you can merge adjacent paragraphs, or listitems. And you can't merge a paragraph into
 944+ // a table row. There may be other rules we will want in here later, for instance, special
 945+ // casing merging a listitem into a paragraph.
 946+
 947+ // wait, some nodes don't have types? Is this the top document node?
 948+ return (
 949+ (
 950+ ( node1 && node2 ) &&
 951+ ( node1.type !== undefined && node2.type !== undefined ) &&
 952+ ( node1.type === node2.type )
 953+ ) &&
 954+ ( node1.getParent() === node2.getParent() )
 955+ );
 956+ }
 957+
 958+ function mergeDelete( range, tx ) {
 959+ // yay, content can be removed in one fell swoop
 960+ var removed = doc.data.slice( range.start, range.end );
 961+ tx.pushRemove( removed );
 962+ }
 963+
 964+ // remove string content only, retain structure
 965+ function stripDelete( range, tx ) {
 966+ var lastOperation, operationStart;
 967+
 968+ var ops = [],
 969+ op;
 970+ //debugger;
 971+
 972+ // get a list of operations, with 0-based indexes
 973+ for (var i = range.start; i < range.end; i++ ) {
 974+ var neededOp = doc.data[i].type === undefined ? 'remove' : 'retain';
 975+ op = ops[ ops.length - 1 ];
 976+ if ( op === undefined || op.type !== neededOp ) {
 977+ ops.push( { type: neededOp, start: i, end: i } );
 978+ } else {
 979+ op.end = i;
 980+ }
 981+ }
 982+
 983+ //debugger;
 984+ // insert operations as transactions (end must be adjusted)
 985+ for (var j = 0; j < ops.length; j++ ) {
 986+ op = ops[j];
 987+ if ( op.type === 'retain' ) {
 988+ // we add one because retain(3,3) really means retain 1 char at pos 3
 989+ tx.pushRetain( op.end - op.start + 1 );
 990+ } else if ( op.type === 'remove' ) {
 991+ // we add one because to remove(3,5) we need to slice(3,6), the ending is last
 992+ // subscript removed + 1.
 993+ tx.pushRemove( this.data.slice( op.start, op.end + 1 ) );
 994+ } else {
 995+ console.log( "this is impossible" );
 996+ }
 997+ }
 998+
 999+ }
 1000+
 1001+
 1002+ var tx = new es.Transaction();
 1003+ range.normalize();
 1004+
 1005+ // Retain to the start of the range
 1006+ if ( range.start > 0 ) {
 1007+ tx.pushRetain( range.start );
 1008+ }
 1009+
 1010+
 1011+ // choose a deletion strategy; merging nodes together, or stripping content from existing
 1012+ // structure.
 1013+ if ( canMerge( range ) ) {
 1014+ mergeDelete( range, tx );
 1015+ } else {
 1016+ stripDelete( range, tx );
 1017+ }
 1018+
 1019+ // Retain up to the end of the document. Why do we do this?
 1020+ if ( range.end < doc.data.length ) {
 1021+ tx.pushRetain( doc.data.length - range.end );
 1022+ }
 1023+
 1024+ return tx;
 1025+};
 1026+
 1027+/**
 1028+ * Generates a transaction which annotates content within a given range.
 1029+ *
 1030+ * @method
 1031+ * @returns {es.Transaction}
 1032+ */
 1033+es.DocumentModel.prototype.prepareContentAnnotation = function( range, method, annotation ) {
 1034+ var tx = new es.Transaction();
 1035+ range.normalize();
 1036+ if ( annotation.hash === undefined ) {
 1037+ annotation.hash = es.DocumentModel.getAnnotationHash( annotation );
 1038+ }
 1039+ var i = range.start,
 1040+ span = i,
 1041+ on = this.data[i].type !== undefined;
 1042+ while ( i < range.end ) {
 1043+ if ( this.data[i].type !== undefined ) {
 1044+ // Don't annotate structural elements
 1045+ if ( on ) {
 1046+ tx.pushStopAnnotating( method, annotation );
 1047+ span = 0;
 1048+ on = false;
 1049+ }
 1050+ } else {
 1051+ var covered = es.DocumentModel.getIndexOfAnnotation( this.data[i], annotation ) !== -1;
 1052+ if ( covered && method === 'set' || !covered && method === 'clear' ) {
 1053+ // Don't set/clear annotations on content that's already set/cleared
 1054+ if ( on ) {
 1055+ if ( span ) {
 1056+ tx.pushRetain( span );
 1057+ }
 1058+ tx.pushStopAnnotating( method, annotation );
 1059+ span = 0;
 1060+ on = false;
 1061+ }
 1062+ } else {
 1063+ // Content
 1064+ if ( !on ) {
 1065+ if ( span ) {
 1066+ tx.pushRetain( span );
 1067+ }
 1068+ tx.pushStartAnnotating( method, annotation );
 1069+ span = 0;
 1070+ on = true;
 1071+ }
 1072+ }
 1073+ }
 1074+ span++;
 1075+ i++;
 1076+ }
 1077+ if ( on ) {
 1078+ if ( span ) {
 1079+ tx.pushRetain( span );
 1080+ }
 1081+ tx.pushStopAnnotating( method, annotation );
 1082+ }
 1083+ if ( range.end < this.data.length ) {
 1084+ tx.pushRetain( this.data.length - range.end );
 1085+ }
 1086+ return tx;
 1087+};
 1088+
 1089+/**
 1090+ * Generates a transaction which changes attributes on an element at a given offset.
 1091+ *
 1092+ * @method
 1093+ * @returns {es.Transaction}
 1094+ */
 1095+es.DocumentModel.prototype.prepareElementAttributeChange = function( offset, method, key, value ) {
 1096+ var tx = new es.Transaction();
 1097+ if ( offset ) {
 1098+ tx.pushRetain( offset );
 1099+ }
 1100+ if ( this.data[offset].type === undefined ) {
 1101+ throw 'Invalid element offset error. Can not set attributes to non-element data.';
 1102+ }
 1103+ if ( this.data[offset].type[0] === '/' ) {
 1104+ throw 'Invalid element offset error. Can not set attributes on closing element.';
 1105+ }
 1106+ tx.pushChangeElementAttribute( method, key, value );
 1107+ if ( offset < this.data.length ) {
 1108+ tx.pushRetain( this.data.length - offset );
 1109+ }
 1110+ return tx;
 1111+};
 1112+
 1113+/**
 1114+ * Applies a transaction to the content data.
 1115+ *
 1116+ * @method
 1117+ * @param {es.Transaction}
 1118+ */
 1119+es.DocumentModel.prototype.commit = function( transaction ) {
 1120+ var state = {
 1121+ 'data': this.data,
 1122+ 'tree': this,
 1123+ 'cursor': 0,
 1124+ 'set': [],
 1125+ 'clear': [],
 1126+ 'rebuild': []
 1127+ },
 1128+ operations = transaction.getOperations();
 1129+ for ( var i = 0, length = operations.length; i < length; i++ ) {
 1130+ var operation = operations[i];
 1131+ if ( operation.type in es.DocumentModel.operations ) {
 1132+ es.DocumentModel.operations[operation.type].commit.call( state, operation );
 1133+ } else {
 1134+ throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
 1135+ }
 1136+ }
 1137+ // TODO: Synchronize op.tree - insert elements and adjust lengths
 1138+};
 1139+
 1140+/**
 1141+ * Reverses a transaction's effects on the content data.
 1142+ *
 1143+ * @method
 1144+ * @param {es.Transaction}
 1145+ */
 1146+es.DocumentModel.prototype.rollback = function( transaction ) {
 1147+ var state = {
 1148+ 'data': this.data,
 1149+ 'tree': this,
 1150+ 'cursor': 0,
 1151+ 'set': [],
 1152+ 'clear': [],
 1153+ 'rebuild': []
 1154+ },
 1155+ operations = transaction.getOperations();
 1156+ for ( var i = 0, length = operations.length; i < length; i++ ) {
 1157+ var operation = operations[i];
 1158+ if ( operation.type in es.DocumentModel.operations ) {
 1159+ es.DocumentModel.operations[operation.type].rollback.call( state, operation );
 1160+ } else {
 1161+ throw 'Invalid operation error. Operation type is not supported: ' + operation.type;
 1162+ }
 1163+ }
 1164+ // TODO: Synchronize op.tree - insert elements and adjust lengths
 1165+};
 1166+
 1167+/* Inheritance */
 1168+
 1169+es.extendClass( es.DocumentModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.HeadingModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.HeadingModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {Integer} length Length of document data element
 10+ */
 11+es.HeadingModel = function( element, length ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, length );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a heading view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.ParagraphView}
 23+ */
 24+es.HeadingModel.prototype.createView = function() {
 25+ return new es.HeadingView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.heading = es.HeadingModel;
 31+
 32+es.DocumentModel.nodeRules.heading = {
 33+ 'parents': null,
 34+ 'children': []
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.HeadingModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.TableRowModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.TableRowModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {es.DocumentModelNode[]} contents List of child nodes to initially add
 10+ */
 11+es.TableRowModel = function( element, contents ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, contents );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a table row view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.TableRowView}
 23+ */
 24+es.TableRowModel.prototype.createView = function() {
 25+ return new es.TableRowView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.tableRow = es.TableRowModel;
 31+
 32+es.DocumentModel.nodeRules.listItem = {
 33+ 'parents': ['table'],
 34+ 'children': ['tableCell']
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.TableRowModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.ParagraphModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.ParagraphModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {Integer} length Length of document data element
 10+ */
 11+es.ParagraphModel = function( element, length ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, length );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a paragraph view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.ParagraphView}
 23+ */
 24+es.ParagraphModel.prototype.createView = function() {
 25+ return new es.ParagraphView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.paragraph = es.ParagraphModel;
 31+
 32+es.DocumentModel.nodeRules.paragraph = {
 33+ 'parents': null,
 34+ 'children': []
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.ParagraphModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.TableCellModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.TableCellModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {es.DocumentModelNode[]} contents List of child nodes to initially add
 10+ */
 11+es.TableCellModel = function( element, contents ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, contents );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a table cell view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.TableCellView}
 23+ */
 24+es.TableCellModel.prototype.createView = function() {
 25+ return new es.TableCellView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.tableCell = es.TableCellModel;
 31+
 32+es.DocumentModel.nodeRules.listItem = {
 33+ 'parents': ['tableRow'],
 34+ 'children': null
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.TableCellModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.TableModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.TableModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {es.DocumentModelNode[]} contents List of child nodes to initially add
 10+ */
 11+es.TableModel = function( element, contents ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, contents );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a table view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.TableView}
 23+ */
 24+es.TableModel.prototype.createView = function() {
 25+ return new es.TableView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.table = es.TableModel;
 31+
 32+es.DocumentModel.nodeRules.table = {
 33+ 'parents': null,
 34+ 'children': ['tableRow']
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.TableModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.SurfaceModel.js
@@ -0,0 +1,16 @@
 2+/**
 3+ * Creates an es.SurfaceModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @param {es.DocumentModel} doc Document model to create surface for
 8+ */
 9+es.SurfaceModel = function( doc ) {
 10+ this.doc = doc;
 11+};
 12+
 13+/* Methods */
 14+
 15+es.SurfaceModel.prototype.getDocument = function() {
 16+ return this.doc;
 17+};
Index: trunk/extensions/VisualEditor/modules/models/es.ListItemModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.ListItemModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {Integer} length Length of document data element
 10+ */
 11+es.ListItemModel = function( element, length ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, length );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a list item view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.ListItemView}
 23+ */
 24+es.ListItemModel.prototype.createView = function() {
 25+ return new es.ListItemView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.listItem = es.ListItemModel;
 31+
 32+es.DocumentModel.nodeRules.listItem = {
 33+ 'parents': ['list'],
 34+ 'children': []
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.ListItemModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/models/es.ListModel.js
@@ -0,0 +1,38 @@
 2+/**
 3+ * Creates an es.ListModel object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentModelNode}
 8+ * @param {Object} element Document data element of this node
 9+ * @param {es.DocumentModelNode[]} contents List of child nodes to initially add
 10+ */
 11+es.ListModel = function( element, contents ) {
 12+ // Inheritance
 13+ es.DocumentModelNode.call( this, element, contents );
 14+};
 15+
 16+/* Methods */
 17+
 18+/**
 19+ * Creates a list view for this model.
 20+ *
 21+ * @method
 22+ * @returns {es.ListView}
 23+ */
 24+es.ListModel.prototype.createView = function() {
 25+ return new es.ListView( this );
 26+};
 27+
 28+/* Registration */
 29+
 30+es.DocumentModel.nodeModels.list = es.ListModel;
 31+
 32+es.DocumentModel.nodeRules.list = {
 33+ 'parents': null,
 34+ 'children': ['listItem']
 35+};
 36+
 37+/* Inheritance */
 38+
 39+es.extendClass( es.ListModel, es.DocumentModelNode );
Index: trunk/extensions/VisualEditor/modules/es.Position.js
@@ -0,0 +1,218 @@
 2+/**
 3+ * Pixel position.
 4+ *
 5+ * This can also support an optional bottom field, to represent a vertical line, such as a cursor.
 6+ *
 7+ * @class
 8+ * @constructor
 9+ * @param left {Integer} Horizontal position
 10+ * @param top {Integer} Vertical top position
 11+ * @param bottom {Integer} Vertical bottom position of bottom (optional, default: top)
 12+ * @property left {Integer} Horizontal position
 13+ * @property top {Integer} Vertical top position
 14+ * @property bottom {Integer} Vertical bottom position of bottom
 15+ */
 16+es.Position = function( left, top, bottom ) {
 17+ this.left = left || 0;
 18+ this.top = top || 0;
 19+ this.bottom = bottom || this.top;
 20+};
 21+
 22+/* Static Methods */
 23+
 24+/**
 25+ * Creates position object from the page position of an element.
 26+ *
 27+ * @static
 28+ * @method
 29+ * @param $element {jQuery} Element to get offset from
 30+ * @returns {es.Position} Position with element data applied
 31+ */
 32+es.Position.newFromElementPagePosition = function( $element ) {
 33+ var offset = $element.offset();
 34+ return new es.Position( offset.left, offset.top );
 35+};
 36+
 37+/**
 38+ * Creates position object from the layer position of an element.
 39+ *
 40+ * @static
 41+ * @method
 42+ * @param $element {jQuery} Element to get position from
 43+ * @returns {es.Position} Position with element data applied
 44+ */
 45+es.Position.newFromElementLayerPosition = function( $element ) {
 46+ var position = $element.position();
 47+ return new es.Position( position.left, position.top );
 48+};
 49+
 50+/**
 51+ * Creates position object from the screen position data in an Event object.
 52+ *
 53+ * @static
 54+ * @method
 55+ * @param event {Event} Event to get position data from
 56+ * @returns {es.Position} Position with event data applied
 57+ */
 58+es.Position.newFromEventScreenPosition = function( event ) {
 59+ return new es.Position( event.screenX, event.screenY );
 60+};
 61+
 62+/**
 63+ * Creates position object from the page position data in an Event object.
 64+ *
 65+ * @static
 66+ * @method
 67+ * @param event {Event} Event to get position data from
 68+ * @returns {es.Position} Position with event data applied
 69+ */
 70+es.Position.newFromEventPagePosition = function( event ) {
 71+ return new es.Position( event.pageX, event.pageY );
 72+};
 73+
 74+/**
 75+ * Creates position object from the layer position data in an Event object.
 76+ *
 77+ * @static
 78+ * @method
 79+ * @param event {Event} Event to get position data from
 80+ * @returns {es.Position} Position with event data applied
 81+ */
 82+es.Position.newFromEventLayerPosition = function( event ) {
 83+ return new es.Position( event.layerX, event.layerY );
 84+};
 85+
 86+/* Methods */
 87+
 88+/**
 89+ * Adds the values of a given position to this one.
 90+ *
 91+ * @method
 92+ * @param position {es.Position} Position to add values from
 93+ */
 94+es.Position.prototype.add = function( position ) {
 95+ this.top += position.top;
 96+ this.bottom += position.bottom;
 97+ this.left += position.left;
 98+};
 99+
 100+/**
 101+ * Subtracts the values of a given position to this one.
 102+ *
 103+ * @method
 104+ * @param position {es.Position} Position to subtract values from
 105+ */
 106+es.Position.prototype.subtract = function( position ) {
 107+ this.top -= position.top;
 108+ this.bottom -= position.bottom;
 109+ this.left -= position.left;
 110+};
 111+
 112+/**
 113+ * Checks if this position is the same as another one.
 114+ *
 115+ * @method
 116+ * @param position {es.Position} Position to compare with
 117+ * @returns {Boolean} If positions have the same left and top values
 118+ */
 119+es.Position.prototype.at = function( position ) {
 120+ return this.left === position.left && this.top === position.top;
 121+};
 122+
 123+/**
 124+ * Checks if this position perpendicular with another one, sharing either a top or left value.
 125+ *
 126+ * @method
 127+ * @param position {es.Position} Position to compare with
 128+ * @returns {Boolean} If positions share a top or a left value
 129+ */
 130+es.Position.prototype.perpendicularWith = function( position ) {
 131+ return this.left === position.left || this.top === position.top;
 132+};
 133+
 134+/**
 135+ * Checks if this position is level with another one, having the same top value.
 136+ *
 137+ * @method
 138+ * @param position {es.Position} Position to compare with
 139+ * @returns {Boolean} If positions have the same top value
 140+ */
 141+es.Position.prototype.levelWith = function( position ) {
 142+ return this.top === position.top;
 143+};
 144+
 145+/**
 146+ * Checks if this position is plumb with another one, having the same left value.
 147+ *
 148+ * @method
 149+ * @param position {es.Position} Position to compare with
 150+ * @returns {Boolean} If positions have the same left value
 151+ */
 152+es.Position.prototype.plumbWith = function( position ) {
 153+ return this.left === position.left;
 154+};
 155+
 156+/**
 157+ * Checks if this position is nearby another one.
 158+ *
 159+ * Distance is measured radially.
 160+ *
 161+ * @method
 162+ * @param position {es.Position} Position to compare with
 163+ * @param radius {Integer} Pixel distance from this position to consider "near-by"
 164+ * @returns {Boolean} If positions are near-by each other
 165+ */
 166+es.Position.prototype.near = function( position, radius ) {
 167+ return Math.sqrt(
 168+ Math.pow( this.left - position.left, 2 ),
 169+ Math.pow( this.top - position.top )
 170+ ) <= radius;
 171+};
 172+
 173+/**
 174+ * Checks if this position is above another one.
 175+ *
 176+ * This method utilizes the bottom property.
 177+ *
 178+ * @method
 179+ * @param position {es.Position} Position to compare with
 180+ * @returns {Boolean} If this position is above the other
 181+ */
 182+es.Position.prototype.above = function( position ) {
 183+ return this.bottom < position.top;
 184+};
 185+
 186+/**
 187+ * Checks if this position is below another one.
 188+ *
 189+ * This method utilizes the bottom property.
 190+ *
 191+ * @method
 192+ * @param position {es.Position} Position to compare with
 193+ * @returns {Boolean} If this position is below the other
 194+ */
 195+es.Position.prototype.below = function( position ) {
 196+ return this.top > position.bottom;
 197+};
 198+
 199+/**
 200+ * Checks if this position is to the left of another one.
 201+ *
 202+ * @method
 203+ * @param position {es.Position} Position to compare with
 204+ * @returns {Boolean} If this position is the left the other
 205+ */
 206+es.Position.prototype.leftOf = function( left ) {
 207+ return this.left < left;
 208+};
 209+
 210+/**
 211+ * Checks if this position is to the right of another one.
 212+ *
 213+ * @method
 214+ * @param position {es.Position} Position to compare with
 215+ * @returns {Boolean} If this position is the right the other
 216+ */
 217+es.Position.prototype.rightOf = function( left ) {
 218+ return this.left > left;
 219+};
Index: trunk/extensions/VisualEditor/modules/es.Surface.css
@@ -0,0 +1,222 @@
 2+.es-surfaceView {
 3+ overflow: hidden;
 4+}
 5+
 6+.es-surfaceView-input {
 7+ position: absolute;
 8+ z-index: -1;
 9+ opacity: 0;
 10+ color: white;
 11+ background-color: white;
 12+ border: none;
 13+ padding: 0;
 14+ margin: 0;
 15+ width: 1px;
 16+}
 17+
 18+.es-surfaceView-input:focus {
 19+ outline: none;
 20+}
 21+
 22+.es-surfaceView-cursor {
 23+ position: absolute;
 24+ background-color: black;
 25+ width: 1px;
 26+ height: 1.5em;
 27+ min-height: 1.5em;
 28+ display: none;
 29+}
 30+
 31+.es-documentView {
 32+ cursor: text;
 33+ margin-top: 1em;
 34+ overflow: hidden;
 35+ -webkit-user-select: none;
 36+}
 37+
 38+.es-contentView {
 39+ position: relative;
 40+}
 41+
 42+.es-headingView,
 43+.es-tableView,
 44+.es-listView,
 45+.es-paragraphView {
 46+ margin: 1em;
 47+ margin-top: 0;
 48+ position: relative;
 49+ min-height: 1.5em;
 50+ font-size: 1em;
 51+}
 52+
 53+.es-headingView-level1,
 54+.es-headingView-level2 {
 55+ border-bottom: 1px solid #AAA;
 56+}
 57+
 58+.es-headingView-level1 > * {
 59+ font-size:188%;
 60+ font-weight: normal;
 61+}
 62+
 63+.es-headingView-level2 > * {
 64+ font-size:150%;
 65+ font-weight: normal;
 66+}
 67+
 68+.es-headingView-level3 > * {
 69+ font-size:132%;
 70+ font-weight: bold;
 71+}
 72+
 73+.es-headingView-level4 > * {
 74+ font-size:116%;
 75+ font-weight: bold;
 76+}
 77+
 78+.es-headingView-level5 > * {
 79+ font-size:100%;
 80+ font-weight: bold;
 81+}
 82+
 83+.es-headingView-level6 > * {
 84+ font-size:80%;
 85+ font-weight: bold;
 86+}
 87+
 88+.es-listItemView {
 89+ position: relative;
 90+ padding: 0 0 0 1em;
 91+ background-position: left 0.6em;
 92+ background-repeat: no-repeat;
 93+}
 94+
 95+.es-listItemView-bullet {
 96+ background-image: url(images/bullet.png);
 97+}
 98+
 99+.es-listItemView-number {
 100+ /* */
 101+}
 102+
 103+.es-listItemView-bullet .es-listItemView-icon {
 104+ display: none;
 105+}
 106+
 107+.es-listItemView-number .es-listItemView-icon {
 108+ position: absolute;
 109+ right: 100%;
 110+ margin-right: -0.5em;
 111+ height: 1.5em;
 112+ line-height: 1.5em;
 113+}
 114+
 115+.es-listItemView-level0 {
 116+ margin-left: 0.5em;
 117+}
 118+
 119+.es-listItemView-level1 {
 120+ margin-left: 2.5em;
 121+}
 122+
 123+.es-listItemView-level2 {
 124+ margin-left: 4.5em;
 125+}
 126+
 127+.es-listItemView-level3 {
 128+ margin-left: 6.5em;
 129+}
 130+
 131+.es-listItemView-level4 {
 132+ margin-left: 8.5em;
 133+}
 134+
 135+.es-listItemView-level5 {
 136+ margin-left: 10.5em;
 137+}
 138+
 139+.es-listItemView-level6 {
 140+ margin-left: 12.5em;
 141+}
 142+
 143+.es-contentView-line,
 144+.es-contentView-ruler {
 145+ line-height: 1.5em;
 146+ cursor: text;
 147+ white-space: nowrap;
 148+ color: #000000;
 149+}
 150+
 151+.es-contentView-ruler {
 152+ position: absolute;
 153+ top: 0;
 154+ left: 0;
 155+ display: inline-block;
 156+ z-index: -1000;
 157+}
 158+
 159+.es-contentView-line.empty {
 160+ display: block;
 161+ width: 0px;
 162+}
 163+
 164+.es-contentView-whitespace {
 165+ color: #ffffff;
 166+}
 167+
 168+.es-contentView-range {
 169+ display: none;
 170+ position: absolute;
 171+ background-color: #bbffcc;
 172+ cursor: text;
 173+ z-index: -1;
 174+}
 175+
 176+.es-contentView-format-object {
 177+ background-color: rgba(0,0,0,0.05);
 178+ border-radius: 0.25em;
 179+ margin: 1px 0 1px 1px;
 180+ padding: 0.25em 0;
 181+ cursor: default;
 182+}
 183+
 184+.es-contentView-format-object * {
 185+ cursor: default !important;
 186+}
 187+
 188+.es-contentView-format-object a:link,
 189+.es-contentView-format-object a:visited,
 190+.es-contentView-format-object a:active {
 191+ color: blue;
 192+}
 193+
 194+.es-contentView-format-italic {
 195+ font-style: italic;
 196+}
 197+
 198+.es-contentView-format-bold {
 199+ font-weight: bold;
 200+}
 201+
 202+.es-contentView-format-link {
 203+ color: blue;
 204+ text-decoration: underline;
 205+}
 206+
 207+.es-contentView-format-big {
 208+ font-size: 1.2em;
 209+}
 210+
 211+.es-contentView-format-small,
 212+.es-contentView-format-sub,
 213+.es-contentView-format-super {
 214+ font-size: .8em;
 215+}
 216+
 217+.es-contentView-format-sub {
 218+ vertical-align: sub;
 219+}
 220+
 221+.es-contentView-format-super {
 222+ vertical-align: super;
 223+}
Index: trunk/extensions/VisualEditor/modules/images/bullet.png
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Property changes on: trunk/extensions/VisualEditor/modules/images/bullet.png
___________________________________________________________________
Added: svn:mime-type
1224 + application/octet-stream
Index: trunk/extensions/VisualEditor/modules/es.Transaction.js
@@ -0,0 +1,64 @@
 2+/**
 3+ * Creates an es.Transaction object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @param {Object[]} operations List of operations
 8+ */
 9+es.Transaction = function( operations ) {
 10+ this.operations = es.isArray( operations ) ? operations : [];
 11+};
 12+
 13+/* Methods */
 14+
 15+es.Transaction.prototype.getOperations = function() {
 16+ return this.operations;
 17+};
 18+
 19+es.Transaction.prototype.pushRetain = function( length ) {
 20+ this.operations.push( {
 21+ 'type': 'retain',
 22+ 'length': length
 23+ } );
 24+};
 25+
 26+es.Transaction.prototype.pushInsert = function( content ) {
 27+ this.operations.push( {
 28+ 'type': 'insert',
 29+ 'data': content
 30+ } );
 31+};
 32+
 33+es.Transaction.prototype.pushRemove = function( data ) {
 34+ this.operations.push( {
 35+ 'type': 'remove',
 36+ 'data': data
 37+ } );
 38+};
 39+
 40+es.Transaction.prototype.pushChangeElementAttribute = function( method, key, value ) {
 41+ this.operations.push( {
 42+ 'type': 'attribute',
 43+ 'method': method,
 44+ 'key': key,
 45+ 'value': value
 46+ } );
 47+};
 48+
 49+es.Transaction.prototype.pushStartAnnotating = function( method, annotation ) {
 50+ this.operations.push( {
 51+ 'type': 'annotate',
 52+ 'method': method,
 53+ 'bias': 'start',
 54+ 'annotation': annotation
 55+ } );
 56+};
 57+
 58+es.Transaction.prototype.pushStopAnnotating = function( method, annotation ) {
 59+ this.operations.push( {
 60+ 'type': 'annotate',
 61+ 'method': method,
 62+ 'bias': 'stop',
 63+ 'annotation': annotation
 64+ } );
 65+};
Index: trunk/extensions/VisualEditor/modules/bases/es.DocumentViewNode.js
@@ -0,0 +1,204 @@
 2+/**
 3+ * Creates an es.DocumentViewNode object.
 4+ *
 5+ * es.DocumentViewNode extends native JavaScript Array objects, without changing Array.prototype by
 6+ * dynamically extending an array literal with the methods of es.DocumentViewNode.
 7+ *
 8+ * View nodes follow the operations performed on model nodes and keep elements in the DOM in sync.
 9+ *
 10+ * Child objects must extend es.DocumentViewNode.
 11+ *
 12+ * @class
 13+ * @constructor
 14+ * @extends {es.DocumentNode}
 15+ * @extends {es.EventEmitter}
 16+ * @param model {es.ModelNode} Model to observe
 17+ * @param {jQuery} [$element=New DIV element] Element to use as a container
 18+ * @property {es.ModelItem} model Model being observed
 19+ * @property {jQuery} $ Container element
 20+ */
 21+es.DocumentViewNode = function( model, $element ) {
 22+ // Inheritance
 23+ es.DocumentNode.call( this );
 24+ es.EventEmitter.call( this );
 25+
 26+ // Properties
 27+ this.model = model;
 28+ this.children = [];
 29+ this.$ = $element || $( '<div/>' );
 30+
 31+ // Reusable function for passing update events upstream
 32+ var _this = this;
 33+ this.emitUpdate = function() {
 34+ _this.emit( 'update' );
 35+ };
 36+
 37+ if ( model ) {
 38+ // Append existing model children
 39+ var childModels = model.getChildren();
 40+ for ( var i = 0; i < childModels.length; i++ ) {
 41+ this.onAfterPush( childModels[i] );
 42+ }
 43+
 44+ // Observe and mimic changes on model
 45+ this.addListenerMethods( this, {
 46+ 'afterPush': 'onAfterPush',
 47+ 'afterUnshift': 'onAfterUnshift',
 48+ 'afterPop': 'onAfterPop',
 49+ 'afterShift': 'onAfterShift',
 50+ 'afterSplice': 'onAfterSplice',
 51+ 'afterSort': 'onAfterSort',
 52+ 'afterReverse': 'onAfterReverse'
 53+ } );
 54+ }
 55+};
 56+
 57+es.DocumentViewNode.prototype.onAfterPush = function( childModel ) {
 58+ var childView = childModel.createView();
 59+ this.emit( 'beforePush', childView );
 60+ childView.attach( this );
 61+ childView.on( 'update', this.emitUpdate );
 62+ // Update children
 63+ this.children.push( childView );
 64+ // Update DOM
 65+ this.$.append( childView.$ );
 66+ this.emit( 'afterPush', childView );
 67+ this.emit( 'update' );
 68+};
 69+
 70+es.DocumentViewNode.prototype.onAfterUnshift = function( childModel ) {
 71+ var childView = childModel.createView();
 72+ this.emit( 'beforeUnshift', childView );
 73+ childView.attach( this );
 74+ childView.on( 'update', this.emitUpdate );
 75+ // Update children
 76+ this.children.unshift( childView );
 77+ // Update DOM
 78+ this.$.prepend( childView.$ );
 79+ this.emit( 'afterUnshift', childView );
 80+ this.emit( 'update' );
 81+};
 82+
 83+es.DocumentViewNode.prototype.onAfterPop = function() {
 84+ this.emit( 'beforePop' );
 85+ // Update children
 86+ var childView = this.children.pop();
 87+ childView.detach();
 88+ childView.removeEventListener( 'update', this.emitUpdate );
 89+ // Update DOM
 90+ childView.$.detach();
 91+ this.emit( 'afterPop' );
 92+ this.emit( 'update' );
 93+};
 94+
 95+es.DocumentViewNode.prototype.onAfterShift = function() {
 96+ this.emit( 'beforeShift' );
 97+ // Update children
 98+ var childView = this.children.shift();
 99+ childView.detach();
 100+ childView.removeEventListener( 'update', this.emitUpdate );
 101+ // Update DOM
 102+ childView.$.detach();
 103+ this.emit( 'afterShift' );
 104+ this.emit( 'update' );
 105+};
 106+
 107+es.DocumentViewNode.prototype.onAfterSplice = function( index, howmany ) {
 108+ var args = Array.prototype.slice( arguments, 0 );
 109+ this.emit.apply( ['beforeSplice'].concat( args ) );
 110+ // Update children
 111+ this.splice.apply( this, args );
 112+ // Update DOM
 113+ this.$.children()
 114+ // Removals
 115+ .slice( index, index + howmany )
 116+ .detach()
 117+ .end()
 118+ // Insertions
 119+ .get( index )
 120+ .after( $.map( args.slice( 2 ), function( childView ) {
 121+ return childView.$;
 122+ } ) );
 123+ this.emit.apply( ['afterSplice'].concat( args ) );
 124+ this.emit( 'update' );
 125+};
 126+
 127+es.DocumentViewNode.prototype.onAfterSort = function() {
 128+ this.emit( 'beforeSort' );
 129+ var childModels = this.model.getChildren();
 130+ for ( var i = 0; i < childModels.length; i++ ) {
 131+ for ( var j = 0; j < this.children.length; j++ ) {
 132+ if ( this.children[j].getModel() === childModels[i] ) {
 133+ var childView = this.children[j];
 134+ // Update children
 135+ this.children.splice( j, 1 );
 136+ this.children.push( childView );
 137+ // Update DOM
 138+ this.$.append( childView.$ );
 139+ }
 140+ }
 141+ }
 142+ this.emit( 'afterSort' );
 143+ this.emit( 'update' );
 144+};
 145+
 146+es.DocumentViewNode.prototype.onAfterReverse = function() {
 147+ this.emit( 'beforeReverse' );
 148+ // Update children
 149+ this.reverse();
 150+ // Update DOM
 151+ this.$.children().each( function() {
 152+ $(this).prependTo( $(this).parent() );
 153+ } );
 154+ this.emit( 'afterReverse' );
 155+ this.emit( 'update' );
 156+};
 157+
 158+/**
 159+ * Gets a reference to the model this node observes.
 160+ *
 161+ * @method
 162+ * @returns {es.ModelNode} Reference to the model this node observes
 163+ */
 164+es.DocumentViewNode.prototype.getModel = function() {
 165+ return this.model;
 166+};
 167+
 168+/**
 169+ * Gets a reference to this node's parent.
 170+ *
 171+ * @method
 172+ * @returns {es.DocumentViewNode} Reference to this node's parent
 173+ */
 174+es.DocumentViewNode.prototype.getParent = function() {
 175+ return this.parent;
 176+};
 177+
 178+/**
 179+ * Attaches node as a child to another node.
 180+ *
 181+ * @method
 182+ * @param {es.DocumentViewNode} parent Node to attach to
 183+ * @emits attach (parent)
 184+ */
 185+es.DocumentViewNode.prototype.attach = function( parent ) {
 186+ this.parent = parent;
 187+ this.emit( 'attach', parent );
 188+};
 189+
 190+/**
 191+ * Detaches node from it's parent.
 192+ *
 193+ * @method
 194+ * @emits detach (parent)
 195+ */
 196+es.DocumentViewNode.prototype.detach = function() {
 197+ var parent = this.parent;
 198+ this.parent = null;
 199+ this.emit( 'detach', parent );
 200+};
 201+
 202+/* Inheritance */
 203+
 204+es.extendClass( es.DocumentViewNode, es.DocumentNode );
 205+es.extendClass( es.DocumentViewNode, es.EventEmitter );
Index: trunk/extensions/VisualEditor/modules/bases/es.DocumentNode.js
@@ -0,0 +1,236 @@
 2+/**
 3+ * Creates an es.DocumentNode object.
 4+ *
 5+ * @class
 6+ * @abstract
 7+ * @constructor
 8+ * @param {es.DocumentNode[]} nodes List of document nodes to initially add
 9+ */
 10+es.DocumentNode = function( nodes ) {
 11+ this.children = es.isArray( nodes ) ? nodes : [];
 12+};
 13+
 14+/* Abstract Methods */
 15+
 16+/**
 17+ * Gets the element length.
 18+ *
 19+ * @abstract
 20+ * @method
 21+ * @returns {Integer} Length of element and it's content
 22+ */
 23+es.DocumentNode.prototype.getElementLength = function() {
 24+ throw 'DocumentNode.getElementLength not implemented in this subclass: ' + this.constructor;
 25+};
 26+
 27+/* Methods */
 28+
 29+/**
 30+ * Gets a list of child nodes.
 31+ *
 32+ * @abstract
 33+ * @method
 34+ * @returns {es.DocumentNode[]} List of document nodes
 35+ */
 36+es.DocumentNode.prototype.getChildren = function() {
 37+ return this.children;
 38+};
 39+
 40+/**
 41+ * Gets the range within this node that a given child node covers.
 42+ *
 43+ * @method
 44+ * @param {es.ModelNode} node Node to get range for
 45+ * @param {Boolean} [shallow] Do not iterate into child nodes of child nodes
 46+ * @returns {es.Range|null} Range of node or null if node was not found
 47+ */
 48+es.DocumentNode.prototype.getRangeFromNode = function( node, shallow ) {
 49+ if ( this.children.length ) {
 50+ for ( var i = 0, length = this.children.length, left = 0; i < length; i++ ) {
 51+ if ( this.children[i] === node ) {
 52+ return new es.Range( left, left + this.children[i].getElementLength() );
 53+ }
 54+ if ( !shallow && this.children[i].getChildren().length ) {
 55+ var range = this.children[i].getRangeFromNode( node );
 56+ if ( range !== null ) {
 57+ // Include opening of parent
 58+ left++;
 59+ return es.Range.newFromTranslatedRange( range, left );
 60+ }
 61+ }
 62+ left += this.children[i].getElementLength();
 63+ }
 64+ }
 65+ return null;
 66+};
 67+
 68+/**
 69+ * Gets the content offset of a node.
 70+ *
 71+ * This method is pretty expensive. If you need to get different slices of the same content, get
 72+ * the content first, then slice it up locally.
 73+ *
 74+ * TODO: Rewrite this method to not use recursion, because the function call overhead is expensive
 75+ *
 76+ * @method
 77+ * @param {es.DocumentModelNode} node Node to get offset of
 78+ * @param {Boolean} [shallow] Do not iterate into child nodes of child nodes
 79+ * @returns {Integer} Offset of node or -1 of node was not found
 80+ */
 81+es.DocumentNode.prototype.getOffsetFromNode = function( node, shallow ) {
 82+ if ( this.children.length ) {
 83+ var offset = 0;
 84+ for ( var i = 0, length = this.children.length; i < length; i++ ) {
 85+ if ( this.children[i] === node ) {
 86+ return offset;
 87+ }
 88+ if ( !shallow && this.children[i].getChildren().length ) {
 89+ var childOffset = this.getOffsetFromNode.call( this.children[i], node );
 90+ if ( childOffset !== -1 ) {
 91+ return offset + 1 + childOffset;
 92+ }
 93+ }
 94+ offset += this.children[i].getElementLength();
 95+ }
 96+ }
 97+ return -1;
 98+};
 99+
 100+/**
 101+ * Gets the node at a given offset.
 102+ *
 103+ * This method is pretty expensive. If you need to get different slices of the same content, get
 104+ * the content first, then slice it up locally.
 105+ *
 106+ * TODO: Rewrite this method to not use recursion, because the function call overhead is expensive
 107+ *
 108+ * @method
 109+ * @param {Integer} offset Offset get node for
 110+ * @param {Boolean} [shallow] Do not iterate into child nodes of child nodes
 111+ * @returns {es.DocumentModelNode|null} Node at offset, or null if non was found
 112+ */
 113+es.DocumentNode.prototype.getNodeFromOffset = function( offset, shallow ) {
 114+ if ( this.children.length ) {
 115+ var nodeOffset = 0,
 116+ nodeLength;
 117+ for ( var i = 0, length = this.children.length; i < length; i++ ) {
 118+ nodeLength = this.children[i].getElementLength();
 119+ if ( offset >= nodeOffset && offset < nodeOffset + nodeLength ) {
 120+ if ( !shallow && this.children[i].getChildren().length ) {
 121+ return this.getNodeFromOffset.call( this.children[i], offset - nodeOffset - 1 );
 122+ } else {
 123+ return this.children[i];
 124+ }
 125+ }
 126+ nodeOffset += nodeLength;
 127+ }
 128+ }
 129+ return null;
 130+};
 131+
 132+/**
 133+ * Gets a list of nodes and their sub-ranges which are covered by a given range.
 134+ *
 135+ * @method
 136+ * @param {es.Range} range Range to select nodes within
 137+ * @param {Boolean} [shallow] Do not recurse into child nodes of child nodes
 138+ * @returns {Array} List of objects with 'node' and 'range' properties describing nodes which are
 139+ * covered by the range and the range within the node that is covered
 140+ */
 141+es.DocumentNode.prototype.selectNodes = function( range, shallow ) {
 142+ range.normalize();
 143+ var nodes = [],
 144+ i,
 145+ left,
 146+ right,
 147+ start = range.start,
 148+ end = range.end,
 149+ startInside,
 150+ endInside;
 151+
 152+ if ( start < 0 ) {
 153+ throw 'The start offset of the range is negative';
 154+ }
 155+
 156+ if ( this.children.length === 0 ) {
 157+ // Special case: this node doesn't have any children
 158+ // The return value is simply the range itself, if it is not out of bounds
 159+ if ( end > this.getContentLength() ) {
 160+ throw 'The end offset of the range is past the end of the node';
 161+ }
 162+ return [{ 'node': this, 'range': new es.Range( start, end ) }];
 163+ }
 164+
 165+ // This node has children, loop over them
 166+ left = 1; // First offset inside the first child. Offset 0 is before the first child
 167+ for ( i = 0; i < this.children.length; i++ ) {
 168+ // left <= any offset inside this.children[i] <= right
 169+ right = left + this.children[i].getContentLength();
 170+
 171+ if ( start == end && ( start == left - 1 || start == right + 1 ) ) {
 172+ // Empty range outside of any node
 173+ return [];
 174+ }
 175+ if ( start == left - 1 && end == right + 1 ) {
 176+ // The range covers the entire node, including its opening and closing elements
 177+ return [ { 'node': this.children[i] } ];
 178+ }
 179+ if ( start == left - 1 ) {
 180+ // start is between this.children[i-1] and this.children[i], move it to left for convenience
 181+ // We don't need to check for start < end here because we already have start != end and
 182+ // start <= end
 183+ start = left;
 184+ }
 185+ if ( end == right + 1 ) {
 186+ // end is between this.children[i] and this.children[i+1], move it to right for convenience
 187+ // We don't need to check for start < end here because we already have start != end and
 188+ // start <= end
 189+ end = right;
 190+ }
 191+
 192+ startInside = start >= left && start <= right; // is the start inside this.children[i]?
 193+ endInside = end >= left && end <= right; // is the end inside this.children[i]?
 194+
 195+ if ( startInside && endInside ) {
 196+ // The range is entirely inside this.children[i]
 197+ if ( shallow ) {
 198+ nodes = [{ 'node': this.children[i], 'range': new es.Range( start - left, end - left ) }];
 199+ } else {
 200+ // Recurse into this.children[i]
 201+ nodes = this.children[i].selectNodes( new es.Range( start - left, end - left ) );
 202+ }
 203+ // Since the start and end are both inside this.children[i], we know for sure that we're done, so
 204+ // return
 205+ return nodes;
 206+ } else if ( startInside ) {
 207+ // The start is inside this.children[i] but the end isn't
 208+ // Add a range from the start of the range to the end of this.children[i]
 209+ nodes.push( { 'node': this.children[i], 'range': new es.Range( start - left, right - left ) } );
 210+ } else if ( endInside ) {
 211+ // The end is inside this.children[i] but the start isn't
 212+ // Add a range from the start of this.children[i] to the end of the range
 213+ nodes.push( { 'node': this.children[i], 'range': new es.Range( 0, end - left ) } );
 214+ // We've found the end, so we're done
 215+ return nodes;
 216+ } else if ( nodes.length > 0 ) {
 217+ // Neither the start nor the end is inside this.children[i], but nodes is non-empty,
 218+ // so this.children[i] must be between the start and the end
 219+ // Add the entire node, so no range property
 220+ nodes.push( { 'node': this.children[i] } );
 221+ }
 222+
 223+ // Move left to the start of this.children[i+1] for the next iteration
 224+ // +2 because we need to jump over the offset between this.children[i] and this.children[i+1]
 225+ left = right + 2;
 226+ }
 227+
 228+ // If we got here, that means that at least some part of the range is out of bounds
 229+ // This is an error
 230+ if ( nodes.length === 0 ) {
 231+ throw 'The start offset of the range is past the end of the node';
 232+ } else {
 233+ // Apparently the start was inside this node, but the end wasn't
 234+ throw 'The end offset of the range is past the end of the node';
 235+ }
 236+ return nodes;
 237+};
Index: trunk/extensions/VisualEditor/modules/bases/es.DocumentModelNode.js
@@ -0,0 +1,423 @@
 2+/**
 3+ * Creates an es.DocumentModelNode object.
 4+ *
 5+ * es.DocumentModelNode is a simple wrapper around es.ModelNode, which adds functionality for model
 6+ * nodes to be used as nodes in a space partitioning tree.
 7+ *
 8+ * @class
 9+ * @constructor
 10+ * @extends {es.DocumentNode}
 11+ * @extends {es.EventEmitter}
 12+ * @param {Integer|Array} contents Either Length of content or array of child nodes to append
 13+ * @property {Integer} contentLength Length of content
 14+ */
 15+es.DocumentModelNode = function( element, contents ) {
 16+ // Inheritance
 17+ es.DocumentNode.call( this );
 18+ es.EventEmitter.call( this );
 19+
 20+ // Reusable function for passing update events upstream
 21+ var _this = this;
 22+ this.emitUpdate = function() {
 23+ _this.emit( 'update' );
 24+ };
 25+
 26+ // Properties
 27+ this.parent = null;
 28+ this.root = this;
 29+ this.element = element || null;
 30+ this.contentLength = 0;
 31+ if ( typeof contents === 'number' ) {
 32+ if ( contents < 0 ) {
 33+ throw 'Invalid content length error. Content length can not be less than 0.';
 34+ }
 35+ this.contentLength = contents;
 36+ } else if ( es.isArray( contents ) ) {
 37+ for ( var i = 0; i < contents.length; i++ ) {
 38+ this.push( contents[i] );
 39+ }
 40+ }
 41+};
 42+
 43+/* Abstract Methods */
 44+
 45+/**
 46+ * Creates a view for this node.
 47+ *
 48+ * @abstract
 49+ * @method
 50+ * @returns {es.DocumentViewNode} New item view associated with this model
 51+ */
 52+es.DocumentModelNode.prototype.createView = function() {
 53+ throw 'DocumentModelNode.createView not implemented in this subclass:' + this.constructor;
 54+};
 55+
 56+/* Methods */
 57+
 58+/**
 59+ * Adds a node to the end of this node's children.
 60+ *
 61+ * @method
 62+ * @param {es.DocumentModelNode} childModel Item to add
 63+ * @returns {Integer} New number of children
 64+ * @emits beforePush (childModel)
 65+ * @emits afterPush (childModel)
 66+ * @emits update
 67+ */
 68+es.DocumentModelNode.prototype.push = function( childModel ) {
 69+ this.emit( 'beforePush', childModel );
 70+ childModel.attach( this );
 71+ childModel.on( 'update', this.emitUpdate );
 72+ this.children.push( childModel );
 73+ this.adjustContentLength( childModel.getElementLength(), true );
 74+ this.emit( 'afterPush', childModel );
 75+ this.emit( 'update' );
 76+ return this.children.length;
 77+};
 78+
 79+/**
 80+ * Adds a node to the beginning of this node's children.
 81+ *
 82+ * @method
 83+ * @param {es.DocumentModelNode} childModel Item to add
 84+ * @returns {Integer} New number of children
 85+ * @emits beforeUnshift (childModel)
 86+ * @emits afterUnshift (childModel)
 87+ * @emits update
 88+ */
 89+es.DocumentModelNode.prototype.unshift = function( childModel ) {
 90+ this.emit( 'beforeUnshift', childModel );
 91+ childModel.attach( this );
 92+ childModel.on( 'update', this.emitUpdate );
 93+ this.children.unshift( childModel );
 94+ this.adjustContentLength( childModel.getElementLength(), true );
 95+ this.emit( 'afterUnshift', childModel );
 96+ this.emit( 'update' );
 97+ return this.children.length;
 98+};
 99+
 100+/**
 101+ * Removes a node from the end of this node's children
 102+ *
 103+ * @method
 104+ * @returns {es.DocumentModelNode} Removed childModel
 105+ * @emits beforePop
 106+ * @emits afterPop
 107+ * @emits update
 108+ */
 109+es.DocumentModelNode.prototype.pop = function() {
 110+ if ( this.children.length ) {
 111+ this.emit( 'beforePop' );
 112+ var childModel = this.children[this.children.length - 1];
 113+ childModel.detach();
 114+ childModel.removeListener( 'update', this.emitUpdate );
 115+ this.children.pop();
 116+ this.adjustContentLength( -childModel.getElementLength(), true );
 117+ this.emit( 'afterPop' );
 118+ this.emit( 'update' );
 119+ return childModel;
 120+ }
 121+};
 122+
 123+/**
 124+ * Removes a node from the beginning of this node's children
 125+ *
 126+ * @method
 127+ * @returns {es.DocumentModelNode} Removed childModel
 128+ * @emits beforeShift
 129+ * @emits afterShift
 130+ * @emits update
 131+ */
 132+es.DocumentModelNode.prototype.shift = function() {
 133+ if ( this.children.length ) {
 134+ this.emit( 'beforeShift' );
 135+ var childModel = this.children[0];
 136+ childModel.detach();
 137+ childModel.removeListener( 'update', this.emitUpdate );
 138+ this.children.shift();
 139+ this.adjustContentLength( -childModel.getElementLength(), true );
 140+ this.emit( 'afterShift' );
 141+ this.emit( 'update' );
 142+ return childModel;
 143+ }
 144+};
 145+
 146+/**
 147+ * Adds and removes nodes from this node's children.
 148+ *
 149+ * @method
 150+ * @param {Integer} index Index to remove and or insert nodes at
 151+ * @param {Integer} howmany Number of nodes to remove
 152+ * @param {es.DocumentModelNode} [...] Variadic list of nodes to insert
 153+ * @returns {es.DocumentModelNode[]} Removed nodes
 154+ * @emits beforeSplice (index, howmany, [...])
 155+ * @emits afterSplice (index, howmany, [...])
 156+ * @emits update
 157+ */
 158+es.DocumentModelNode.prototype.splice = function( index, howmany ) {
 159+ var i,
 160+ length,
 161+ args = Array.prototype.slice.call( arguments, 0 ),
 162+ diff = 0;
 163+ this.emit.apply( this, ['beforeSplice'].concat( args ) );
 164+ if ( args.length >= 3 ) {
 165+ for ( i = 2, length = args.length; i < length; i++ ) {
 166+ diff += args[i].getElementLength();
 167+ args[i].attach( this );
 168+ }
 169+ }
 170+ var removed = this.children.splice.apply( this.children, args );
 171+ for ( i = 0, length = removed.length; i < length; i++ ) {
 172+ diff -= removed[i].getElementLength();
 173+ removed[i].detach();
 174+ removed[i].removeListener( 'update', this.emitUpdate );
 175+ }
 176+ this.adjustContentLength( diff, true );
 177+ this.emit.apply( this, ['afterSplice'].concat( args ) );
 178+ this.emit( 'update' );
 179+ return removed;
 180+};
 181+
 182+/**
 183+ * Sorts this node's children.
 184+ *
 185+ * @method
 186+ * @param {Function} sortfunc Function to use when sorting
 187+ * @emits beforeSort (sortfunc)
 188+ * @emits afterSort (sortfunc)
 189+ * @emits update
 190+ */
 191+es.DocumentModelNode.prototype.sort = function( sortfunc ) {
 192+ this.emit( 'beforeSort', sortfunc );
 193+ this.children.sort( sortfunc );
 194+ this.emit( 'afterSort', sortfunc );
 195+ this.emit( 'update' );
 196+};
 197+
 198+/**
 199+ * Reverses the order of this node's children.
 200+ *
 201+ * @method
 202+ * @emits beforeReverse
 203+ * @emits afterReverse
 204+ * @emits update
 205+ */
 206+es.DocumentModelNode.prototype.reverse = function() {
 207+ this.emit( 'beforeReverse' );
 208+ this.children.reverse();
 209+ this.emit( 'afterReverse' );
 210+ this.emit( 'update' );
 211+};
 212+
 213+/**
 214+ * Gets a reference to this node's parent.
 215+ *
 216+ * @method
 217+ * @returns {es.DocumentModelNode} Reference to this node's parent
 218+ */
 219+es.DocumentModelNode.prototype.getParent = function() {
 220+ return this.parent;
 221+};
 222+
 223+/**
 224+ * Gets the root node in the tree this node is currently attached to.
 225+ *
 226+ * @method
 227+ * @returns {es.DocumentModelNode} Root node
 228+ */
 229+es.DocumentModelNode.prototype.getRoot = function() {
 230+ return this.root;
 231+};
 232+
 233+/**
 234+ * Sets the root node to this and all of it's children.
 235+ *
 236+ * @method
 237+ * @param {es.DocumentModelNode} root Node to use as root
 238+ */
 239+es.DocumentModelNode.prototype.setRoot = function( root ) {
 240+ this.root = root;
 241+ for ( var i = 0; i < this.children.length; i++ ) {
 242+ this.children[i].setRoot( root );
 243+ }
 244+};
 245+
 246+/**
 247+ * Clears the root node from this and all of it's children.
 248+ *
 249+ * @method
 250+ */
 251+es.DocumentModelNode.prototype.clearRoot = function() {
 252+ this.root = null;
 253+ for ( var i = 0; i < this.children.length; i++ ) {
 254+ this.children[i].clearRoot();
 255+ }
 256+};
 257+
 258+/**
 259+ * Attaches this node to another as a child.
 260+ *
 261+ * @method
 262+ * @param {es.DocumentModelNode} parent Node to attach to
 263+ * @emits attach (parent)
 264+ */
 265+es.DocumentModelNode.prototype.attach = function( parent ) {
 266+ this.emit( 'beforeAttach', parent );
 267+ this.parent = parent;
 268+ this.setRoot( parent.getRoot() );
 269+ this.emit( 'afterAttach', parent );
 270+};
 271+
 272+/**
 273+ * Detaches this node from it's parent.
 274+ *
 275+ * @method
 276+ * @emits detach
 277+ */
 278+es.DocumentModelNode.prototype.detach = function() {
 279+ this.emit( 'beforeDetach' );
 280+ this.parent = null;
 281+ this.clearRoot();
 282+ this.emit( 'afterDetach' );
 283+};
 284+
 285+/**
 286+ * Sets the content length.
 287+ *
 288+ * @method
 289+ * @param {Integer} contentLength Length of content
 290+ * @throws Invalid content length error if contentLength is less than 0
 291+ */
 292+es.DocumentModelNode.prototype.setContentLength = function( contentLength ) {
 293+ if ( contentLength < 0 ) {
 294+ throw 'Invalid content length error. Content length can not be less than 0.';
 295+ }
 296+ var diff = contentLength - this.contentLength;
 297+ this.contentLength = contentLength;
 298+ if ( this.parent ) {
 299+ this.parent.adjustContentLength( diff );
 300+ }
 301+};
 302+
 303+/**
 304+ * Adjust the content length.
 305+ *
 306+ * @method
 307+ * @param {Integer} adjustment Amount to adjust content length by
 308+ * @throws Invalid adjustment error if resulting length is less than 0
 309+ */
 310+es.DocumentModelNode.prototype.adjustContentLength = function( adjustment, quiet ) {
 311+ this.contentLength += adjustment;
 312+ // Make sure the adjustment was sane
 313+ if ( this.contentLength < 0 ) {
 314+ // Reverse the adjustment
 315+ this.contentLength -= adjustment;
 316+ // Complain about it
 317+ throw 'Invalid adjustment error. Content length can not be less than 0.';
 318+ }
 319+ if ( this.parent ) {
 320+ this.parent.adjustContentLength( adjustment, true );
 321+ }
 322+ if ( !quiet ) {
 323+ this.emit( 'update' );
 324+ }
 325+};
 326+
 327+/**
 328+ * Gets the content length.
 329+ *
 330+ * @method
 331+ * @returns {Integer} Length of content
 332+ */
 333+es.DocumentModelNode.prototype.getContentLength = function() {
 334+ return this.contentLength;
 335+};
 336+
 337+/**
 338+ * Gets the element length.
 339+ *
 340+ * @method
 341+ * @returns {Integer} Length of content
 342+ */
 343+es.DocumentModelNode.prototype.getElementLength = function() {
 344+ return this.contentLength + 2;
 345+};
 346+
 347+/**
 348+ * Gets the element object.
 349+ *
 350+ * @method
 351+ * @returns {Object} Element object in linear data model
 352+ */
 353+es.DocumentModelNode.prototype.getElement = function() {
 354+ return this.element;
 355+};
 356+
 357+/**
 358+ * Gets the symbolic element type name.
 359+ *
 360+ * @method
 361+ * @returns {String} Symbolic name of element type
 362+ */
 363+es.DocumentModelNode.prototype.getElementType = function() {
 364+ return this.element.type;
 365+};
 366+
 367+/**
 368+ * Gets an element attribute value.
 369+ *
 370+ * @method
 371+ * @returns {Mixed} Value of attribute, or null if no such attribute exists
 372+ */
 373+es.DocumentModelNode.prototype.getElementAttribute = function( key ) {
 374+ if ( this.element.attributes && key in this.element.attributes ) {
 375+ return this.element.attributes[key];
 376+ }
 377+ return null;
 378+};
 379+
 380+/**
 381+ * Gets the content length.
 382+ *
 383+ * FIXME: This method makes assumptions that a node with a data property is a DocumentModel, which
 384+ * may be an issue if sub-classes of DocumentModelNode other than DocumentModel have a data property
 385+ * as well. A safer way of determining this would be helpful in preventing future bugs.
 386+ *
 387+ * @method
 388+ * @param {es.Range} [range] Range of content to get
 389+ * @returns {Integer} Length of content
 390+ */
 391+es.DocumentModelNode.prototype.getContent = function( range ) {
 392+ // Find root
 393+ var root = this.data ? this : ( this.root.data ? this.root : null );
 394+
 395+ if ( root ) {
 396+ return root.getContentFromNode( this, range );
 397+ }
 398+ return [];
 399+};
 400+
 401+/**
 402+ * Gets plain text version of the content within a specific range.
 403+ *
 404+ * @method
 405+ * @param {es.Range} [range] Range of text to get
 406+ * @returns {String} Text within given range
 407+ */
 408+es.DocumentModelNode.prototype.getText = function( range ) {
 409+ var content = this.getContent( range );
 410+ // Copy characters
 411+ var text = '';
 412+ for ( var i = 0, length = content.length; i < length; i++ ) {
 413+ // If not using in IE6 or IE7 (which do not support array access for strings) use this..
 414+ // text += this.data[i][0];
 415+ // Otherwise use this...
 416+ text += typeof content[i] === 'string' ? content[i] : content[i][0];
 417+ }
 418+ return text;
 419+};
 420+
 421+/* Inheritance */
 422+
 423+es.extendClass( es.DocumentModelNode, es.DocumentNode );
 424+es.extendClass( es.DocumentModelNode, es.EventEmitter );
Index: trunk/extensions/VisualEditor/modules/bases/es.DocumentViewLeafNode.js
@@ -0,0 +1,92 @@
 2+/**
 3+ * Creates an es.DocumentViewLeafNode object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewNode}
 8+ * @param model {es.ModelNode} Model to observe
 9+ * @param {jQuery} [$element] Element to use as a container
 10+ */
 11+es.DocumentViewLeafNode = function( model, $element ) {
 12+ // Inheritance
 13+ es.DocumentViewNode.call( this, model, $element );
 14+
 15+ // Properties
 16+ this.$content = $( '<div class="es-content"></div>' ).appendTo( this.$ );
 17+ this.contentView = new es.ContentView( this.$content, model );
 18+};
 19+
 20+/* Methods */
 21+
 22+/**
 23+ * Render content.
 24+ *
 25+ * @method
 26+ */
 27+es.DocumentViewLeafNode.prototype.renderContent = function() {
 28+ this.contentView.render();
 29+};
 30+
 31+/**
 32+ * Draw selection around a given range.
 33+ *
 34+ * @method
 35+ * @param {es.Range} range Range of content to draw selection around
 36+ */
 37+es.DocumentViewLeafNode.prototype.drawSelection = function( range ) {
 38+ this.contentView.drawSelection( range );
 39+};
 40+
 41+/**
 42+ * Clear selection.
 43+ *
 44+ * @method
 45+ */
 46+es.DocumentViewLeafNode.prototype.clearSelection = function() {
 47+ this.contentView.clearSelection();
 48+};
 49+
 50+/**
 51+ * Gets the nearest offset of a rendered position.
 52+ *
 53+ * @method
 54+ * @param {es.Position} position Position to get offset for
 55+ * @returns {Integer} Offset of position
 56+ */
 57+es.DocumentViewLeafNode.prototype.getOffsetFromRenderedPosition = function( position ) {
 58+ return this.contentView.getOffsetFromRenderedPosition( position );
 59+};
 60+
 61+/**
 62+ * Gets rendered position of offset within content.
 63+ *
 64+ * @method
 65+ * @param {Integer} offset Offset to get position for
 66+ * @returns {es.Position} Position of offset
 67+ */
 68+es.DocumentViewLeafNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
 69+ var position = this.contentView.getRenderedPositionFromOffset( offset, leftBias ),
 70+ offset = this.$content.offset();
 71+ position.top += offset.top;
 72+ position.left += offset.left;
 73+ position.bottom += offset.top;
 74+ return position;
 75+};
 76+
 77+/**
 78+ * Gets the length of the content in the model.
 79+ *
 80+ * @method
 81+ * @returns {Integer} Length of content
 82+ */
 83+es.DocumentViewLeafNode.prototype.getElementLength = function() {
 84+ return this.model.getElementLength();
 85+};
 86+
 87+es.DocumentViewLeafNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
 88+ return this.contentView.getRenderedLineRangeFromOffset( offset );
 89+};
 90+
 91+/* Inheritance */
 92+
 93+es.extendClass( es.DocumentViewLeafNode, es.DocumentViewNode );
Index: trunk/extensions/VisualEditor/modules/bases/es.EventEmitter.js
@@ -0,0 +1,185 @@
 2+/**
 3+ * Event emitter.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @property events {Object}
 8+ */
 9+es.EventEmitter = function() {
 10+ this.events = {};
 11+};
 12+
 13+/* Methods */
 14+
 15+/**
 16+ * Emits an event.
 17+ *
 18+ * @method
 19+ * @param type {String} Type of event
 20+ * @param args {Mixed} First in a list of variadic arguments passed to event handler (optional)
 21+ * @returns {Boolean} If event was handled by at least one listener
 22+ */
 23+es.EventEmitter.prototype.emit = function( type ) {
 24+ if ( type === 'error' && !( 'error' in this.events ) ) {
 25+ throw 'Missing error handler error.';
 26+ }
 27+ if ( !( type in this.events ) ) {
 28+ return false;
 29+ }
 30+ var listeners = this.events[type].slice();
 31+ var args = Array.prototype.slice.call( arguments, 1 );
 32+ for ( var i = 0; i < listeners.length; i++ ) {
 33+ listeners[i].apply( this, args );
 34+ }
 35+ return true;
 36+};
 37+
 38+/**
 39+ * Adds a listener to events of a specific type.
 40+ *
 41+ * @method
 42+ * @param type {String} Type of event to listen to
 43+ * @param listener {Function} Listener to call when event occurs
 44+ * @returns {es.EventEmitter} This object
 45+ * @throws "Invalid listener error" if listener argument is not a function
 46+ */
 47+es.EventEmitter.prototype.addListener = function( type, listener ) {
 48+ if ( typeof listener !== 'function' ) {
 49+ throw 'Invalid listener error. Function expected.';
 50+ }
 51+ this.emit( 'newListener', type, listener );
 52+ if ( type in this.events ) {
 53+ this.events[type].push( listener );
 54+ } else {
 55+ this.events[type] = [listener];
 56+ }
 57+ return this;
 58+};
 59+
 60+/**
 61+ * Add multiple listeners at once.
 62+ *
 63+ * @method
 64+ * @param listeners {Object} List of event/callback pairs
 65+ * @returns {es.EventEmitter} This object
 66+ */
 67+es.EventEmitter.prototype.addListeners = function( listeners ) {
 68+ for ( var event in listeners ) {
 69+ this.addListener( event, listeners[event] );
 70+ }
 71+ return this;
 72+};
 73+
 74+/**
 75+ * Add a listener, mapped to a method on a target object.
 76+ *
 77+ * @method
 78+ * @param target {Object} Object to call methods on when events occur
 79+ * @param event {String} Name of event to trigger on
 80+ * @param method {String} Name of method to call
 81+ * @returns {es.EventEmitter} This object
 82+ */
 83+es.EventEmitter.prototype.addListenerMethod = function( target, event, method ) {
 84+ return this.addListener( event, function() {
 85+ if ( typeof target[method] === 'function' ) {
 86+ target[method].apply( target, Array.prototype.slice.call( arguments, 0 ) );
 87+ } else {
 88+ throw 'Listener method error. Target has no such method: ' + method;
 89+ }
 90+ } );
 91+};
 92+
 93+/**
 94+ * Add multiple listeners, each mapped to a method on a target object.
 95+ *
 96+ * @method
 97+ * @param target {Object} Object to call methods on when events occur
 98+ * @param methods {Object} List of event/method name pairs
 99+ * @returns {es.EventEmitter} This object
 100+ */
 101+es.EventEmitter.prototype.addListenerMethods = function( target, methods ) {
 102+ for ( var event in methods ) {
 103+ this.addListenerMethod( target, event, methods[event] );
 104+ }
 105+ return this;
 106+};
 107+
 108+/**
 109+ * Alias for addListener
 110+ *
 111+ * @method
 112+ */
 113+es.EventEmitter.prototype.on = es.EventEmitter.prototype.addListener;
 114+
 115+/**
 116+ * Adds a one-time listener to a specific event.
 117+ *
 118+ * @method
 119+ * @param type {String} Type of event to listen to
 120+ * @param listener {Function} Listener to call when event occurs
 121+ * @returns {es.EventEmitter} This object
 122+ */
 123+es.EventEmitter.prototype.once = function( type, listener ) {
 124+ var eventEmitter = this;
 125+ return this.addListener( type, function listenerWrapper() {
 126+ eventEmitter.removeListener( type, listenerWrapper );
 127+ listener.apply( eventEmitter, Array.prototype.slice.call( arguments, 0 ) );
 128+ } );
 129+};
 130+
 131+/**
 132+ * Removes a specific listener from a specific event.
 133+ *
 134+ * @method
 135+ * @param type {String} Type of event to remove listener from
 136+ * @param listener {Function} Listener to remove
 137+ * @returns {es.EventEmitter} This object
 138+ * @throws "Invalid listener error" if listener argument is not a function
 139+ */
 140+es.EventEmitter.prototype.removeListener = function( type, listener ) {
 141+ if ( typeof listener !== 'function' ) {
 142+ throw 'Invalid listener error. Function expected.';
 143+ }
 144+ if ( !( type in this.events ) || !this.events[type].length ) {
 145+ return this;
 146+ }
 147+ var handlers = this.events[type];
 148+ if ( handlers.length === 1 && handlers[0] === listener ) {
 149+ delete this.events[type];
 150+ } else {
 151+ var i = handlers.indexOf( listener );
 152+ if ( i < 0 ) {
 153+ return this;
 154+ }
 155+ handlers.splice( i, 1 );
 156+ if ( handlers.length === 0 ) {
 157+ delete this.events[type];
 158+ }
 159+ }
 160+ return this;
 161+};
 162+
 163+/**
 164+ * Removes all listeners from a specific event.
 165+ *
 166+ * @method
 167+ * @param type {String} Type of event to remove listeners from
 168+ * @returns {es.EventEmitter} This object
 169+ */
 170+es.EventEmitter.prototype.removeAllListeners = function( type ) {
 171+ if ( type in this.events ) {
 172+ delete this.events[type];
 173+ }
 174+ return this;
 175+};
 176+
 177+/**
 178+ * Gets a list of listeners attached to a specific event.
 179+ *
 180+ * @method
 181+ * @param type {String} Type of event to get listeners for
 182+ * @returns {Array} List of listeners to an event
 183+ */
 184+es.EventEmitter.prototype.listeners = function( type ) {
 185+ return type in this.events ? this.events[type] : [];
 186+};
Index: trunk/extensions/VisualEditor/modules/bases/es.DocumentViewBranchNode.js
@@ -0,0 +1,128 @@
 2+/**
 3+ * Creates an es.DocumentViewBranchNode object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewNode}
 8+ * @param model {es.ModelNode} Model to observe
 9+ * @param {jQuery} [$element] Element to use as a container
 10+ */
 11+es.DocumentViewBranchNode = function( model, $element, horizontal ) {
 12+ // Inheritance
 13+ es.DocumentViewNode.call( this, model, $element );
 14+
 15+ // Properties
 16+ this.horizontal = horizontal || false;
 17+};
 18+
 19+/* Methods */
 20+
 21+/**
 22+ * Render content.
 23+ *
 24+ * @method
 25+ */
 26+es.DocumentViewBranchNode.prototype.renderContent = function() {
 27+ for ( var i = 0; i < this.children.length; i++ ) {
 28+ this.children[i].renderContent();
 29+ }
 30+};
 31+
 32+/**
 33+ * Draw selection around a given range.
 34+ *
 35+ * @method
 36+ * @param {es.Range} range Range of content to draw selection around
 37+ */
 38+es.DocumentViewBranchNode.prototype.drawSelection = function( range ) {
 39+ var nodes = this.selectNodes( range, true );
 40+ for ( var i = 0; i < this.children.length; i++ ) {
 41+ if ( nodes.length && this.children[i] === nodes[0].node ) {
 42+ for ( var j = 0; j < nodes.length; j++ ) {
 43+ nodes[j].node.drawSelection( nodes[j].range );
 44+ i++;
 45+ }
 46+ } else {
 47+ this.children[i].clearSelection();
 48+ }
 49+ }
 50+};
 51+
 52+/**
 53+ * Clear selection.
 54+ *
 55+ * @method
 56+ */
 57+es.DocumentViewBranchNode.prototype.clearSelection = function() {
 58+ for ( var i = 0; i < this.children.length; i++ ) {
 59+ this.children[i].clearSelection();
 60+ }
 61+};
 62+
 63+/**
 64+ * Gets the nearest offset of a rendered position.
 65+ *
 66+ * @method
 67+ * @param {es.Position} position Position to get offset for
 68+ * @returns {Integer} Offset of position
 69+ */
 70+es.DocumentViewBranchNode.prototype.getOffsetFromRenderedPosition = function( position ) {
 71+ if ( this.children.length === 0 ) {
 72+ return 0;
 73+ }
 74+ var node = this.children[0];
 75+ for ( var i = 1; i < this.children.length; i++ ) {
 76+ if ( this.horizontal && this.children[i].$.offset().left > position.left ) {
 77+ break;
 78+ } else if ( !this.horizontal && this.children[i].$.offset().top > position.top ) {
 79+ break;
 80+ }
 81+ node = this.children[i];
 82+ }
 83+ return node.getParent().getOffsetFromNode( node, true ) +
 84+ node.getOffsetFromRenderedPosition( position ) + 1;
 85+};
 86+
 87+/**
 88+ * Gets rendered position of offset within content.
 89+ *
 90+ * @method
 91+ * @param {Integer} offset Offset to get position for
 92+ * @returns {es.Position} Position of offset
 93+ */
 94+es.DocumentViewBranchNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
 95+ var node = this.getNodeFromOffset( offset, true );
 96+ if ( node !== null ) {
 97+ return node.getRenderedPositionFromOffset(
 98+ offset - this.getOffsetFromNode( node, true ) - 1,
 99+ leftBias
 100+ );
 101+ }
 102+ return null;
 103+};
 104+
 105+/**
 106+ * Gets the length of the content in the model.
 107+ *
 108+ * @method
 109+ * @returns {Integer} Length of content
 110+ */
 111+es.DocumentViewBranchNode.prototype.getElementLength = function() {
 112+ return this.model.getElementLength();
 113+};
 114+
 115+es.DocumentViewBranchNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
 116+ var node = this.getNodeFromOffset( offset, true );
 117+ if ( node !== null ) {
 118+ var nodeOffset = this.getOffsetFromNode( node, true );
 119+ return es.Range.newFromTranslatedRange(
 120+ node.getRenderedLineRangeFromOffset( offset - nodeOffset - 1 ),
 121+ nodeOffset + 1
 122+ );
 123+ }
 124+ return null;
 125+};
 126+
 127+/* Inheritance */
 128+
 129+es.extendClass( es.DocumentViewBranchNode, es.DocumentViewNode );
Index: trunk/extensions/VisualEditor/modules/es.Range.js
@@ -0,0 +1,72 @@
 2+/**
 3+ * Range of content.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @param from {Integer} Starting offset
 8+ * @param to {Integer} Ending offset
 9+ * @property from {Integer} Starting offset
 10+ * @property to {Integer} Ending offset
 11+ * @property start {Integer} Normalized starting offset
 12+ * @property end {Integer} Normalized ending offset
 13+ */
 14+es.Range = function( from, to ) {
 15+ this.from = from || 0;
 16+ this.to = typeof to === 'undefined' ? this.from : to;
 17+ this.normalize();
 18+};
 19+
 20+
 21+/**
 22+ * Creates a new es.Range object that's a translated version of another.
 23+ *
 24+ * @method
 25+ * @param {es.Range} range Range to base new range on
 26+ * @param {Integer} distance Distance to move range by
 27+ * @returns {es.Range} New translated range
 28+ */
 29+es.Range.newFromTranslatedRange = function( range, distance ) {
 30+ return new es.Range( range.from + distance, range.to + distance );
 31+};
 32+
 33+/* Methods */
 34+
 35+/**
 36+ * Checks if an offset is within this range.
 37+ *
 38+ * @method
 39+ * @param offset {Integer} Offset to check
 40+ * @returns {Boolean} If offset is within this range
 41+ */
 42+es.Range.prototype.containsOffset = function( offset ) {
 43+ this.normalize();
 44+ return offset >= this.start && offset < this.end;
 45+};
 46+
 47+/**
 48+ * Gets the length of the range.
 49+ *
 50+ * @method
 51+ * @returns {Integer} Length of range
 52+ */
 53+es.Range.prototype.getLength = function() {
 54+ return Math.abs( this.from - this.to );
 55+};
 56+
 57+/**
 58+ * Sets start and end properties, ensuring start is always before end.
 59+ *
 60+ * This should always be called before using the start or end properties. Do not call this unless
 61+ * you are about to use these properties.
 62+ *
 63+ * @method
 64+ */
 65+es.Range.prototype.normalize = function() {
 66+ if ( this.from < this.to ) {
 67+ this.start = this.from;
 68+ this.end = this.to;
 69+ } else {
 70+ this.start = this.to;
 71+ this.end = this.from;
 72+ }
 73+};
Index: trunk/extensions/VisualEditor/modules/views/es.ParagraphView.js
@@ -0,0 +1,19 @@
 2+/**
 3+ * Creates an es.ParagraphView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewLeafNode}
 8+ * @param {es.ParagraphModel} model Paragraph model to view
 9+ */
 10+es.ParagraphView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewLeafNode.call( this, model );
 13+
 14+ // DOM Changes
 15+ this.$.addClass( 'es-paragraphView' );
 16+};
 17+
 18+/* Inheritance */
 19+
 20+es.extendClass( es.ParagraphView, es.DocumentViewLeafNode );
Index: trunk/extensions/VisualEditor/modules/views/es.TableCellView.js
@@ -0,0 +1,21 @@
 2+/**
 3+ * Creates an es.TableCellView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.TableCellModel} model Table cell model to view
 9+ */
 10+es.TableCellView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewBranchNode.call( this, model, $( '<td>' ) );
 13+
 14+ // DOM Changes
 15+ this.$
 16+ .attr( 'style', model.getElementAttribute( 'html/style' ) )
 17+ .addClass( 'es-tableCellView' );
 18+};
 19+
 20+/* Inheritance */
 21+
 22+es.extendClass( es.TableCellView, es.DocumentViewBranchNode );
Index: trunk/extensions/VisualEditor/modules/views/es.TableView.js
@@ -0,0 +1,21 @@
 2+/**
 3+ * Creates an es.TableView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.TableModel} model Table model to view
 9+ */
 10+es.TableView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewBranchNode.call( this, model, $( '<table>' ) );
 13+
 14+ // DOM Changes
 15+ this.$
 16+ .attr( 'style', model.getElementAttribute( 'html/style' ) )
 17+ .addClass( 'es-tableView' );
 18+};
 19+
 20+/* Inheritance */
 21+
 22+es.extendClass( es.TableView, es.DocumentViewBranchNode );
Index: trunk/extensions/VisualEditor/modules/views/es.SurfaceView.js
@@ -0,0 +1,275 @@
 2+/**
 3+ * Creates an es.SurfaceView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @param {jQuery} $container DOM Container to render surface into
 8+ * @param {es.SurfaceModel} model Surface model to view
 9+ */
 10+es.SurfaceView = function( $container, model ) {
 11+ this.$ = $container.addClass( 'es-surfaceView' );
 12+ this.$window = $( window );
 13+ this.model = model;
 14+
 15+ // Initialize document view
 16+ this.documentView = new es.DocumentView( this.model.getDocument(), this );
 17+ this.$.append( this.documentView.$ );
 18+
 19+ // Interaction state
 20+ this.mouse = {
 21+ selecting: false,
 22+ clicks: 0,
 23+ clickDelay: 500,
 24+ clickTimeout: null,
 25+ clickPosition: null,
 26+ hotSpotRadius: 1,
 27+ lastMovePosition: null
 28+ };
 29+ this.cursor = {
 30+ $: $( '<div class="es-surfaceView-cursor"></div>' ).appendTo( this.$ ),
 31+ interval: null,
 32+ offset: null,
 33+ initialLeft: null,
 34+ initialBias: false
 35+ };
 36+ this.keyboard = {
 37+ selecting: false,
 38+ cursorAnchor: null,
 39+ keydownTimeout: null,
 40+ keys: {
 41+ shift: false,
 42+ control: false,
 43+ command: false,
 44+ alt: false
 45+ }
 46+ };
 47+ this.selection = {
 48+ from: 0,
 49+ to: 0
 50+ };
 51+
 52+ // References for use in closures
 53+ var surfaceView = this,
 54+ $document = $( document );
 55+
 56+ // MouseDown on surface
 57+ this.$.on( {
 58+ 'mousedown' : function(e) {
 59+ return surfaceView.onMouseDown( e );
 60+ }
 61+ } );
 62+
 63+ // Hidden input
 64+ this.$input = $( '<input class="es-surfaceView-input" />' )
 65+ .prependTo( this.$ )
 66+ .on( {
 67+ 'focus' : function() {
 68+ //console.log("focus");
 69+ $document.off( '.es-surfaceView' );
 70+ $document.on({
 71+ 'mousemove.es-surfaceView': function(e) {
 72+ //return surfaceView.onMouseMove( e );
 73+ },
 74+ 'mouseup.es-surfaceView': function(e) {
 75+ //return surfaceView.onMouseUp( e );
 76+ },
 77+ 'keydown.es-surfaceView': function( e ) {
 78+ return surfaceView.onKeyDown( e );
 79+ },
 80+ 'keyup.es-surfaceView': function( e ) {
 81+ return surfaceView.onKeyUp( e );
 82+ }
 83+ });
 84+ },
 85+ 'blur': function( e ) {
 86+ //console.log("blur");
 87+ $document.off( '.es-surfaceView' );
 88+ surfaceView.hideCursor();
 89+ }
 90+ } ).focus();
 91+
 92+ // First render
 93+ this.documentView.renderContent();
 94+
 95+ this.dimensions = {
 96+ width: this.$.width(),
 97+ height: this.$window.height(),
 98+ scrollTop: this.$window.scrollTop()
 99+ };
 100+
 101+ // Re-render when resizing horizontally
 102+ this.$window.resize( function() {
 103+ surfaceView.hideCursor();
 104+ surfaceView.dimensions.height = surfaceView.$window.height();
 105+ var width = surfaceView.$.width();
 106+ if ( surfaceView.dimensions.width !== width ) {
 107+ surfaceView.dimensions.width = width;
 108+ surfaceView.documentView.renderContent();
 109+ }
 110+ } );
 111+
 112+ this.$window.scroll( function() {
 113+ surfaceView.dimensions.scrollTop = surfaceView.$window.scrollTop();
 114+ } );
 115+};
 116+
 117+es.SurfaceView.prototype.onMouseDown = function( e ) {
 118+ var position = es.Position.newFromEventPagePosition( e ),
 119+ offset = this.documentView.getOffsetFromEvent( e ),
 120+ nodeView = this.documentView.getNodeFromOffset( offset, false );
 121+ this.showCursor( offset, position.left > nodeView.$.offset().left );
 122+ if ( !this.$input.is( ':focus' ) ) {
 123+ this.$input.focus().select();
 124+ }
 125+ return false;
 126+};
 127+
 128+es.SurfaceView.prototype.onKeyDown = function( e ) {
 129+ switch ( e.keyCode ) {
 130+ case 16: // Shift
 131+ this.keyboard.keys.shift = true;
 132+ break;
 133+ case 17: // Control
 134+ this.keyboard.keys.control = true;
 135+ break;
 136+ case 18: // Alt
 137+ this.keyboard.keys.alt = true;
 138+ break;
 139+ case 91: // Command
 140+ this.keyboard.keys.command = true;
 141+ break;
 142+ case 36: // Home
 143+ this.moveCursor( 'home' );
 144+ break;
 145+ case 35: // End
 146+ this.moveCursor( 'end' );
 147+ break;
 148+ case 37: // Left arrow
 149+ if ( this.keyboard.keys.command ) {
 150+ this.moveCursor( 'home' );
 151+ } else {
 152+ this.moveCursor( 'left' );
 153+ }
 154+ break;
 155+ case 38: // Up arrow
 156+ this.moveCursor( 'up' );
 157+ break;
 158+ case 39: // Right arrow
 159+ if ( this.keyboard.keys.command ) {
 160+ this.moveCursor( 'end' );
 161+ } else {
 162+ this.moveCursor( 'right' );
 163+ }
 164+ break;
 165+ case 40: // Down arrow
 166+ this.moveCursor( 'down' );
 167+ break;
 168+ case 8: // Backspace
 169+ break;
 170+ case 46: // Delete
 171+ break;
 172+ default: // Insert content (maybe)
 173+ break;
 174+ }
 175+ return false;
 176+};
 177+
 178+es.SurfaceView.prototype.onKeyUp = function( e ) {
 179+ switch ( e.keyCode ) {
 180+ case 16: // Shift
 181+ this.keyboard.keys.shift = false;
 182+ if ( this.keyboard.selecting ) {
 183+ this.keyboard.selecting = false;
 184+ }
 185+ break;
 186+ case 17: // Control
 187+ this.keyboard.keys.control = false;
 188+ break;
 189+ case 18: // Alt
 190+ this.keyboard.keys.alt = false;
 191+ break;
 192+ case 91: // Command
 193+ this.keyboard.keys.command = false;
 194+ break;
 195+ default:
 196+ break;
 197+ }
 198+ return true;
 199+};
 200+
 201+es.SurfaceView.prototype.moveCursor = function( instruction ) {
 202+ if ( instruction === 'left') {
 203+ this.showCursor(
 204+ this.documentView.getModel().getRelativeContentOffset( this.cursor.offset, -1 )
 205+ );
 206+ } else if ( instruction === 'right' ) {
 207+ this.showCursor(
 208+ this.documentView.getModel().getRelativeContentOffset( this.cursor.offset, 1 )
 209+ );
 210+ } else if ( instruction === 'up' || instruction === 'down' ) {
 211+ // ...
 212+ } else if ( instruction === 'home' || instruction === 'end' ) {
 213+ var offset;
 214+ if ( this.cursor.initialBias ) {
 215+ offset = this.documentView.getModel().getRelativeContentOffset(
 216+ this.cursor.offset, -1 );
 217+ } else {
 218+ offset = this.cursor.offset;
 219+ }
 220+ if ( instruction === 'home' ) {
 221+ this.showCursor(
 222+ this.documentView.getRenderedLineRangeFromOffset( offset ).start, false );
 223+ } else { // end
 224+ this.showCursor( this.documentView.getRenderedLineRangeFromOffset( offset ).end, true );
 225+ }
 226+ }
 227+};
 228+
 229+/**
 230+ * Shows the cursor in a new position.
 231+ *
 232+ * @method
 233+ * @param offset {Integer} Position to show the cursor at
 234+ */
 235+es.SurfaceView.prototype.showCursor = function( offset, leftBias ) {
 236+ if ( typeof offset !== 'undefined' ) {
 237+ this.cursor.initialBias = leftBias ? true : false;
 238+ this.cursor.offset = offset;
 239+ var position = this.documentView.getRenderedPositionFromOffset(
 240+ this.cursor.offset, leftBias
 241+ );
 242+ this.cursor.$.css( {
 243+ 'left': position.left,
 244+ 'top': position.top,
 245+ 'height': position.bottom - position.top
 246+ } );
 247+ this.$input.css({
 248+ 'top': position.top,
 249+ 'height': position.bottom - position.top
 250+ });
 251+ }
 252+
 253+ this.cursor.$.show();
 254+
 255+ // cursor blinking
 256+ if ( this.cursor.interval ) {
 257+ clearInterval( this.cursor.interval );
 258+ }
 259+ this.cursor.interval = setInterval( function( surface ) {
 260+ surface.cursor.$.css( 'display', function( index, value ) {
 261+ return value === 'block' ? 'none' : 'block';
 262+ } );
 263+ }, 500, this );
 264+};
 265+
 266+/**
 267+ * Hides the cursor.
 268+ *
 269+ * @method
 270+ */
 271+es.SurfaceView.prototype.hideCursor = function() {
 272+ if( this.cursor.interval ) {
 273+ clearInterval( this.cursor.interval );
 274+ }
 275+ this.cursor.$.hide();
 276+};
Index: trunk/extensions/VisualEditor/modules/views/es.ContentView.js
@@ -0,0 +1,898 @@
 2+/**
 3+ * Creates an es.ContentView object.
 4+ *
 5+ * A content view flows text into a DOM element and provides methods to get information about the
 6+ * rendered output. HTML serialized specifically for rendering into and editing surface.
 7+ *
 8+ * Rendering occurs automatically when content is modified, by responding to "update" events from
 9+ * the model. Rendering is iterative and interruptable to reduce user feedback latency.
 10+ *
 11+ * TODO: Cleanup code and comments
 12+ *
 13+ * @class
 14+ * @constructor
 15+ * @param {jQuery} $container Element to render into
 16+ * @param {es.ModelNode} model Model to produce view for
 17+ * @property {jQuery} $
 18+ * @property {es.ContentModel} model
 19+ * @property {Array} boundaries
 20+ * @property {Array} lines
 21+ * @property {Integer} width
 22+ * @property {RegExp} bondaryTest
 23+ * @property {Object} widthCache
 24+ * @property {Object} renderState
 25+ * @property {Object} contentCache
 26+ */
 27+es.ContentView = function( $container, model ) {
 28+ // Inheritance
 29+ es.EventEmitter.call( this );
 30+
 31+ // Properties
 32+ this.$ = $container;
 33+ this.model = model;
 34+ this.boundaries = [];
 35+ this.lines = [];
 36+ this.width = null;
 37+ this.boundaryTest = /([ \-\t\r\n\f])/g;
 38+ this.widthCache = {};
 39+ this.renderState = {};
 40+ this.contentCache = null;
 41+
 42+ if ( model ) {
 43+ // Events
 44+ var _this = this;
 45+ this.model.on( 'update', function( args ) {
 46+ _this.scanBoundaries();
 47+ _this.render( args ? args.offset : 0 );
 48+ } );
 49+
 50+ // DOM Changes
 51+ this.$ranges = $( '<div class="es-contentView-ranges"></div>' );
 52+ this.$rangeStart = $( '<div class="es-contentView-range"></div>' );
 53+ this.$rangeFill = $( '<div class="es-contentView-range"></div>' );
 54+ this.$rangeEnd = $( '<div class="es-contentView-range"></div>' );
 55+ this.$.prepend( this.$ranges.append( this.$rangeStart, this.$rangeFill, this.$rangeEnd ) );
 56+
 57+ // Initialization
 58+ this.scanBoundaries();
 59+ }
 60+};
 61+
 62+/* Static Members */
 63+
 64+/**
 65+ * List of annotation rendering implementations.
 66+ *
 67+ * Each supported annotation renderer must have an open and close property, each either a string or
 68+ * a function which accepts a data argument.
 69+ *
 70+ * @static
 71+ * @member
 72+ */
 73+es.ContentView.annotationRenderers = {
 74+ 'template': {
 75+ 'open': function( data ) {
 76+ return '<span class="es-contentView-format-object">' + data.html;
 77+ },
 78+ 'close': '</span>',
 79+ 'float': function( data ) {
 80+ console.log( data.html );
 81+ return $( data.html ).css( 'float' );
 82+ }
 83+ },
 84+ 'bold': {
 85+ 'open': '<span class="es-contentView-format-bold">',
 86+ 'close': '</span>',
 87+ 'float': false
 88+ },
 89+ 'italic': {
 90+ 'open': '<span class="es-contentView-format-italic">',
 91+ 'close': '</span>',
 92+ 'float': false
 93+ },
 94+ 'size': {
 95+ 'open': function( data ) {
 96+ return '<span class="es-contentView-format-' + data.type + '">';
 97+ },
 98+ 'close': '</span>',
 99+ 'float': false
 100+ },
 101+ 'script': {
 102+ 'open': function( data ) {
 103+ return '<span class="es-contentView-format-' + data.type + '">';
 104+ },
 105+ 'close': '</span>',
 106+ 'float': false
 107+ },
 108+ 'xlink': {
 109+ 'open': function( data ) {
 110+ return '<span class="es-contentView-format-link" data-href="' + data.href + '">';
 111+ },
 112+ 'close': '</span>',
 113+ 'float': false
 114+ },
 115+ 'ilink': {
 116+ 'open': function( data ) {
 117+ return '<span class="es-contentView-format-link" data-title="' + data.title + '">';
 118+ },
 119+ 'close': '</span>',
 120+ 'float': false
 121+ }
 122+};
 123+
 124+/**
 125+ * Mapping of character and HTML entities or renderings.
 126+ *
 127+ * @static
 128+ * @member
 129+ */
 130+es.ContentView.htmlCharacters = {
 131+ '&': '&amp;',
 132+ '<': '&lt;',
 133+ '>': '&gt;',
 134+ '\'': '&#039;',
 135+ '"': '&quot;',
 136+ '\n': '<span class="es-contentView-whitespace">&#182;</span>',
 137+ '\t': '<span class="es-contentView-whitespace">&#8702;</span>',
 138+ ' ': '&nbsp;'
 139+};
 140+
 141+/* Static Methods */
 142+
 143+/**
 144+ * Gets a rendered opening or closing of an annotation.
 145+ *
 146+ * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
 147+ * argument should be used while rendering content.
 148+ *
 149+ * @static
 150+ * @method
 151+ * @param {String} bias Which side of the annotation to render, either "open" or "close"
 152+ * @param {Object} annotation Annotation to render
 153+ * @param {Array} stack List of currently open annotations
 154+ * @returns {String} Rendered annotation
 155+ */
 156+es.ContentView.renderAnnotation = function( bias, annotation, stack ) {
 157+ var renderers = es.ContentView.annotationRenderers,
 158+ type = annotation.type,
 159+ out = '';
 160+ if ( type in renderers ) {
 161+ if ( bias === 'open' ) {
 162+ // Add annotation to the top of the stack
 163+ stack.push( annotation );
 164+ // Open annotation
 165+ out += typeof renderers[type].open === 'function' ?
 166+ renderers[type].open( annotation.data ) : renderers[type].open;
 167+ } else {
 168+ if ( stack[stack.length - 1] === annotation ) {
 169+ // Remove annotation from top of the stack
 170+ stack.pop();
 171+ // Close annotation
 172+ out += typeof renderers[type].close === 'function' ?
 173+ renderers[type].close( annotation.data ) : renderers[type].close;
 174+ } else {
 175+ // Find the annotation in the stack
 176+ var depth = stack.indexOf( annotation ),
 177+ i;
 178+ if ( depth === -1 ) {
 179+ throw 'Invalid stack error. An element is missing from the stack.';
 180+ }
 181+ // Close each already opened annotation
 182+ for ( i = stack.length - 1; i >= depth + 1; i-- ) {
 183+ out += typeof renderers[stack[i].type].close === 'function' ?
 184+ renderers[stack[i].type].close( stack[i].data ) :
 185+ renderers[stack[i].type].close;
 186+ }
 187+ // Close the buried annotation
 188+ out += typeof renderers[type].close === 'function' ?
 189+ renderers[type].close( annotation.data ) : renderers[type].close;
 190+ // Re-open each previously opened annotation
 191+ for ( i = depth + 1; i < stack.length; i++ ) {
 192+ out += typeof renderers[stack[i].type].open === 'function' ?
 193+ renderers[stack[i].type].open( stack[i].data ) :
 194+ renderers[stack[i].type].open;
 195+ }
 196+ // Remove the annotation from the middle of the stack
 197+ stack.splice( depth, 1 );
 198+ }
 199+ }
 200+ }
 201+ return out;
 202+};
 203+
 204+/* Methods */
 205+
 206+/**
 207+ * Draws selection around a given range of content.
 208+ *
 209+ * @method
 210+ * @param {es.Range} range Range to draw selection around
 211+ */
 212+es.ContentView.prototype.drawSelection = function( range ) {
 213+ range.normalize();
 214+
 215+ var fromLineIndex = this.getRenderedLineIndexFromOffset( range.start ),
 216+ toLineIndex = this.getRenderedLineIndexFromOffset( range.end ),
 217+ fromPosition = this.getRenderedPositionFromOffset( range.start ),
 218+ toPosition = this.getRenderedPositionFromOffset( range.end );
 219+
 220+ if ( fromLineIndex === toLineIndex ) {
 221+ // Single line selection
 222+ this.$rangeStart.css( {
 223+ 'top': fromPosition.top,
 224+ 'left': fromPosition.left,
 225+ 'width': toPosition.left - fromPosition.left,
 226+ 'height': fromPosition.bottom - fromPosition.top
 227+ } ).show();
 228+ this.$rangeFill.hide();
 229+ this.$rangeEnd.hide();
 230+ } else {
 231+ // Multiple line selection
 232+ var contentWidth = this.$.width();
 233+ this.$rangeStart.css( {
 234+ 'top': fromPosition.top,
 235+ 'left': fromPosition.left,
 236+ 'width': contentWidth - fromPosition.left,
 237+ 'height': fromPosition.bottom - fromPosition.top
 238+ } ).show();
 239+ this.$rangeEnd.css( {
 240+ 'top': toPosition.top,
 241+ 'left': 0,
 242+ 'width': toPosition.left,
 243+ 'height': toPosition.bottom - toPosition.top
 244+ } ).show();
 245+ if ( fromLineIndex + 1 < toLineIndex ) {
 246+ this.$rangeFill.css( {
 247+ 'top': fromPosition.bottom,
 248+ 'left': 0,
 249+ 'width': contentWidth,
 250+ 'height': toPosition.top - fromPosition.bottom
 251+ } ).show();
 252+ } else {
 253+ this.$rangeFill.hide();
 254+ }
 255+ }
 256+};
 257+
 258+/**
 259+ * Clears selection if any was drawn.
 260+ *
 261+ * @method
 262+ */
 263+es.ContentView.prototype.clearSelection = function() {
 264+ this.$rangeStart.hide();
 265+ this.$rangeFill.hide();
 266+ this.$rangeEnd.hide();
 267+};
 268+
 269+/**
 270+ * Gets the index of the rendered line a given offset is within.
 271+ *
 272+ * Offsets that are out of range will always return the index of the last line.
 273+ *
 274+ * @method
 275+ * @param {Integer} offset Offset to get line for
 276+ * @returns {Integer} Index of rendered lin offset is within
 277+ */
 278+es.ContentView.prototype.getRenderedLineIndexFromOffset = function( offset ) {
 279+ for ( var i = 0; i < this.lines.length; i++ ) {
 280+ if ( this.lines[i].range.containsOffset( offset ) ) {
 281+ return i;
 282+ }
 283+ }
 284+ return this.lines.length - 1;
 285+};
 286+
 287+/*
 288+ * Gets the index of the rendered line closest to a given position.
 289+ *
 290+ * If the position is above the first line, the offset will always be 0, and if the position is
 291+ * below the last line the offset will always be the content length. All other vertical
 292+ * positions will fall inside of one of the lines.
 293+ *
 294+ * @method
 295+ * @returns {Integer} Index of rendered line closest to position
 296+ */
 297+es.ContentView.prototype.getRenderedLineIndexFromPosition = function( position ) {
 298+ var lineCount = this.lines.length;
 299+ // Positions above the first line always jump to the first offset
 300+ if ( !lineCount || position.top < 0 ) {
 301+ return 0;
 302+ }
 303+ // Find which line the position is inside of
 304+ var i = 0,
 305+ top = 0;
 306+ while ( i < lineCount ) {
 307+ top += this.lines[i].height;
 308+ if ( position.top < top ) {
 309+ break;
 310+ }
 311+ i++;
 312+ }
 313+ // Positions below the last line always jump to the last offset
 314+ if ( i === lineCount ) {
 315+ return i - 1;
 316+ }
 317+ return i;
 318+};
 319+
 320+/**
 321+ * Gets the range of the rendered line a given offset is within.
 322+ *
 323+ * Offsets that are out of range will always return the range of the last line.
 324+ *
 325+ * @method
 326+ * @param {Integer} offset Offset to get line for
 327+ * @returns {es.Range} Range of line offset is within
 328+ */
 329+es.ContentView.prototype.getRenderedLineRangeFromOffset = function( offset ) {
 330+ for ( var i = 0; i < this.lines.length; i++ ) {
 331+ if ( this.lines[i].range.containsOffset( offset ) ) {
 332+ return this.lines[i].range;
 333+ }
 334+ }
 335+ return this.lines[this.lines.length - 1].range;
 336+};
 337+
 338+/**
 339+ * Gets offset within content model closest to of a given position.
 340+ *
 341+ * Position is assumed to be local to the container the text is being flowed in.
 342+ *
 343+ * @method
 344+ * @param {Object} position Position to find offset for
 345+ * @param {Integer} position.left Horizontal position in pixels
 346+ * @param {Integer} position.top Vertical position in pixels
 347+ * @returns {Integer} Offset within content model nearest the given coordinates
 348+ */
 349+es.ContentView.prototype.getOffsetFromRenderedPosition = function( position ) {
 350+ // Empty content model shortcut
 351+ if ( this.model.getContentLength() === 0 ) {
 352+ return 0;
 353+ }
 354+
 355+ // Localize position
 356+ position.subtract( es.Position.newFromElementPagePosition( this.$ ) );
 357+
 358+ // Get the line object nearest the position
 359+ var line = this.lines[this.getRenderedLineIndexFromPosition( position )];
 360+
 361+ /*
 362+ * Offset finding
 363+ *
 364+ * Now that we know which line we are on, we can just use the "fitCharacters" method to get the
 365+ * last offset before "position.left".
 366+ *
 367+ * TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before
 368+ * the cursor.
 369+ */
 370+ var $ruler = $( '<div class="es-contentView-ruler"></div>' ).appendTo( this.$ ),
 371+ ruler = $ruler[0],
 372+ fit = this.fitCharacters( line.range, ruler, position.left ),
 373+ center;
 374+ ruler.innerHTML = this.getHtml( new es.Range( line.range.start, fit.end ) );
 375+ if ( fit.end < this.model.getContentLength() ) {
 376+ var left = ruler.clientWidth;
 377+ ruler.innerHTML = this.getHtml( new es.Range( line.range.start, fit.end + 1 ) );
 378+ center = Math.round( left + ( ( ruler.clientWidth - left ) / 2 ) );
 379+ } else {
 380+ center = ruler.clientWidth;
 381+ }
 382+ $ruler.remove();
 383+ // Reset RegExp object's state
 384+ this.boundaryTest.lastIndex = 0;
 385+ return Math.min(
 386+ // If the position is right of the center of the character it's on top of, increment offset
 387+ fit.end + ( position.left >= center ? 1 : 0 ),
 388+ // Don't allow the value to be higher than the end
 389+ line.range.end
 390+ );
 391+};
 392+
 393+/**
 394+ * Gets position coordinates of a given offset.
 395+ *
 396+ * Offsets are boundaries between plain or annotated characters within content model. Results are
 397+ * given in left, top and bottom positions, which could be used to draw a cursor, highlighting, etc.
 398+ *
 399+ * @method
 400+ * @param {Integer} offset Offset within content model
 401+ * @returns {Object} Object containing left, top and bottom properties, each positions in pixels as
 402+ * well as a line index
 403+ */
 404+es.ContentView.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
 405+ /*
 406+ * Range validation
 407+ *
 408+ * Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is
 409+ * less than 0 or greater than the length of the content model.
 410+ */
 411+ if ( offset < 0 ) {
 412+ throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
 413+ } else if ( offset > this.model.getContentLength() ) {
 414+ throw 'Out of range error. Offset is expected to be less than or equal to text length.';
 415+ }
 416+ /*
 417+ * Line finding
 418+ *
 419+ * It's possible that a more efficient method could be used here, but the number of lines to be
 420+ * iterated through will rarely be over 100, so it's unlikely that any significant gains will be
 421+ * had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom
 422+ * positions, which is a nice benefit of this method.
 423+ */
 424+ var line,
 425+ lineCount = this.lines.length,
 426+ lineIndex = 0,
 427+ position = new es.Position();
 428+ while ( lineIndex < lineCount ) {
 429+ line = this.lines[lineIndex];
 430+ if ( line.range.containsOffset( offset ) || ( leftBias && line.range.end === offset ) ) {
 431+ position.bottom = position.top + line.height;
 432+ break;
 433+ }
 434+ position.top += line.height;
 435+ lineIndex++;
 436+ }
 437+ /*
 438+ * Virtual n+1 position
 439+ *
 440+ * To allow access to position information of the right side of the last character on the last
 441+ * line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause
 442+ * an exception to be thrown.
 443+ */
 444+ if ( lineIndex === lineCount ) {
 445+ position.bottom = position.top;
 446+ position.top -= line.height;
 447+ }
 448+ /*
 449+ * Offset measuring
 450+ *
 451+ * Since the left position will be zero for the first character in the line, so we can skip
 452+ * measuring for those cases.
 453+ */
 454+ if ( line.range.start < offset ) {
 455+ var $ruler = $( '<div class="es-contentView-ruler"></div>' ).appendTo( this.$ ),
 456+ ruler = $ruler[0];
 457+ ruler.innerHTML = this.getHtml( new es.Range( line.range.start, offset ) );
 458+ position.left = ruler.clientWidth;
 459+ $ruler.remove();
 460+ }
 461+ return position;
 462+};
 463+
 464+/**
 465+ * Updates the word boundary cache, which is used for word fitting.
 466+ *
 467+ * @method
 468+ */
 469+es.ContentView.prototype.scanBoundaries = function() {
 470+ /*
 471+ * Word boundary scan
 472+ *
 473+ * To perform binary-search on words, rather than characters, we need to collect word boundary
 474+ * offsets into an array. The offset of the right side of the breaking character is stored, so
 475+ * the gaps between stored offsets always include the breaking character at the end.
 476+ *
 477+ * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
 478+ * build a list of HTML escaped strings for each gap between the offsets stored in the
 479+ * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
 480+ * the words.
 481+ */
 482+ // Get and cache a copy of all content, the make a plain-text version of the cached content
 483+ var data = this.contentCache = this.model.getContent(),
 484+ text = '';
 485+ debugger;
 486+ for ( var i = 0, length = data.length; i < length; i++ ) {
 487+ text += typeof data[i] === 'string' ? data[i] : data[i][0];
 488+ }
 489+ // Purge "boundaries" and "words" arrays
 490+ this.boundaries = [0];
 491+ // Reset RegExp object's state
 492+ this.boundaryTest.lastIndex = 0;
 493+ // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go
 494+ var match,
 495+ end;
 496+ while ( ( match = this.boundaryTest.exec( text ) ) ) {
 497+ // Include the boundary character in the range
 498+ end = match.index + 1;
 499+ // Store the boundary offset
 500+ this.boundaries.push( end );
 501+ }
 502+ // If the last character is not a boundary character, we need to append the final range to the
 503+ // "boundaries" and "words" arrays
 504+ if ( end < text.length || this.boundaries.length === 1 ) {
 505+ this.boundaries.push( text.length );
 506+ }
 507+};
 508+
 509+/**
 510+ * Renders a batch of lines and then yields execution before rendering another batch.
 511+ *
 512+ * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
 513+ * causing them to be fragmented. Word fragments are rendered on their own lines, except for their
 514+ * remainder, which is combined with whatever proceeding words can fit on the same line.
 515+ *
 516+ * @method
 517+ * @param {Integer} limit Maximum number of iterations to render before yeilding
 518+ */
 519+es.ContentView.prototype.renderIteration = function( limit ) {
 520+ var rs = this.renderState,
 521+ iteration = 0,
 522+ fractional = false,
 523+ lineStart = this.boundaries[rs.wordOffset],
 524+ lineEnd,
 525+ wordFit = null,
 526+ charOffset = 0,
 527+ charFit = null,
 528+ wordCount = this.boundaries.length;
 529+ while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) {
 530+ wordFit = this.fitWords( new es.Range( rs.wordOffset, wordCount - 1 ), rs.ruler, rs.width );
 531+ fractional = false;
 532+ if ( wordFit.width > rs.width ) {
 533+ // The first word didn't fit, we need to split it up
 534+ charOffset = lineStart;
 535+ var lineOffset = rs.wordOffset;
 536+ rs.wordOffset++;
 537+ lineEnd = this.boundaries[rs.wordOffset];
 538+ do {
 539+ charFit = this.fitCharacters(
 540+ new es.Range( charOffset, lineEnd ), rs.ruler, rs.width
 541+ );
 542+ // If we were able to get the rest of the characters on the line OK
 543+ if ( charFit.end === lineEnd) {
 544+ // Try to fit more words on the line
 545+ wordFit = this.fitWords(
 546+ new es.Range( rs.wordOffset, wordCount - 1 ),
 547+ rs.ruler,
 548+ rs.width - charFit.width
 549+ );
 550+ if ( wordFit.end > rs.wordOffset ) {
 551+ lineOffset = rs.wordOffset;
 552+ rs.wordOffset = wordFit.end;
 553+ charFit.end = lineEnd = this.boundaries[rs.wordOffset];
 554+ }
 555+ }
 556+ this.appendLine( new es.Range( charOffset, charFit.end ), lineOffset, fractional );
 557+ // Move on to another line
 558+ charOffset = charFit.end;
 559+ // Mark the next line as fractional
 560+ fractional = true;
 561+ } while ( charOffset < lineEnd );
 562+ } else {
 563+ lineEnd = this.boundaries[wordFit.end];
 564+ this.appendLine( new es.Range( lineStart, lineEnd ), rs.wordOffset, fractional );
 565+ rs.wordOffset = wordFit.end;
 566+ }
 567+ lineStart = lineEnd;
 568+ }
 569+ // Only perform on actual last iteration
 570+ if ( rs.wordOffset >= wordCount - 1 ) {
 571+ // Cleanup
 572+ rs.$ruler.remove();
 573+ this.lines = rs.lines;
 574+ this.$.find( '.es-contentView-line[line-index=' + ( this.lines.length - 1 ) + ']' )
 575+ .nextAll()
 576+ .remove();
 577+ rs.timeout = undefined;
 578+ this.emit( 'update' );
 579+ } else {
 580+ rs.ruler.innerHTML = '';
 581+ var that = this;
 582+ rs.timeout = setTimeout( function() {
 583+ that.renderIteration( 3 );
 584+ }, 0 );
 585+ }
 586+};
 587+
 588+/**
 589+ * Renders text into a series of HTML elements, each a single line of wrapped text.
 590+ *
 591+ * The offset parameter can be used to reduce the amount of work involved in re-rendering the same
 592+ * text, but will be automatically ignored if the text or width of the container has changed.
 593+ *
 594+ * Rendering happens asynchronously, and yields execution between iterations. Iterative rendering
 595+ * provides the JavaScript engine an ability to process events between rendering batches of lines,
 596+ * allowing rendering to be interrupted and restarted if changes to content model are happening before
 597+ * rendering of all lines is complete.
 598+ *
 599+ * @method
 600+ * @param {Integer} offset Offset to re-render from, if possible (not yet implemented)
 601+ */
 602+es.ContentView.prototype.render = function( offset ) {
 603+ var rs = this.renderState;
 604+ // Check if rendering is currently underway
 605+ if ( rs.timeout !== undefined ) {
 606+ // Cancel the active rendering process
 607+ clearTimeout( rs.timeout );
 608+ // Cleanup
 609+ rs.$ruler.remove();
 610+ }
 611+ // Clear caches that were specific to the previous render
 612+ this.widthCache = {};
 613+ // In case of empty content model we still want to display empty with non-breaking space inside
 614+ // This is very important for lists
 615+ if(this.model.getContentLength() === 0) {
 616+ var $line = $( '<div class="es-contentView-line" line-index="0">&nbsp;</div>' );
 617+ this.$.empty().append( $line );
 618+ this.lines = [{
 619+ 'text': ' ',
 620+ 'range': new es.Range( 0,0 ),
 621+ 'width': 0,
 622+ 'height': $line.outerHeight(),
 623+ 'wordOffset': 0,
 624+ 'fractional': false
 625+ }];
 626+ this.emit( 'update' );
 627+ return;
 628+ }
 629+ /*
 630+ * Container measurement
 631+ *
 632+ * To get an accurate measurement of the inside of the container, without having to deal with
 633+ * inconsistencies between browsers and box models, we can just create an element inside the
 634+ * container and measure it.
 635+ */
 636+ rs.$ruler = $( '<div>&nbsp;</div>' ).appendTo( this.$ );
 637+ rs.width = rs.$ruler.innerWidth();
 638+ rs.ruler = rs.$ruler.addClass('es-contentView-ruler')[0];
 639+ // Ignore offset optimization if the width has changed or the text has never been flowed before
 640+ if (this.width !== rs.width) {
 641+ offset = undefined;
 642+ }
 643+ this.width = rs.width;
 644+ // Reset the render state
 645+ if ( offset ) {
 646+ var gap,
 647+ currentLine = this.lines.length - 1;
 648+ for ( var i = this.lines.length - 1; i >= 0; i-- ) {
 649+ var line = this.lines[i];
 650+ if ( line.range.start < offset && line.range.end > offset ) {
 651+ currentLine = i;
 652+ }
 653+ if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) {
 654+ rs.lines = this.lines.slice( 0, i );
 655+ rs.wordOffset = line.wordOffset;
 656+ gap = currentLine - i;
 657+ break;
 658+ }
 659+ }
 660+ this.renderIteration( 2 + gap );
 661+ } else {
 662+ rs.lines = [];
 663+ rs.wordOffset = 0;
 664+ this.renderIteration( 3 );
 665+ }
 666+};
 667+
 668+/**
 669+ * Adds a line containing a given range of text to the end of the DOM and the "lines" array.
 670+ *
 671+ * @method
 672+ * @param {es.Range} range Range of data within content model to append
 673+ * @param {Integer} start Beginning of text range for line
 674+ * @param {Integer} end Ending of text range for line
 675+ * @param {Integer} wordOffset Index within this.words which the line begins with
 676+ * @param {Boolean} fractional If the line begins in the middle of a word
 677+ */
 678+es.ContentView.prototype.appendLine = function( range, wordOffset, fractional ) {
 679+ var rs = this.renderState,
 680+ lineCount = rs.lines.length,
 681+ $line = this.$.children( '[line-index=' + lineCount + ']' );
 682+ if ( !$line.length ) {
 683+ $line = $(
 684+ '<div class="es-contentView-line" line-index="' + lineCount + '"></div>'
 685+ );
 686+ this.$.append( $line );
 687+ }
 688+ $line[0].innerHTML = this.getHtml( range );
 689+ // Collect line information
 690+ rs.lines.push({
 691+ 'text': this.model.getText( range ),
 692+ 'range': range,
 693+ 'width': $line.outerWidth(),
 694+ 'height': $line.outerHeight(),
 695+ 'wordOffset': wordOffset,
 696+ 'fractional': fractional
 697+ });
 698+ // Disable links within rendered content
 699+ $line.find( '.es-contentView-format-object a' )
 700+ .mousedown( function( e ) {
 701+ e.preventDefault();
 702+ } )
 703+ .click( function( e ) {
 704+ e.preventDefault();
 705+ } );
 706+};
 707+
 708+/**
 709+ * Gets the index of the boundary of last word that fits inside the line
 710+ *
 711+ * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
 712+ * areas within the text. Using these, we can perform a binary-search for the best fit of words
 713+ * within a line, just as we would with characters.
 714+ *
 715+ * Results are given as an object containing both an index and a width, the later of which can be
 716+ * used to detect when the first word was too long to fit on a line. In such cases the result will
 717+ * contain the index of the boundary of the first word and it's width.
 718+ *
 719+ * TODO: Because limit is most likely given as "words.length", it may be possible to improve the
 720+ * efficiency of this code by making a best guess and working from there, rather than always
 721+ * starting with [offset .. limit], which usually results in reducing the end position in all but
 722+ * the last line, and in most cases more than 3 times, before changing directions.
 723+ *
 724+ * @method
 725+ * @param {es.Range} range Range of data within content model to try to fit
 726+ * @param {HTMLElement} ruler Element to take measurements with
 727+ * @param {Integer} width Maximum width to allow the line to extend to
 728+ * @returns {Integer} Last index within "words" that contains a word that fits
 729+ */
 730+es.ContentView.prototype.fitWords = function( range, ruler, width ) {
 731+ var offset = range.start,
 732+ start = range.start,
 733+ end = range.end,
 734+ charOffset = this.boundaries[offset],
 735+ middle,
 736+ charMiddle,
 737+ lineWidth,
 738+ cacheKey;
 739+ do {
 740+ // Place "middle" directly in the center of "start" and "end"
 741+ middle = Math.ceil( ( start + end ) / 2 );
 742+ charMiddle = this.boundaries[middle];
 743+ // Measure and cache width of substring
 744+ cacheKey = charOffset + ':' + charMiddle;
 745+ // Prepare the line for measurement using pre-escaped HTML
 746+ ruler.innerHTML = this.getHtml( new es.Range( charOffset, charMiddle ) );
 747+ // Test for over/under using width of the rendered line
 748+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 749+ // Test for over/under using width of the rendered line
 750+ if ( lineWidth > width ) {
 751+ // Detect impossible fit (the first word won't fit by itself)
 752+ if (middle - offset === 1) {
 753+ start = middle;
 754+ break;
 755+ }
 756+ // Words after "middle" won't fit
 757+ end = middle - 1;
 758+ } else {
 759+ // Words before "middle" will fit
 760+ start = middle;
 761+ }
 762+ } while ( start < end );
 763+ // Check if we ended by moving end to the left of middle
 764+ if ( end === middle - 1 ) {
 765+ // A final measurement is required
 766+ var charStart = this.boundaries[start];
 767+ ruler.innerHTML = this.getHtml( new es.Range( charOffset, charStart ) );
 768+ lineWidth = this.widthCache[charOffset + ':' + charStart] = ruler.clientWidth;
 769+ }
 770+ return { 'end': start, 'width': lineWidth };
 771+};
 772+
 773+/**
 774+ * Gets the index of the boundary of the last character that fits inside the line
 775+ *
 776+ * Results are given as an object containing both an index and a width, the later of which can be
 777+ * used to detect when the first character was too long to fit on a line. In such cases the result
 778+ * will contain the index of the first character and it's width.
 779+ *
 780+ * @method
 781+ * @param {es.Range} range Range of data within content model to try to fit
 782+ * @param {HTMLElement} ruler Element to take measurements with
 783+ * @param {Integer} width Maximum width to allow the line to extend to
 784+ * @returns {Integer} Last index within "text" that contains a character that fits
 785+ */
 786+es.ContentView.prototype.fitCharacters = function( range, ruler, width ) {
 787+ var offset = range.start,
 788+ start = range.start,
 789+ end = range.end,
 790+ middle,
 791+ lineWidth,
 792+ cacheKey;
 793+ do {
 794+ // Place "middle" directly in the center of "start" and "end"
 795+ middle = Math.ceil( ( start + end ) / 2 );
 796+ // Measure and cache width of substring
 797+ cacheKey = offset + ':' + middle;
 798+ if ( cacheKey in this.widthCache ) {
 799+ lineWidth = this.widthCache[cacheKey];
 800+ } else {
 801+ // Fill the line with a portion of the text, escaped as HTML
 802+ ruler.innerHTML = this.getHtml( new es.Range( offset, middle ) );
 803+ // Test for over/under using width of the rendered line
 804+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 805+ }
 806+ if ( lineWidth > width ) {
 807+ // Detect impossible fit (the first character won't fit by itself)
 808+ if (middle - offset === 1) {
 809+ start = middle - 1;
 810+ break;
 811+ }
 812+ // Words after "middle" won't fit
 813+ end = middle - 1;
 814+ } else {
 815+ // Words before "middle" will fit
 816+ start = middle;
 817+ }
 818+ } while ( start < end );
 819+ // Check if we ended by moving end to the left of middle
 820+ if ( end === middle - 1 ) {
 821+ // Try for cache hit
 822+ cacheKey = offset + ':' + start;
 823+ if ( cacheKey in this.widthCache ) {
 824+ lineWidth = this.widthCache[cacheKey];
 825+ } else {
 826+ // A final measurement is required
 827+ ruler.innerHTML = this.getHtml( new es.Range( offset, start ) );
 828+ lineWidth = this.widthCache[cacheKey] = ruler.clientWidth;
 829+ }
 830+ }
 831+ return { 'end': start, 'width': lineWidth };
 832+};
 833+
 834+/**
 835+ * Gets an HTML rendering of a range of data within content model.
 836+ *
 837+ * @method
 838+ * @param {es.Range} range Range of content to render
 839+ * @param {String} Rendered HTML of data within content model
 840+ */
 841+es.ContentView.prototype.getHtml = function( range, options ) {
 842+ if ( range ) {
 843+ range.normalize();
 844+ } else {
 845+ range = { 'start': 0, 'end': undefined };
 846+ }
 847+ var data = this.contentCache.slice( range.start, range.end ),
 848+ render = es.ContentView.renderAnnotation,
 849+ htmlChars = es.ContentView.htmlCharacters;
 850+ var out = '',
 851+ left = '',
 852+ right,
 853+ leftPlain,
 854+ rightPlain,
 855+ stack = [],
 856+ i,
 857+ j;
 858+ for ( i = 0; i < data.length; i++ ) {
 859+ right = data[i];
 860+ leftPlain = typeof left === 'string';
 861+ rightPlain = typeof right === 'string';
 862+ if ( !leftPlain && rightPlain ) {
 863+ // [formatted][plain] pair, close any annotations for left
 864+ for ( j = 1; j < left.length; j++ ) {
 865+ out += render( 'close', left[j], stack );
 866+ }
 867+ } else if ( leftPlain && !rightPlain ) {
 868+ // [plain][formatted] pair, open any annotations for right
 869+ for ( j = 1; j < right.length; j++ ) {
 870+ out += render( 'open', right[j], stack );
 871+ }
 872+ } else if ( !leftPlain && !rightPlain ) {
 873+ // [formatted][formatted] pair, open/close any differences
 874+ for ( j = 1; j < left.length; j++ ) {
 875+ if ( right.indexOf( left[j] ) === -1 ) {
 876+ out += render( 'close', left[j], stack );
 877+ }
 878+ }
 879+ for ( j = 1; j < right.length; j++ ) {
 880+ if ( left.indexOf( right[j] ) === -1 ) {
 881+ out += render( 'open', right[j], stack );
 882+ }
 883+ }
 884+ }
 885+ out += right[0] in htmlChars ? htmlChars[right[0]] : right[0];
 886+ left = right;
 887+ }
 888+ // Close all remaining tags at the end of the content
 889+ if ( !rightPlain && right ) {
 890+ for ( j = 1; j < right.length; j++ ) {
 891+ out += render( 'close', right[j], stack );
 892+ }
 893+ }
 894+ return out;
 895+};
 896+
 897+/* Inheritance */
 898+
 899+es.extendClass( es.ContentView, es.EventEmitter );
Index: trunk/extensions/VisualEditor/modules/views/es.ListItemView.js
@@ -0,0 +1,49 @@
 2+/**
 3+ * Creates an es.ListItemView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewLeafNode}
 8+ * @param {es.ListItemModel} model List item model to view
 9+ */
 10+es.ListItemView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewLeafNode.call( this, model );
 13+
 14+ // Properties
 15+ this.$icon = $( '<div class="es-listItemView-icon"></div>' ).prependTo( this.$ );
 16+
 17+ // DOM Changes
 18+ this.$.addClass( 'es-listItemView' );
 19+
 20+ // Events
 21+ this.on( 'update', this.setClasses );
 22+
 23+ // Initialization
 24+ this.setClasses();
 25+};
 26+
 27+es.ListItemView.prototype.setClasses = function() {
 28+ var classes = this.$.attr( 'class' ),
 29+ styles = this.model.getElementAttribute( 'styles' );
 30+ this.$
 31+ // Remove any existing level classes
 32+ .attr(
 33+ 'class',
 34+ classes
 35+ .replace( /es-listItemView-level[0-9]+/, '' )
 36+ .replace( /es-listItemView-(bullet|number)/, '' )
 37+ )
 38+ // Set the list style class from the style on top of the stack
 39+ .addClass( 'es-listItemView-' + styles[styles.length - 1] )
 40+ // Set the list level class from the length of the stack
 41+ .addClass( 'es-listItemView-level' + ( styles.length - 1 ) );
 42+};
 43+
 44+es.ListItemView.prototype.setNumber = function( number ) {
 45+ this.$icon.text( number + '.' );
 46+};
 47+
 48+/* Inheritance */
 49+
 50+es.extendClass( es.ListItemView, es.DocumentViewLeafNode );
Index: trunk/extensions/VisualEditor/modules/views/es.ListView.js
@@ -0,0 +1,45 @@
 2+/**
 3+ * Creates an es.ListView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.ListModel} model List model to view
 9+ */
 10+es.ListView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewBranchNode.call( this, model );
 13+
 14+ // DOM Changes
 15+ this.$.addClass( 'es-listView' );
 16+
 17+ // Events
 18+ this.on( 'update', this.enumerate );
 19+
 20+ // Initialization
 21+ this.enumerate();
 22+};
 23+
 24+/**
 25+ * Set the number labels of all ordered list items.
 26+ *
 27+ * @method
 28+ */
 29+es.ListView.prototype.enumerate = function() {
 30+ var styles,
 31+ levels = [];
 32+ for ( var i = 0; i < this.children.length; i++ ) {
 33+ styles = this.children[i].model.getElementAttribute( 'styles' );
 34+ levels = levels.slice( 0, styles.length );
 35+ if ( styles[styles.length - 1] === 'number' ) {
 36+ if ( !levels[styles.length - 1] ) {
 37+ levels[styles.length - 1] = 0;
 38+ }
 39+ this.children[i].setNumber( ++levels[styles.length - 1] );
 40+ }
 41+ }
 42+};
 43+
 44+/* Inheritance */
 45+
 46+es.extendClass( es.ListView, es.DocumentViewBranchNode );
Index: trunk/extensions/VisualEditor/modules/views/es.DocumentView.js
@@ -0,0 +1,35 @@
 2+/**
 3+ * Creates an es.DocumentView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.DocumentModel} documentModel Document model to view
 9+ * @param {es.SurfaceView} surfaceView Surface view this view is a child of
 10+ */
 11+es.DocumentView = function( model, surfaceView ) {
 12+ // Inheritance
 13+ es.DocumentViewBranchNode.call( this, model );
 14+
 15+ // Properties
 16+ this.surfaceView = surfaceView;
 17+
 18+ // DOM Changes
 19+ this.$.addClass( 'es-documentView' );
 20+};
 21+
 22+/**
 23+ * Get the document offset of a position created from passed DOM event
 24+ *
 25+ * @method
 26+ * @param e {Event} Event to create es.Position from
 27+ * @returns {Integer} Document offset
 28+ */
 29+es.DocumentView.prototype.getOffsetFromEvent = function( e ) {
 30+ var position = es.Position.newFromEventPagePosition( e );
 31+ return this.getOffsetFromRenderedPosition( position );
 32+};
 33+
 34+/* Inheritance */
 35+
 36+es.extendClass( es.DocumentView, es.DocumentViewBranchNode );
Index: trunk/extensions/VisualEditor/modules/views/es.HeadingView.js
@@ -0,0 +1,37 @@
 2+/**
 3+ * Creates an es.HeadingView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewLeafNode}
 8+ * @param {es.HeadingModel} model Heading model to view
 9+ */
 10+es.HeadingView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewLeafNode.call( this, model, $( '<h' + model.getElementAttribute( 'level' ) + '>') );
 13+
 14+ // DOM Changes
 15+ this.$.addClass( 'es-headingView' );
 16+
 17+ // Events
 18+ this.on( 'update', this.setClasses );
 19+
 20+ // Initialization
 21+ this.setClasses();
 22+};
 23+
 24+/* Methods */
 25+
 26+es.HeadingView.prototype.setClasses = function() {
 27+ var classes = this.$.attr( 'class' ),
 28+ level = this.model.getElementAttribute( 'level' );
 29+ this.$
 30+ // Remove any existing level classes
 31+ .attr( 'class', classes.replace( /es-headingView-level[1-6]/, '' ) )
 32+ // Add a new level class
 33+ .addClass( 'es-headingView-level' + level );
 34+};
 35+
 36+/* Inheritance */
 37+
 38+es.extendClass( es.HeadingView, es.DocumentViewLeafNode );
Index: trunk/extensions/VisualEditor/modules/views/es.TableRowView.js
@@ -0,0 +1,21 @@
 2+/**
 3+ * Creates an es.TableRowView object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {es.DocumentViewBranchNode}
 8+ * @param {es.TableRowModel} model Table row model to view
 9+ */
 10+es.TableRowView = function( model ) {
 11+ // Inheritance
 12+ es.DocumentViewBranchNode.call( this, model, $( '<tr>' ), true );
 13+
 14+ // DOM Changes
 15+ this.$
 16+ .attr( 'style', model.getElementAttribute( 'html/style' ) )
 17+ .addClass( 'es-tableRowView' );
 18+};
 19+
 20+/* Inheritance */
 21+
 22+es.extendClass( es.TableRowView, es.DocumentViewBranchNode );

Status & tagging log