r110815 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r110814‎ | r110815 | r110816 >
Date:01:43, 7 February 2012
Author:inez
Status:deferred
Tags:nodeploy, visualeditor 
Comment:
Adapt ContentEditable code to new schema of directories and files
Modified paths:
  • /trunk/extensions/VisualEditor/demo-ce (added) (history)
  • /trunk/extensions/VisualEditor/demo-ce/index.php (added) (history)
  • /trunk/extensions/VisualEditor/demo-ce/main.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/nodes (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/nodes/ve.es.DocumentNode.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/styles (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/styles/ve.es.Document.css (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/styles/ve.es.Surface.css (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Content.js (added) (history)
  • /trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Surface.js (added) (history)

Diff [purge]

Index: trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Surface.js
@@ -0,0 +1,1020 @@
 2+/**
 3+ * Creates an ve.es.Surface object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @param {jQuery} $container DOM Container to render surface into
 8+ * @param {ve.dm.Surface} model Surface model to view
 9+ */
 10+ve.es.Surface = function( $container, model ) {
 11+ // Inheritance
 12+ ve.EventEmitter.call( this );
 13+
 14+ // References for use in closures
 15+ var _this = this,
 16+ $document = $( document ),
 17+ $window = $( window );
 18+
 19+ // Properties
 20+ this.model = model;
 21+ this.currentSelection = new ve.Range();
 22+ this.documentView = new ve.es.DocumentNode( this.model.getDocument(), this );
 23+ this.contextView = null;
 24+ this.$ = $container
 25+ .addClass( 'es-surfaceView' )
 26+ .append( this.documentView.$ );
 27+ this.$input = $( '<textarea class="es-surfaceView-textarea" autocapitalize="off" />' )
 28+ .appendTo( 'body' );
 29+ this.$cursor = $( '<div class="es-surfaceView-cursor"></div>' )
 30+ .appendTo( 'body' );
 31+ this.insertionAnnotations = [];
 32+ this.updateSelectionTimeout = undefined;
 33+ this.emitUpdateTimeout = undefined;
 34+ this.emitCursorTimeout = undefined;
 35+
 36+ // Interaction states
 37+
 38+ /*
 39+ * There are three different selection modes available for mouse. Selection of:
 40+ * 1 - chars
 41+ * 2 - words
 42+ * 3 - nodes (e.g. paragraph, listitem)
 43+ *
 44+ * In case of 2 and 3 selectedRange stores the range of original selection caused by double
 45+ * or triple mousedowns.
 46+ */
 47+ this.mouse = {
 48+ selectingMode: null,
 49+ selectedRange: null
 50+ };
 51+ this.cursor = {
 52+ interval: null,
 53+ initialLeft: null,
 54+ initialBias: false
 55+ };
 56+ this.keyboard = {
 57+ selecting: false,
 58+ cursorAnchor: null,
 59+ keydownTimeout: null,
 60+ keys: { shift: false }
 61+ };
 62+ this.dimensions = {
 63+ width: this.$.width(),
 64+ height: $window.height(),
 65+ scrollTop: $window.scrollTop(),
 66+ // XXX: This is a dirty hack!
 67+ toolbarHeight: $( '#es-toolbar' ).height()
 68+ };
 69+
 70+ // Events
 71+ /*
 72+ this.model.on( 'select', function( selection ) {
 73+ // Keep a copy of the current selection on hand
 74+ _this.currentSelection = selection.clone();
 75+ // Respond to selection changes
 76+ _this.updateSelection();
 77+ if ( selection.getLength() ) {
 78+ _this.$input.val( _this.documentView.model.getContentText( selection ) ).select();
 79+ _this.clearInsertionAnnotations();
 80+ } else {
 81+ _this.$input.val('').select();
 82+ _this.loadInsertionAnnotations();
 83+ }
 84+ } );
 85+ */
 86+ this.model.getDocument().on( 'update', function() {
 87+ //_this.emitUpdate( 25 );
 88+ } );
 89+ this.on( 'update', function() {
 90+ //_this.updateSelection( 25 );
 91+ } );
 92+ this.$.mousedown( function(e) {
 93+ //return _this.onMouseDown( e );
 94+ } );
 95+ this.$input.bind( {
 96+ 'focus': function() {
 97+ // Make sure we aren't double-binding
 98+ $document.unbind( '.es-surfaceView' );
 99+ // Bind mouse and key events to the document to ensure we don't miss anything
 100+ $document.bind( {
 101+ 'mousemove.es-surfaceView': function(e) {
 102+ return _this.onMouseMove( e );
 103+ },
 104+ 'mouseup.es-surfaceView': function(e) {
 105+ return _this.onMouseUp( e );
 106+ },
 107+ 'keydown.es-surfaceView': function( e ) {
 108+ return _this.onKeyDown( e );
 109+ },
 110+ 'keyup.es-surfaceView': function( e ) {
 111+ return _this.onKeyUp( e );
 112+ },
 113+ 'copy.es-surfaceView': function( e ) {
 114+ return _this.onCopy( e );
 115+ },
 116+ 'cut.es-surfaceView': function( e ) {
 117+ return _this.onCut( e );
 118+ },
 119+ 'paste.es-surfaceView': function( e ) {
 120+ return _this.onPaste( e );
 121+ }
 122+ } );
 123+ },
 124+ 'blur': function( e ) {
 125+ // Release our event handlers when not focused
 126+ $document.unbind( '.es-surfaceView' );
 127+ _this.hideCursor();
 128+ },
 129+ 'paste': function() {
 130+ setTimeout( function() {
 131+ _this.model.breakpoint();
 132+ _this.insertFromInput();
 133+ _this.model.breakpoint();
 134+ }, 0 );
 135+ }
 136+ } );
 137+ $window.bind( {
 138+ 'resize': function() {
 139+ // Re-render when resizing horizontally
 140+ // TODO: Instead of re-rendering on every single 'resize' event wait till user is done
 141+ // with resizing - can be implemented with setTimeout
 142+ _this.hideCursor();
 143+ _this.dimensions.height = $window.height();
 144+ // XXX: This is a dirty hack!
 145+ _this.dimensions.toolbarHeight = $( '#es-toolbar' ).height();
 146+ var width = _this.$.width();
 147+ if ( _this.dimensions.width !== width ) {
 148+ _this.dimensions.width = width;
 149+ _this.documentView.renderContent();
 150+ _this.emitUpdate( 25 );
 151+ }
 152+ },
 153+ 'scroll': function() {
 154+ _this.dimensions.scrollTop = $window.scrollTop();
 155+ if ( _this.contextView ) {
 156+ if ( _this.currentSelection.getLength() && !_this.mouse.selectingMode ) {
 157+ _this.contextView.set();
 158+ } else {
 159+ _this.contextView.clear();
 160+ }
 161+ }
 162+ },
 163+ 'blur': function() {
 164+ _this.keyboard.keys.shift = false;
 165+ }
 166+ } );
 167+
 168+ // Configuration
 169+ this.mac = navigator.userAgent.match(/mac/i) ? true : false; // (yes it's evil, for keys only!)
 170+ this.ie8 = $.browser.msie && $.browser.version === "8.0";
 171+
 172+ // Initialization
 173+ this.$input.focus();
 174+ this.documentView.renderContent();
 175+};
 176+
 177+/* Methods */
 178+
 179+ve.es.Surface.prototype.attachContextView = function( contextView ) {
 180+ this.contextView = contextView;
 181+};
 182+
 183+ve.es.Surface.prototype.getContextView = function() {
 184+ return this.contextView ;
 185+};
 186+
 187+ve.es.Surface.prototype.annotate = function( method, annotation ) {
 188+ if ( method === 'toggle' ) {
 189+ var annotations = this.getAnnotations();
 190+ if ( ve.dm.DocumentNode.getIndexOfAnnotation( annotations.full, annotation ) !== -1 ) {
 191+ method = 'clear';
 192+ } else {
 193+ method = 'set';
 194+ }
 195+ }
 196+ if ( this.currentSelection.getLength() ) {
 197+ var tx = this.model.getDocument().prepareContentAnnotation(
 198+ this.currentSelection, method, annotation
 199+ );
 200+ this.model.transact( tx );
 201+ } else {
 202+ if ( method === 'set' ) {
 203+ this.addInsertionAnnotation( annotation );
 204+ } else if ( method === 'clear' ) {
 205+ this.removeInsertionAnnotation( annotation );
 206+ }
 207+ }
 208+};
 209+
 210+ve.es.Surface.prototype.getAnnotations = function() {
 211+ return this.currentSelection.getLength() ?
 212+ this.model.getDocument().getAnnotationsFromRange( this.currentSelection ) :
 213+ {
 214+ 'full': this.insertionAnnotations,
 215+ 'partial': [],
 216+ 'all': this.insertionAnnotations
 217+ };
 218+};
 219+
 220+ve.es.Surface.prototype.emitCursor = function() {
 221+ if ( this.emitCursorTimeout ) {
 222+ clearTimeout( this.emitCursorTimeout );
 223+ }
 224+ var _this = this;
 225+ this.emitCursorTimeout = setTimeout( function() {
 226+ var annotations = _this.getAnnotations(),
 227+ nodes = [],
 228+ model = _this.documentView.model;
 229+ if ( _this.currentSelection.from === _this.currentSelection.to ) {
 230+ nodes.push( model.getNodeFromOffset( _this.currentSelection.from ) );
 231+ } else {
 232+ var startNode = model.getNodeFromOffset( _this.currentSelection.start ),
 233+ endNode = model.getNodeFromOffset( _this.currentSelection.end );
 234+ if ( startNode === endNode ) {
 235+ nodes.push( startNode );
 236+ } else {
 237+ model.traverseLeafNodes( function( node ) {
 238+ nodes.push( node );
 239+ if( node === endNode ) {
 240+ return false;
 241+ }
 242+ }, startNode );
 243+ }
 244+ }
 245+ _this.emit( 'cursor', annotations, nodes );
 246+ }, 50 );
 247+};
 248+
 249+ve.es.Surface.prototype.getInsertionAnnotations = function() {
 250+ return this.insertionAnnotations;
 251+};
 252+
 253+ve.es.Surface.prototype.addInsertionAnnotation = function( annotation ) {
 254+ this.insertionAnnotations.push( annotation );
 255+ this.emitCursor();
 256+};
 257+
 258+ve.es.Surface.prototype.loadInsertionAnnotations = function( annotation ) {
 259+ this.insertionAnnotations =
 260+ this.model.getDocument().getAnnotationsFromOffset( this.currentSelection.to - 1 );
 261+ // Filter out annotations that aren't textStyles or links
 262+ for ( var i = 0; i < this.insertionAnnotations.length; i++ ) {
 263+ if ( !this.insertionAnnotations[i].type.match( /(textStyle\/|link\/)/ ) ) {
 264+ this.insertionAnnotations.splice( i, 1 );
 265+ i--;
 266+ }
 267+ }
 268+ this.emitCursor();
 269+};
 270+
 271+ve.es.Surface.prototype.removeInsertionAnnotation = function( annotation ) {
 272+ var index = ve.dm.DocumentNode.getIndexOfAnnotation( this.insertionAnnotations, annotation );
 273+ if ( index !== -1 ) {
 274+ this.insertionAnnotations.splice( index, 1 );
 275+ }
 276+ this.emitCursor();
 277+};
 278+
 279+ve.es.Surface.prototype.clearInsertionAnnotations = function() {
 280+ this.insertionAnnotations = [];
 281+ this.emitCursor();
 282+};
 283+
 284+ve.es.Surface.prototype.getModel = function() {
 285+ return this.model;
 286+};
 287+
 288+ve.es.Surface.prototype.updateSelection = function( delay ) {
 289+ var _this = this;
 290+ function update() {
 291+ if ( _this.currentSelection.getLength() ) {
 292+ _this.clearInsertionAnnotations();
 293+ _this.hideCursor();
 294+ _this.documentView.drawSelection( _this.currentSelection );
 295+ } else {
 296+ _this.showCursor();
 297+ _this.documentView.clearSelection( _this.currentSelection );
 298+ }
 299+ if ( _this.contextView ) {
 300+ if ( _this.currentSelection.getLength() && !_this.mouse.selectingMode ) {
 301+ _this.contextView.set();
 302+ } else {
 303+ _this.contextView.clear();
 304+ }
 305+ }
 306+ _this.updateSelectionTimeout = undefined;
 307+ }
 308+ if ( delay ) {
 309+ if ( this.updateSelectionTimeout !== undefined ) {
 310+ return;
 311+ }
 312+ this.updateSelectionTimeout = setTimeout( update, delay );
 313+ } else {
 314+ update();
 315+ }
 316+};
 317+
 318+ve.es.Surface.prototype.emitUpdate = function( delay ) {
 319+ if ( delay ) {
 320+ if ( this.emitUpdateTimeout !== undefined ) {
 321+ return;
 322+ }
 323+ var _this = this;
 324+ this.emitUpdateTimeout = setTimeout( function() {
 325+ _this.emit( 'update' );
 326+ _this.emitUpdateTimeout = undefined;
 327+ }, delay );
 328+ } else {
 329+ this.emit( 'update' );
 330+ }
 331+};
 332+
 333+ve.es.Surface.prototype.onMouseDown = function( e ) {
 334+ // Only for left mouse button
 335+ if ( e.which === 1 ) {
 336+ var selection = this.currentSelection.clone(),
 337+ offset = this.documentView.getOffsetFromEvent( e );
 338+ // Single click
 339+ if ( this.ie8 || e.originalEvent.detail === 1 ) {
 340+ // @see {ve.es.Surface.prototype.onMouseMove}
 341+ this.mouse.selectingMode = 1;
 342+
 343+ if ( this.keyboard.keys.shift && offset !== selection.from ) {
 344+ // Extend current or create new selection
 345+ selection.to = offset;
 346+ } else {
 347+ selection.from = selection.to = offset;
 348+
 349+ var position = ve.Position.newFromEventPagePosition( e ),
 350+ nodeView = this.documentView.getNodeFromOffset( offset, false );
 351+ this.cursor.initialBias = position.left > nodeView.contentView.$.offset().left;
 352+ }
 353+ }
 354+ // Double click
 355+ else if ( e.originalEvent.detail === 2 ) {
 356+ // @see {ve.es.Surface.prototype.onMouseMove}
 357+ this.mouse.selectingMode = 2;
 358+
 359+ var wordRange = this.model.getDocument().getWordBoundaries( offset );
 360+ if( wordRange ) {
 361+ selection = wordRange;
 362+ this.mouse.selectedRange = selection.clone();
 363+ }
 364+ }
 365+ // Triple click
 366+ else if ( e.originalEvent.detail >= 3 ) {
 367+ // @see {ve.es.Surface.prototype.onMouseMove}
 368+ this.mouse.selectingMode = 3;
 369+
 370+ var node = this.documentView.getNodeFromOffset( offset ),
 371+ nodeOffset = this.documentView.getOffsetFromNode( node, false );
 372+
 373+ selection.from = this.model.getDocument().getRelativeContentOffset( nodeOffset, 1 );
 374+ selection.to = this.model.getDocument().getRelativeContentOffset(
 375+ nodeOffset + node.getElementLength(), -1
 376+ );
 377+ this.mouse.selectedRange = selection.clone();
 378+ }
 379+ }
 380+
 381+ var _this = this;
 382+
 383+ function select() {
 384+ if ( e.which === 1 ) {
 385+ // Reset the initial left position
 386+ _this.cursor.initialLeft = null;
 387+ // Apply new selection
 388+ _this.model.select( selection, true );
 389+ }
 390+
 391+ // If the inut isn't already focused, focus it and select it's contents
 392+ if ( !_this.$input.is( ':focus' ) ) {
 393+ _this.$input.focus().select();
 394+ }
 395+ }
 396+
 397+ if ( this.ie8 ) {
 398+ setTimeout( select, 0 );
 399+ } else {
 400+ select();
 401+ }
 402+
 403+ return false;
 404+};
 405+
 406+ve.es.Surface.prototype.onMouseMove = function( e ) {
 407+ // Only with the left mouse button while in selecting mode
 408+ if ( e.which === 1 && this.mouse.selectingMode ) {
 409+ var selection = this.currentSelection.clone(),
 410+ offset = this.documentView.getOffsetFromEvent( e );
 411+
 412+ // Character selection
 413+ if ( this.mouse.selectingMode === 1 ) {
 414+ selection.to = offset;
 415+ }
 416+ // Word selection
 417+ else if ( this.mouse.selectingMode === 2 ) {
 418+ var wordRange = this.model.getDocument().getWordBoundaries( offset );
 419+ if ( wordRange ) {
 420+ if ( wordRange.to <= this.mouse.selectedRange.from ) {
 421+ selection.from = wordRange.from;
 422+ selection.to = this.mouse.selectedRange.to;
 423+ } else {
 424+ selection.from = this.mouse.selectedRange.from;
 425+ selection.to = wordRange.to;
 426+ }
 427+ }
 428+ }
 429+ // Node selection
 430+ else if ( this.mouse.selectingMode === 3 ) {
 431+ // @see {ve.es.Surface.prototype.onMouseMove}
 432+ this.mouse.selectingMode = 3;
 433+
 434+ var nodeRange = this.documentView.getRangeFromNode(
 435+ this.documentView.getNodeFromOffset( offset )
 436+ );
 437+ if ( nodeRange.to <= this.mouse.selectedRange.from ) {
 438+ selection.from = this.model.getDocument().getRelativeContentOffset(
 439+ nodeRange.from, 1
 440+ );
 441+ selection.to = this.mouse.selectedRange.to;
 442+ } else {
 443+ selection.from = this.mouse.selectedRange.from;
 444+ selection.to = this.model.getDocument().getRelativeContentOffset(
 445+ nodeRange.to, -1
 446+ );
 447+ }
 448+ }
 449+ // Apply new selection
 450+ this.model.select( selection, true );
 451+ }
 452+};
 453+
 454+ve.es.Surface.prototype.onMouseUp = function( e ) {
 455+ if ( e.which === 1 ) { // left mouse button
 456+ this.mouse.selectingMode = this.mouse.selectedRange = null;
 457+ this.model.select( this.currentSelection, true );
 458+ if ( this.contextView ) {
 459+ // We have to manually call this because the selection will not have changed between the
 460+ // most recent mousemove and this mouseup
 461+ this.contextView.set();
 462+ }
 463+ }
 464+};
 465+
 466+ve.es.Surface.prototype.onCopy = function( e ) {
 467+ // TODO: Keep a data copy around
 468+ return true;
 469+};
 470+
 471+ve.es.Surface.prototype.onCut = function( e ) {
 472+ var _this = this;
 473+ setTimeout( function() {
 474+ _this.handleDelete();
 475+ }, 10 );
 476+ return true;
 477+};
 478+
 479+ve.es.Surface.prototype.onPaste = function( e ) {
 480+ // TODO: Check if the data copy is the same as what got pasted, and use that instead if so
 481+ return true;
 482+};
 483+
 484+ve.es.Surface.prototype.onKeyDown = function( e ) {
 485+ switch ( e.keyCode ) {
 486+ // Tab
 487+ case 9:
 488+ if ( !e.metaKey && !e.ctrlKey && !e.altKey ) {
 489+ this.$input.val( '\t' );
 490+ this.handleInsert();
 491+ e.preventDefault();
 492+ return false;
 493+ }
 494+ return true;
 495+ // Shift
 496+ case 16:
 497+ this.keyboard.keys.shift = true;
 498+ this.keyboard.selecting = true;
 499+ break;
 500+ // Ctrl
 501+ case 17:
 502+ break;
 503+ // Home
 504+ case 36:
 505+ this.moveCursor( 'left', 'line' );
 506+ break;
 507+ // End
 508+ case 35:
 509+ this.moveCursor( 'right', 'line' );
 510+ break;
 511+ // Left arrow
 512+ case 37:
 513+ if ( !this.mac ) {
 514+ if ( e.ctrlKey ) {
 515+ this.moveCursor( 'left', 'word' );
 516+ } else {
 517+ this.moveCursor( 'left', 'char' );
 518+ }
 519+ } else {
 520+ if ( e.metaKey || e.ctrlKey ) {
 521+ this.moveCursor( 'left', 'line' );
 522+ } else if ( e.altKey ) {
 523+ this.moveCursor( 'left', 'word' );
 524+ } else {
 525+ this.moveCursor( 'left', 'char' );
 526+ }
 527+ }
 528+ break;
 529+ // Up arrow
 530+ case 38:
 531+ if ( !this.mac ) {
 532+ if ( e.ctrlKey ) {
 533+ this.moveCursor( 'up', 'unit' );
 534+ } else {
 535+ this.moveCursor( 'up', 'char' );
 536+ }
 537+ } else {
 538+ if ( e.altKey ) {
 539+ this.moveCursor( 'up', 'unit' );
 540+ } else {
 541+ this.moveCursor( 'up', 'char' );
 542+ }
 543+ }
 544+ break;
 545+ // Right arrow
 546+ case 39:
 547+ if ( !this.mac ) {
 548+ if ( e.ctrlKey ) {
 549+ this.moveCursor( 'right', 'word' );
 550+ } else {
 551+ this.moveCursor( 'right', 'char' );
 552+ }
 553+ } else {
 554+ if ( e.metaKey || e.ctrlKey ) {
 555+ this.moveCursor( 'right', 'line' );
 556+ } else if ( e.altKey ) {
 557+ this.moveCursor( 'right', 'word' );
 558+ } else {
 559+ this.moveCursor( 'right', 'char' );
 560+ }
 561+ }
 562+ break;
 563+ // Down arrow
 564+ case 40:
 565+ if ( !this.mac ) {
 566+ if ( e.ctrlKey ) {
 567+ this.moveCursor( 'down', 'unit' );
 568+ } else {
 569+ this.moveCursor( 'down', 'char' );
 570+ }
 571+ } else {
 572+ if ( e.altKey ) {
 573+ this.moveCursor( 'down', 'unit' );
 574+ } else {
 575+ this.moveCursor( 'down', 'char' );
 576+ }
 577+ }
 578+ break;
 579+ // Backspace
 580+ case 8:
 581+ this.handleDelete( true );
 582+ break;
 583+ // Delete
 584+ case 46:
 585+ this.handleDelete();
 586+ break;
 587+ // Enter
 588+ case 13:
 589+ if ( this.keyboard.keys.shift ) {
 590+ this.$input.val( '\n' );
 591+ this.handleInsert();
 592+ e.preventDefault();
 593+ return false;
 594+ }
 595+ this.handleEnter();
 596+ e.preventDefault();
 597+ break;
 598+ // Insert content (maybe)
 599+ default:
 600+ // Control/command + character combos
 601+ if ( e.metaKey || e.ctrlKey ) {
 602+ switch ( e.keyCode ) {
 603+ // y (redo)
 604+ case 89:
 605+ this.model.redo();
 606+ return false;
 607+ // z (undo/redo)
 608+ case 90:
 609+ if ( this.keyboard.keys.shift ) {
 610+ this.model.redo();
 611+ } else {
 612+ this.model.undo();
 613+ }
 614+ return false;
 615+ // a (select all)
 616+ case 65:
 617+ this.model.select( new ve.Range(
 618+ this.model.getDocument().getRelativeContentOffset( 0, 1 ),
 619+ this.model.getDocument().getRelativeContentOffset(
 620+ this.model.getDocument().getContentLength(), -1
 621+ )
 622+ ), true );
 623+ return false;
 624+ // b (bold)
 625+ case 66:
 626+ this.annotate( 'toggle', {'type': 'textStyle/bold' } );
 627+ return false;
 628+ // i (italic)
 629+ case 73:
 630+ this.annotate( 'toggle', {'type': 'textStyle/italic' } );
 631+ return false;
 632+ // k (hyperlink)
 633+ case 75:
 634+ if ( this.currentSelection.getLength() ) {
 635+ this.contextView.openInspector( 'link' );
 636+ } else {
 637+ var range = this.model.getDocument().getAnnotationBoundaries(
 638+ this.currentSelection.from, { 'type': 'link/internal' }, true
 639+ );
 640+ if ( range ) {
 641+ this.model.select( range );
 642+ this.contextView.openInspector( 'link' );
 643+ }
 644+ }
 645+ return false;
 646+ }
 647+ }
 648+ // Regular text insertion
 649+ this.handleInsert();
 650+ break;
 651+ }
 652+ return true;
 653+};
 654+
 655+ve.es.Surface.prototype.onKeyUp = function( e ) {
 656+ if ( e.keyCode === 16 ) {
 657+ this.keyboard.keys.shift = false;
 658+ if ( this.keyboard.selecting ) {
 659+ this.keyboard.selecting = false;
 660+ }
 661+ }
 662+};
 663+
 664+ve.es.Surface.prototype.handleInsert = function() {
 665+ var _this = this;
 666+ if ( _this.keyboard.keydownTimeout ) {
 667+ clearTimeout( _this.keyboard.keydownTimeout );
 668+ }
 669+ _this.keyboard.keydownTimeout = setTimeout( function () {
 670+ _this.insertFromInput();
 671+ }, 10 );
 672+};
 673+
 674+ve.es.Surface.prototype.handleDelete = function( backspace, isPartial ) {
 675+ var selection = this.currentSelection.clone(),
 676+ sourceOffset,
 677+ targetOffset,
 678+ sourceSplitableNode,
 679+ targetSplitableNode,
 680+ tx;
 681+ if ( selection.from === selection.to ) {
 682+ if ( backspace ) {
 683+ sourceOffset = selection.to;
 684+ targetOffset = this.model.getDocument().getRelativeContentOffset(
 685+ sourceOffset,
 686+ -1
 687+ );
 688+ } else {
 689+ sourceOffset = this.model.getDocument().getRelativeContentOffset(
 690+ selection.to,
 691+ 1
 692+ );
 693+ targetOffset = selection.to;
 694+ }
 695+
 696+ var sourceNode = this.documentView.getNodeFromOffset( sourceOffset, false ),
 697+ targetNode = this.documentView.getNodeFromOffset( targetOffset, false );
 698+
 699+ if ( sourceNode.model.getElementType() === targetNode.model.getElementType() ) {
 700+ sourceSplitableNode = ve.es.Node.getSplitableNode( sourceNode );
 701+ targetSplitableNode = ve.es.Node.getSplitableNode( targetNode );
 702+ }
 703+
 704+ selection.from = selection.to = targetOffset;
 705+ this.model.select( selection );
 706+
 707+ if ( sourceNode === targetNode ||
 708+ ( typeof sourceSplitableNode !== 'undefined' &&
 709+ sourceSplitableNode.getParent() === targetSplitableNode.getParent() ) ) {
 710+ tx = this.model.getDocument().prepareRemoval(
 711+ new ve.Range( targetOffset, sourceOffset )
 712+ );
 713+ this.model.transact( tx, isPartial );
 714+ } else {
 715+ tx = this.model.getDocument().prepareInsertion(
 716+ targetOffset, sourceNode.model.getContentData()
 717+ );
 718+ this.model.transact( tx, isPartial );
 719+
 720+ var nodeToDelete = sourceNode;
 721+ ve.Node.traverseUpstream( nodeToDelete, function( node ) {
 722+ if ( node.getParent().children.length === 1 ) {
 723+ nodeToDelete = node.getParent();
 724+ return true;
 725+ } else {
 726+ return false;
 727+ }
 728+ } );
 729+ var range = new ve.Range();
 730+ range.from = this.documentView.getOffsetFromNode( nodeToDelete, false );
 731+ range.to = range.from + nodeToDelete.getElementLength();
 732+ tx = this.model.getDocument().prepareRemoval( range );
 733+ this.model.transact( tx, isPartial );
 734+ }
 735+ } else {
 736+ // selection removal
 737+ tx = this.model.getDocument().prepareRemoval( selection );
 738+ this.model.transact( tx, isPartial );
 739+ selection.from = selection.to = selection.start;
 740+ this.model.select( selection );
 741+ }
 742+};
 743+
 744+ve.es.Surface.prototype.handleEnter = function() {
 745+ var selection = this.currentSelection.clone(),
 746+ tx;
 747+ if ( selection.from !== selection.to ) {
 748+ this.handleDelete( false, true );
 749+ }
 750+ var node = this.documentView.getNodeFromOffset( selection.to, false ),
 751+ nodeOffset = this.documentView.getOffsetFromNode( node, false );
 752+
 753+ if (
 754+ nodeOffset + node.getContentLength() + 1 === selection.to &&
 755+ node === ve.es.Node.getSplitableNode( node )
 756+ ) {
 757+ tx = this.documentView.model.prepareInsertion(
 758+ nodeOffset + node.getElementLength(),
 759+ [ { 'type': 'paragraph' }, { 'type': '/paragraph' } ]
 760+ );
 761+ this.model.transact( tx );
 762+ selection.from = selection.to = nodeOffset + node.getElementLength() + 1;
 763+ } else {
 764+ var stack = [],
 765+ splitable = false;
 766+
 767+ ve.Node.traverseUpstream( node, function( node ) {
 768+ var elementType = node.model.getElementType();
 769+ if (
 770+ splitable === true &&
 771+ ve.es.DocumentNode.splitRules[ elementType ].children === true
 772+ ) {
 773+ return false;
 774+ }
 775+ stack.splice(
 776+ stack.length / 2,
 777+ 0,
 778+ { 'type': '/' + elementType },
 779+ {
 780+ 'type': elementType,
 781+ 'attributes': ve.copyObject( node.model.element.attributes )
 782+ }
 783+ );
 784+ splitable = ve.es.DocumentNode.splitRules[ elementType ].self;
 785+ return true;
 786+ } );
 787+ tx = this.documentView.model.prepareInsertion( selection.to, stack );
 788+ this.model.transact( tx );
 789+ selection.from = selection.to =
 790+ this.model.getDocument().getRelativeContentOffset( selection.to, 1 );
 791+ }
 792+ this.model.select( selection );
 793+};
 794+
 795+ve.es.Surface.prototype.insertFromInput = function() {
 796+ var selection = this.currentSelection.clone(),
 797+ val = this.$input.val();
 798+ if ( val.length > 0 ) {
 799+ // Check if there was any effective input
 800+ var input = this.$input[0],
 801+ // Internet Explorer
 802+ range = document.selection && document.selection.createRange();
 803+ if (
 804+ // DOM 3.0
 805+ ( 'selectionStart' in input && input.selectionEnd - input.selectionStart ) ||
 806+ // Internet Explorer
 807+ ( range && range.text.length )
 808+ ) {
 809+ // The input is still selected, so the key must not have inserted anything
 810+ return;
 811+ }
 812+
 813+ // Clear the value for more input
 814+ this.$input.val( '' );
 815+
 816+ // Prepare and process a transaction
 817+ var tx;
 818+ if ( selection.from != selection.to ) {
 819+ tx = this.model.getDocument().prepareRemoval( selection );
 820+ this.model.transact( tx, true );
 821+ selection.from = selection.to =
 822+ Math.min( selection.from, selection.to );
 823+ }
 824+ var data = val.split('');
 825+ ve.dm.DocumentNode.addAnnotationsToData( data, this.getInsertionAnnotations() );
 826+ tx = this.model.getDocument().prepareInsertion( selection.from, data );
 827+ this.model.transact( tx );
 828+
 829+ // Move the selection
 830+ selection.from += val.length;
 831+ selection.to += val.length;
 832+ this.model.select( selection );
 833+ }
 834+};
 835+
 836+/**
 837+ * @param {String} direction up | down | left | right
 838+ * @param {String} unit char | word | line | node | page
 839+ */
 840+ve.es.Surface.prototype.moveCursor = function( direction, unit ) {
 841+ if ( direction !== 'up' && direction !== 'down' ) {
 842+ this.cursor.initialLeft = null;
 843+ }
 844+ var selection = this.currentSelection.clone(),
 845+ to,
 846+ offset;
 847+ switch ( direction ) {
 848+ case 'left':
 849+ case 'right':
 850+ switch ( unit ) {
 851+ case 'char':
 852+ case 'word':
 853+ if ( this.keyboard.keys.shift || selection.from === selection.to ) {
 854+ offset = selection.to;
 855+ } else {
 856+ offset = direction === 'left' ? selection.start : selection.end;
 857+ }
 858+ to = this.model.getDocument().getRelativeContentOffset(
 859+ offset,
 860+ direction === 'left' ? -1 : 1
 861+ );
 862+ if ( unit === 'word' ) {
 863+ var wordRange = this.model.getDocument().getWordBoundaries(
 864+ direction === 'left' ? to : offset
 865+ );
 866+ if ( wordRange ) {
 867+ to = direction === 'left' ? wordRange.start : wordRange.end;
 868+ }
 869+ }
 870+ break;
 871+ case 'line':
 872+ offset = this.cursor.initialBias ?
 873+ this.model.getDocument().getRelativeContentOffset(
 874+ selection.to,
 875+ -1) :
 876+ selection.to;
 877+ var range = this.documentView.getRenderedLineRangeFromOffset( offset );
 878+ to = direction === 'left' ? range.start : range.end;
 879+ break;
 880+ default:
 881+ throw new Error( 'unrecognized cursor movement unit' );
 882+ break;
 883+ }
 884+ break;
 885+ case 'up':
 886+ case 'down':
 887+ switch ( unit ) {
 888+ case 'unit':
 889+ var toNode = null;
 890+ this.model.getDocument().traverseLeafNodes(
 891+ function( node ) {
 892+ var doNextChild = toNode === null;
 893+ toNode = node;
 894+ return doNextChild;
 895+ },
 896+ this.documentView.getNodeFromOffset( selection.to, false ).getModel(),
 897+ direction === 'up' ? true : false
 898+ );
 899+ to = this.model.getDocument().getOffsetFromNode( toNode, false ) + 1;
 900+ break;
 901+ case 'char':
 902+ /*
 903+ * Looks for the in-document character position that would match up with the
 904+ * same horizontal position - jumping a few pixels up/down at a time until we
 905+ * reach the next/previous line
 906+ */
 907+ var position = this.documentView.getRenderedPositionFromOffset(
 908+ selection.to,
 909+ this.cursor.initialBias
 910+ );
 911+
 912+ if ( this.cursor.initialLeft === null ) {
 913+ this.cursor.initialLeft = position.left;
 914+ }
 915+ var fakePosition = new ve.Position( this.cursor.initialLeft, position.top ),
 916+ i = 0,
 917+ step = direction === 'up' ? -5 : 5,
 918+ top = this.$.position().top;
 919+
 920+ this.cursor.initialBias = position.left > this.documentView.getNodeFromOffset(
 921+ selection.to, false
 922+ ).contentView.$.offset().left;
 923+
 924+ do {
 925+ i++;
 926+ fakePosition.top += i * step;
 927+ if ( fakePosition.top < top ) {
 928+ break;
 929+ } else if (
 930+ fakePosition.top > top + this.dimensions.height +
 931+ this.dimensions.scrollTop
 932+ ) {
 933+ break;
 934+ }
 935+ fakePosition = this.documentView.getRenderedPositionFromOffset(
 936+ this.documentView.getOffsetFromRenderedPosition( fakePosition ),
 937+ this.cursor.initialBias
 938+ );
 939+ fakePosition.left = this.cursor.initialLeft;
 940+ } while ( position.top === fakePosition.top );
 941+ to = this.documentView.getOffsetFromRenderedPosition( fakePosition );
 942+ break;
 943+ default:
 944+ throw new Error( 'unrecognized cursor movement unit' );
 945+ }
 946+ break;
 947+ default:
 948+ throw new Error( 'unrecognized cursor direction' );
 949+ }
 950+
 951+ if( direction != 'up' && direction != 'down' ) {
 952+ this.cursor.initialBias = direction === 'right' && unit === 'line' ? true : false;
 953+ }
 954+
 955+ if ( this.keyboard.keys.shift && selection.from !== to) {
 956+ selection.to = to;
 957+ } else {
 958+ selection.from = selection.to = to;
 959+ }
 960+ this.model.select( selection, true );
 961+};
 962+
 963+/**
 964+ * Shows the cursor in a new position.
 965+ *
 966+ * @method
 967+ * @param offset {Integer} Position to show the cursor at
 968+ */
 969+ve.es.Surface.prototype.showCursor = function() {
 970+ var $window = $( window ),
 971+ position = this.documentView.getRenderedPositionFromOffset(
 972+ this.currentSelection.to, this.cursor.initialBias
 973+ );
 974+
 975+ this.$cursor.css( {
 976+ 'left': position.left,
 977+ 'top': position.top,
 978+ 'height': position.bottom - position.top
 979+ } ).show();
 980+ this.$input.css({
 981+ 'top': position.top,
 982+ 'height': position.bottom - position.top
 983+ });
 984+
 985+ // Auto scroll to cursor
 986+ var inputTop = this.$input.offset().top,
 987+ inputBottom = inputTop + position.bottom - position.top;
 988+ if ( inputTop - this.dimensions.toolbarHeight < this.dimensions.scrollTop ) {
 989+ $window.scrollTop( inputTop - this.dimensions.toolbarHeight );
 990+ } else if ( inputBottom > ( this.dimensions.scrollTop + this.dimensions.height ) ) {
 991+ $window.scrollTop( inputBottom - this.dimensions.height );
 992+ }
 993+
 994+ // cursor blinking
 995+ if ( this.cursor.interval ) {
 996+ clearInterval( this.cursor.interval );
 997+ }
 998+
 999+ var _this = this;
 1000+ this.cursor.interval = setInterval( function( surface ) {
 1001+ _this.$cursor.css( 'display', function( index, value ) {
 1002+ return value === 'block' ? 'none' : 'block';
 1003+ } );
 1004+ }, 500 );
 1005+};
 1006+
 1007+/**
 1008+ * Hides the cursor.
 1009+ *
 1010+ * @method
 1011+ */
 1012+ve.es.Surface.prototype.hideCursor = function() {
 1013+ if( this.cursor.interval ) {
 1014+ clearInterval( this.cursor.interval );
 1015+ }
 1016+ this.$cursor.hide();
 1017+};
 1018+
 1019+/* Inheritance */
 1020+
 1021+ve.extendClass( ve.es.Surface, ve.EventEmitter );
Index: trunk/extensions/VisualEditor/modules/ve/ce/styles/ve.es.Document.css
@@ -0,0 +1,159 @@
 2+.es-documentView {
 3+ cursor: text;
 4+ margin-top: 1em;
 5+ overflow: hidden;
 6+ /*-webkit-user-select: none;*/
 7+}
 8+
 9+.es-headingView,
 10+.es-tableView,
 11+.es-listView,
 12+.es-preView,
 13+.es-paragraphView {
 14+ margin: 1em;
 15+ margin-top: 0;
 16+ position: relative;
 17+ min-height: 1.5em;
 18+}
 19+
 20+.es-listItemView > .es-paragraphView {
 21+ margin-left: 0;
 22+ margin-right: 0;
 23+}
 24+.es-listItemView > .es-viewBranchNode-firstChild {
 25+ margin: 0;
 26+}
 27+
 28+.es-preView {
 29+ padding: 1em;
 30+ border: 1px dashed #2F6FAB;
 31+}
 32+.es-preView > * {
 33+ font-family: monospace,"Courier New";
 34+}
 35+
 36+.es-headingView-level1,
 37+.es-headingView-level2 {
 38+ border-bottom: 1px solid #AAA;
 39+}
 40+
 41+.es-headingView-level1 > * {
 42+ font-size: 188%;
 43+ font-weight: normal;
 44+}
 45+
 46+.es-headingView-level2 > * {
 47+ font-size: 150%;
 48+ font-weight: normal;
 49+}
 50+
 51+.es-headingView-level3 > * {
 52+ font-size: 132%;
 53+ font-weight: bold;
 54+}
 55+
 56+.es-headingView-level4 > * {
 57+ font-size: 116%;
 58+ font-weight: bold;
 59+}
 60+
 61+.es-headingView-level5 > * {
 62+ font-size: 100%;
 63+ font-weight: bold;
 64+}
 65+
 66+.es-headingView-level6 > * {
 67+ font-size: 80%;
 68+ font-weight: bold;
 69+}
 70+
 71+.es-listItemView {
 72+ position: relative;
 73+}
 74+
 75+.es-listItemView-bullet {
 76+ padding-left: 1.2em;
 77+}
 78+
 79+.es-listItemView-number {
 80+ padding-left: 3.2em;
 81+}
 82+
 83+.es-listItemView-icon {
 84+ position: absolute;
 85+ right: 100%;
 86+ height: 1.5em;
 87+ line-height: 1.5em;
 88+}
 89+
 90+.es-listItemView-bullet .es-listItemView-icon {
 91+ background-image: url(images/bullet-icon.png);
 92+ background-position: left 0.6em;
 93+ background-repeat: no-repeat;
 94+ width: 5px;
 95+ margin-right: -0.5em;
 96+}
 97+
 98+.es-listItemView-number .es-listItemView-icon {
 99+ margin-right: -2.8em;
 100+}
 101+
 102+.es-listItemView-term {
 103+ font-weight: bold;
 104+}
 105+
 106+.es-listItemView-definition .es-contentView {
 107+ margin-left: 2em;
 108+}
 109+
 110+.es-listItemView-level0 {
 111+ margin-left: 0;
 112+}
 113+
 114+.es-listItemView-level1 {
 115+ margin-left: 2em;
 116+}
 117+
 118+.es-listItemView-level2 {
 119+ margin-left: 4em;
 120+}
 121+
 122+.es-listItemView-level3 {
 123+ margin-left: 6em;
 124+}
 125+
 126+.es-listItemView-level4 {
 127+ margin-left: 8em;
 128+}
 129+
 130+.es-listItemView-level5 {
 131+ margin-left: 10em;
 132+}
 133+
 134+.es-listItemView-level6 {
 135+ margin-left: 12em;
 136+}
 137+
 138+.es-listItemView-level1.es-listItemView-number {
 139+ margin-left: 4em;
 140+}
 141+
 142+.es-listItemView-level2.es-listItemView-number {
 143+ margin-left: 8em;
 144+}
 145+
 146+.es-listItemView-level3.es-listItemView-number {
 147+ margin-left: 12em;
 148+}
 149+
 150+.es-listItemView-level4.es-listItemView-number {
 151+ margin-left: 16em;
 152+}
 153+
 154+.es-listItemView-level5.es-listItemView-number {
 155+ margin-left: 18em;
 156+}
 157+
 158+.es-listItemView-level6.es-listItemView-number {
 159+ margin-left: 22em;
 160+}
Index: trunk/extensions/VisualEditor/modules/ve/ce/styles/ve.es.Surface.css
@@ -0,0 +1,36 @@
 2+.es-surfaceView {
 3+ overflow: hidden;
 4+ font-size: 1em; /* to look more like MediaWiki use: 0.8em */;
 5+ margin-left: -1em;
 6+ margin-right: -1em;
 7+ /*
 8+ -webkit-user-select: none;
 9+ -moz-user-select: none;
 10+ -ms-user-select: none;
 11+ -o-user-select;
 12+ user-select: none;
 13+ */
 14+}
 15+
 16+.es-surfaceView-textarea {
 17+ position: absolute;
 18+ z-index: -1;
 19+ opacity: 0;
 20+ color: white;
 21+ background-color: white;
 22+ border: none;
 23+ padding: 0;
 24+ margin: 0;
 25+ width: 1px;
 26+}
 27+
 28+.es-surfaceView-textarea:focus {
 29+ outline: none;
 30+}
 31+
 32+.es-surfaceView-cursor {
 33+ position: absolute;
 34+ background-color: black;
 35+ width: 1px;
 36+ display: none;
 37+}
Index: trunk/extensions/VisualEditor/modules/ve/ce/ve.es.Content.js
@@ -0,0 +1,929 @@
 2+/**
 3+ * Creates an ve.es.Content 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 {ve.ModelNode} model Model to produce view for
 17+ * @property {jQuery} $
 18+ * @property {ve.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+ve.es.Content = function( $container, model ) {
 28+ // Inheritance
 29+ ve.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( offset ) {
 46+ _this.scanBoundaries();
 47+ _this.render( 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+ve.es.Content.annotationRenderers = {
 74+ 'object/template': {
 75+ 'open': function( data ) {
 76+ return '<span class="es-contentView-format-object">' + data.html;
 77+ },
 78+ 'close': '</span>'
 79+ },
 80+ 'object/hook': {
 81+ 'open': function( data ) {
 82+ return '<span class="es-contentView-format-object">' + data.html;
 83+ },
 84+ 'close': '</span>'
 85+ },
 86+ 'textStyle/bold': {
 87+ 'open': '<span class="es-contentView-format-textStyle-bold">',
 88+ 'close': '</span>'
 89+ },
 90+ 'textStyle/italic': {
 91+ 'open': '<span class="es-contentView-format-textStyle-italic">',
 92+ 'close': '</span>'
 93+ },
 94+ 'textStyle/strong': {
 95+ 'open': '<span class="es-contentView-format-textStyle-strong">',
 96+ 'close': '</span>'
 97+ },
 98+ 'textStyle/emphasize': {
 99+ 'open': '<span class="es-contentView-format-textStyle-emphasize">',
 100+ 'close': '</span>'
 101+ },
 102+ 'textStyle/big': {
 103+ 'open': '<span class="es-contentView-format-textStyle-big">',
 104+ 'close': '</span>'
 105+ },
 106+ 'textStyle/small': {
 107+ 'open': '<span class="es-contentView-format-textStyle-small">',
 108+ 'close': '</span>'
 109+ },
 110+ 'textStyle/superScript': {
 111+ 'open': '<span class="es-contentView-format-textStyle-superScript">',
 112+ 'close': '</span>'
 113+ },
 114+ 'textStyle/subScript': {
 115+ 'open': '<span class="es-contentView-format-textStyle-subScript">',
 116+ 'close': '</span>'
 117+ },
 118+ 'link/external': {
 119+ 'open': function( data ) {
 120+ return '<span class="es-contentView-format-link" data-href="' + data.href + '">';
 121+ },
 122+ 'close': '</span>'
 123+ },
 124+ 'link/internal': {
 125+ 'open': function( data ) {
 126+ return '<span class="es-contentView-format-link" data-title="wiki/' + data.title + '">';
 127+ },
 128+ 'close': '</span>'
 129+ }
 130+};
 131+
 132+/**
 133+ * Mapping of character and HTML entities or renderings.
 134+ *
 135+ * @static
 136+ * @member
 137+ */
 138+ve.es.Content.htmlCharacters = {
 139+ '&': '&amp;',
 140+ '<': '&lt;',
 141+ '>': '&gt;',
 142+ '\'': '&#039;',
 143+ '"': '&quot;',
 144+ '\n': '<span class="es-contentView-whitespace">&#182;</span>',
 145+ '\t': '<span class="es-contentView-whitespace">&#8702;</span>',
 146+ //' ': '&nbsp;'
 147+};
 148+
 149+/* Static Methods */
 150+
 151+/**
 152+ * Gets a rendered opening or closing of an annotation.
 153+ *
 154+ * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
 155+ * argument should be used while rendering content.
 156+ *
 157+ * @static
 158+ * @method
 159+ * @param {String} bias Which side of the annotation to render, either "open" or "close"
 160+ * @param {Object} annotation Annotation to render
 161+ * @param {Array} stack List of currently open annotations
 162+ * @returns {String} Rendered annotation
 163+ */
 164+ve.es.Content.renderAnnotation = function( bias, annotation, stack ) {
 165+ var renderers = ve.es.Content.annotationRenderers,
 166+ type = annotation.type,
 167+ out = '';
 168+ if ( type in renderers ) {
 169+ if ( bias === 'open' ) {
 170+ // Add annotation to the top of the stack
 171+ stack.push( annotation );
 172+ // Open annotation
 173+ out += typeof renderers[type].open === 'function' ?
 174+ renderers[type].open( annotation.data ) : renderers[type].open;
 175+ } else {
 176+ if ( stack[stack.length - 1] === annotation ) {
 177+ // Remove annotation from top of the stack
 178+ stack.pop();
 179+ // Close annotation
 180+ out += typeof renderers[type].close === 'function' ?
 181+ renderers[type].close( annotation.data ) : renderers[type].close;
 182+ } else {
 183+ // Find the annotation in the stack
 184+ var depth = ve.inArray( annotation, stack ),
 185+ i;
 186+ if ( depth === -1 ) {
 187+ throw 'Invalid stack error. An element is missing from the stack.';
 188+ }
 189+ // Close each already opened annotation
 190+ for ( i = stack.length - 1; i >= depth + 1; i-- ) {
 191+ out += typeof renderers[stack[i].type].close === 'function' ?
 192+ renderers[stack[i].type].close( stack[i].data ) :
 193+ renderers[stack[i].type].close;
 194+ }
 195+ // Close the buried annotation
 196+ out += typeof renderers[type].close === 'function' ?
 197+ renderers[type].close( annotation.data ) : renderers[type].close;
 198+ // Re-open each previously opened annotation
 199+ for ( i = depth + 1; i < stack.length; i++ ) {
 200+ out += typeof renderers[stack[i].type].open === 'function' ?
 201+ renderers[stack[i].type].open( stack[i].data ) :
 202+ renderers[stack[i].type].open;
 203+ }
 204+ // Remove the annotation from the middle of the stack
 205+ stack.splice( depth, 1 );
 206+ }
 207+ }
 208+ }
 209+ return out;
 210+};
 211+
 212+/* Methods */
 213+
 214+/**
 215+ * Draws selection around a given range of content.
 216+ *
 217+ * @method
 218+ * @param {ve.Range} range Range to draw selection around
 219+ */
 220+ve.es.Content.prototype.drawSelection = function( range ) {
 221+ if ( typeof range === 'undefined' ) {
 222+ range = new ve.Range( 0, this.model.getContentLength() );
 223+ } else {
 224+ range.normalize();
 225+ }
 226+ var fromLineIndex = this.getRenderedLineIndexFromOffset( range.start ),
 227+ toLineIndex = this.getRenderedLineIndexFromOffset( range.end ),
 228+ fromPosition = this.getRenderedPositionFromOffset( range.start ),
 229+ toPosition = this.getRenderedPositionFromOffset( range.end );
 230+
 231+ if ( fromLineIndex === toLineIndex ) {
 232+ // Single line selection
 233+ if ( toPosition.left - fromPosition.left ) {
 234+ this.$rangeStart.css( {
 235+ 'top': fromPosition.top,
 236+ 'left': fromPosition.left,
 237+ 'width': toPosition.left - fromPosition.left,
 238+ 'height': fromPosition.bottom - fromPosition.top
 239+ } ).show();
 240+ }
 241+ this.$rangeFill.hide();
 242+ this.$rangeEnd.hide();
 243+ } else {
 244+ // Multiple line selection
 245+ var contentWidth = this.$.width();
 246+ if ( contentWidth - fromPosition.left ) {
 247+ this.$rangeStart.css( {
 248+ 'top': fromPosition.top,
 249+ 'left': fromPosition.left,
 250+ 'width': contentWidth - fromPosition.left,
 251+ 'height': fromPosition.bottom - fromPosition.top
 252+ } ).show();
 253+ } else {
 254+ this.$rangeStart.hide();
 255+ }
 256+ if ( toPosition.left ) {
 257+ this.$rangeEnd.css( {
 258+ 'top': toPosition.top,
 259+ 'left': 0,
 260+ 'width': toPosition.left,
 261+ 'height': toPosition.bottom - toPosition.top
 262+ } ).show();
 263+ } else {
 264+ this.$rangeEnd.hide();
 265+ }
 266+ if ( fromLineIndex + 1 < toLineIndex ) {
 267+ this.$rangeFill.css( {
 268+ 'top': fromPosition.bottom,
 269+ 'left': 0,
 270+ 'width': contentWidth,
 271+ 'height': toPosition.top - fromPosition.bottom
 272+ } ).show();
 273+ } else {
 274+ this.$rangeFill.hide();
 275+ }
 276+ }
 277+};
 278+
 279+/**
 280+ * Clears selection if any was drawn.
 281+ *
 282+ * @method
 283+ */
 284+ve.es.Content.prototype.clearSelection = function() {
 285+ this.$rangeStart.hide();
 286+ this.$rangeFill.hide();
 287+ this.$rangeEnd.hide();
 288+};
 289+
 290+/**
 291+ * Gets the index of the rendered line a given offset is within.
 292+ *
 293+ * Offsets that are out of range will always return the index of the last line.
 294+ *
 295+ * @method
 296+ * @param {Integer} offset Offset to get line for
 297+ * @returns {Integer} Index of rendered lin offset is within
 298+ */
 299+ve.es.Content.prototype.getRenderedLineIndexFromOffset = function( offset ) {
 300+ for ( var i = 0; i < this.lines.length; i++ ) {
 301+ if ( this.lines[i].range.containsOffset( offset ) ) {
 302+ return i;
 303+ }
 304+ }
 305+ return this.lines.length - 1;
 306+};
 307+
 308+/*
 309+ * Gets the index of the rendered line closest to a given position.
 310+ *
 311+ * If the position is above the first line, the offset will always be 0, and if the position is
 312+ * below the last line the offset will always be the content length. All other vertical
 313+ * positions will fall inside of one of the lines.
 314+ *
 315+ * @method
 316+ * @returns {Integer} Index of rendered line closest to position
 317+ */
 318+ve.es.Content.prototype.getRenderedLineIndexFromPosition = function( position ) {
 319+ var lineCount = this.lines.length;
 320+ // Positions above the first line always jump to the first offset
 321+ if ( !lineCount || position.top < 0 ) {
 322+ return 0;
 323+ }
 324+ // Find which line the position is inside of
 325+ var i = 0,
 326+ top = 0;
 327+ while ( i < lineCount ) {
 328+ top += this.lines[i].height;
 329+ if ( position.top < top ) {
 330+ break;
 331+ }
 332+ i++;
 333+ }
 334+ // Positions below the last line always jump to the last offset
 335+ if ( i === lineCount ) {
 336+ return i - 1;
 337+ }
 338+ return i;
 339+};
 340+
 341+/**
 342+ * Gets the range of the rendered line a given offset is within.
 343+ *
 344+ * Offsets that are out of range will always return the range of the last line.
 345+ *
 346+ * @method
 347+ * @param {Integer} offset Offset to get line for
 348+ * @returns {ve.Range} Range of line offset is within
 349+ */
 350+ve.es.Content.prototype.getRenderedLineRangeFromOffset = function( offset ) {
 351+ for ( var i = 0; i < this.lines.length; i++ ) {
 352+ if ( this.lines[i].range.containsOffset( offset ) ) {
 353+ return this.lines[i].range;
 354+ }
 355+ }
 356+ return this.lines[this.lines.length - 1].range;
 357+};
 358+
 359+/**
 360+ * Gets offset within content model closest to of a given position.
 361+ *
 362+ * Position is assumed to be local to the container the text is being flowed in.
 363+ *
 364+ * @method
 365+ * @param {Object} position Position to find offset for
 366+ * @param {Integer} position.left Horizontal position in pixels
 367+ * @param {Integer} position.top Vertical position in pixels
 368+ * @returns {Integer} Offset within content model nearest the given coordinates
 369+ */
 370+ve.es.Content.prototype.getOffsetFromRenderedPosition = function( position ) {
 371+ // Empty content model shortcut
 372+ if ( this.model.getContentLength() === 0 ) {
 373+ return 0;
 374+ }
 375+
 376+ // Localize position
 377+ position.subtract( ve.Position.newFromElementPagePosition( this.$ ) );
 378+
 379+ // Get the line object nearest the position
 380+ var line = this.lines[this.getRenderedLineIndexFromPosition( position )];
 381+
 382+ /*
 383+ * Offset finding
 384+ *
 385+ * Now that we know which line we are on, we can just use the "fitCharacters" method to get the
 386+ * last offset before "position.left".
 387+ *
 388+ * TODO: The offset needs to be chosen based on nearest offset to the cursor, not offset before
 389+ * the cursor.
 390+ */
 391+ var $ruler = $( '<div class="es-contentView-ruler"></div>' ).appendTo( this.$ ),
 392+ ruler = $ruler[0],
 393+ fit = this.fitCharacters( line.range, ruler, position.left ),
 394+ center;
 395+ ruler.innerHTML = this.getHtml( new ve.Range( line.range.start, fit.end ) );
 396+ if ( fit.end < this.model.getContentLength() ) {
 397+ var left = ruler.clientWidth;
 398+ ruler.innerHTML = this.getHtml( new ve.Range( line.range.start, fit.end + 1 ) );
 399+ center = Math.round( left + ( ( ruler.clientWidth - left ) / 2 ) );
 400+ } else {
 401+ center = ruler.clientWidth;
 402+ }
 403+ $ruler.remove();
 404+ // Reset RegExp object's state
 405+ this.boundaryTest.lastIndex = 0;
 406+ return Math.min(
 407+ // If the position is right of the center of the character it's on top of, increment offset
 408+ fit.end + ( position.left >= center ? 1 : 0 ),
 409+ // Don't allow the value to be higher than the end
 410+ line.range.end
 411+ );
 412+};
 413+
 414+/**
 415+ * Gets position coordinates of a given offset.
 416+ *
 417+ * Offsets are boundaries between plain or annotated characters within content model. Results are
 418+ * given in left, top and bottom positions, which could be used to draw a cursor, highlighting, etc.
 419+ *
 420+ * @method
 421+ * @param {Integer} offset Offset within content model
 422+ * @returns {Object} Object containing left, top and bottom properties, each positions in pixels as
 423+ * well as a line index
 424+ */
 425+ve.es.Content.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
 426+ /*
 427+ * Range validation
 428+ *
 429+ * Rather than clamping the range, which can hide errors, exceptions will be thrown if offset is
 430+ * less than 0 or greater than the length of the content model.
 431+ */
 432+ if ( offset < 0 ) {
 433+ throw 'Out of range error. Offset is expected to be greater than or equal to 0.';
 434+ } else if ( offset > this.model.getContentLength() ) {
 435+ throw 'Out of range error. Offset is expected to be less than or equal to text length.';
 436+ }
 437+ /*
 438+ * Line finding
 439+ *
 440+ * It's possible that a more efficient method could be used here, but the number of lines to be
 441+ * iterated through will rarely be over 100, so it's unlikely that any significant gains will be
 442+ * had. Plus, as long as we are iterating over each line, we can also sum up the top and bottom
 443+ * positions, which is a nice benefit of this method.
 444+ */
 445+ var line,
 446+ lineCount = this.lines.length,
 447+ lineIndex = 0,
 448+ position = new ve.Position();
 449+ while ( lineIndex < lineCount ) {
 450+ line = this.lines[lineIndex];
 451+ if ( line.range.containsOffset( offset ) || ( leftBias && line.range.end === offset ) ) {
 452+ position.bottom = position.top + line.height;
 453+ break;
 454+ }
 455+ position.top += line.height;
 456+ lineIndex++;
 457+ }
 458+ /*
 459+ * Virtual n+1 position
 460+ *
 461+ * To allow access to position information of the right side of the last character on the last
 462+ * line, a virtual n+1 position is supported. Offsets beyond this virtual position will cause
 463+ * an exception to be thrown.
 464+ */
 465+ if ( lineIndex === lineCount ) {
 466+ position.bottom = position.top;
 467+ position.top -= line.height;
 468+ }
 469+ /*
 470+ * Offset measuring
 471+ *
 472+ * Since the left position will be zero for the first character in the line, so we can skip
 473+ * measuring for those cases.
 474+ */
 475+ if ( line.range.start < offset ) {
 476+ var $ruler = $( '<div class="es-contentView-ruler"></div>' ).appendTo( this.$ ),
 477+ ruler = $ruler[0];
 478+ ruler.innerHTML = this.getHtml( new ve.Range( line.range.start, offset ) );
 479+ position.left = ruler.clientWidth;
 480+ $ruler.remove();
 481+ }
 482+ return position;
 483+};
 484+
 485+/**
 486+ * Updates the word boundary cache, which is used for word fitting.
 487+ *
 488+ * @method
 489+ */
 490+ve.es.Content.prototype.scanBoundaries = function() {
 491+ /*
 492+ * Word boundary scan
 493+ *
 494+ * To perform binary-search on words, rather than characters, we need to collect word boundary
 495+ * offsets into an array. The offset of the right side of the breaking character is stored, so
 496+ * the gaps between stored offsets always include the breaking character at the end.
 497+ *
 498+ * To avoid encoding the same words as HTML over and over while fitting text to lines, we also
 499+ * build a list of HTML escaped strings for each gap between the offsets stored in the
 500+ * "boundaries" array. Slices of the "words" array can be joined, producing the escaped HTML of
 501+ * the words.
 502+ */
 503+ // Get and cache a copy of all content, the make a plain-text version of the cached content
 504+ var data = this.contentCache = this.model.getContentData(),
 505+ text = '';
 506+ for ( var i = 0, length = data.length; i < length; i++ ) {
 507+ text += typeof data[i] === 'string' ? data[i] : data[i][0];
 508+ }
 509+ // Purge "boundaries" and "words" arrays
 510+ this.boundaries = [0];
 511+ // Reset RegExp object's state
 512+ this.boundaryTest.lastIndex = 0;
 513+ // Iterate over each word+boundary sequence, capturing offsets and encoding text as we go
 514+ var match,
 515+ end;
 516+ while ( ( match = this.boundaryTest.exec( text ) ) ) {
 517+ // Include the boundary character in the range
 518+ end = match.index + 1;
 519+ // Store the boundary offset
 520+ this.boundaries.push( end );
 521+ }
 522+ // If the last character is not a boundary character, we need to append the final range to the
 523+ // "boundaries" and "words" arrays
 524+ if ( end < text.length || this.boundaries.length === 1 ) {
 525+ this.boundaries.push( text.length );
 526+ }
 527+};
 528+
 529+/**
 530+ * Renders a batch of lines and then yields execution before rendering another batch.
 531+ *
 532+ * In cases where a single word is too long to fit on a line, the word will be "virtually" wrapped,
 533+ * causing them to be fragmented. Word fragments are rendered on their own lines, except for their
 534+ * remainder, which is combined with whatever proceeding words can fit on the same line.
 535+ *
 536+ * @method
 537+ * @param {Integer} limit Maximum number of iterations to render before yeilding
 538+ */
 539+ve.es.Content.prototype.renderIteration = function( limit ) {
 540+ var rs = this.renderState,
 541+ iteration = 0,
 542+ fractional = false,
 543+ lineStart = this.boundaries[rs.wordOffset],
 544+ lineEnd,
 545+ wordFit = null,
 546+ charOffset = 0,
 547+ charFit = null,
 548+ wordCount = this.boundaries.length;
 549+ while ( ++iteration <= limit && rs.wordOffset < wordCount - 1 ) {
 550+ wordFit = this.fitWords( new ve.Range( rs.wordOffset, wordCount - 1 ), rs.ruler, rs.width );
 551+ fractional = false;
 552+ if ( wordFit.width > rs.width ) {
 553+ // The first word didn't fit, we need to split it up
 554+ charOffset = lineStart;
 555+ var lineOffset = rs.wordOffset;
 556+ rs.wordOffset++;
 557+ lineEnd = this.boundaries[rs.wordOffset];
 558+ do {
 559+ charFit = this.fitCharacters(
 560+ new ve.Range( charOffset, lineEnd ), rs.ruler, rs.width
 561+ );
 562+ // If we were able to get the rest of the characters on the line OK
 563+ if ( charFit.end === lineEnd) {
 564+ // Try to fit more words on the line
 565+ wordFit = this.fitWords(
 566+ new ve.Range( rs.wordOffset, wordCount - 1 ),
 567+ rs.ruler,
 568+ rs.width - charFit.width
 569+ );
 570+ if ( wordFit.end > rs.wordOffset ) {
 571+ lineOffset = rs.wordOffset;
 572+ rs.wordOffset = wordFit.end;
 573+ charFit.end = lineEnd = this.boundaries[rs.wordOffset];
 574+ }
 575+ }
 576+ this.appendLine( new ve.Range( charOffset, charFit.end ), lineOffset, fractional );
 577+ // Move on to another line
 578+ charOffset = charFit.end;
 579+ // Mark the next line as fractional
 580+ fractional = true;
 581+ } while ( charOffset < lineEnd );
 582+ } else {
 583+ lineEnd = this.boundaries[wordFit.end];
 584+ this.appendLine( new ve.Range( lineStart, lineEnd ), rs.wordOffset, fractional );
 585+ rs.wordOffset = wordFit.end;
 586+ }
 587+ lineStart = lineEnd;
 588+ }
 589+ // Only perform on actual last iteration
 590+ if ( rs.wordOffset >= wordCount - 1 ) {
 591+ // Cleanup
 592+ rs.$ruler.remove();
 593+ if ( rs.line < this.lines.length ) {
 594+ this.lines.splice( rs.line, this.lines.length - rs.line );
 595+ }
 596+ this.$.find( '.es-contentView-line[line-index=' + ( this.lines.length - 1 ) + ']' )
 597+ .nextAll()
 598+ .remove();
 599+ rs.timeout = undefined;
 600+ this.emit( 'update' );
 601+ } else {
 602+ rs.ruler.innerHTML = '';
 603+ var that = this;
 604+ rs.timeout = setTimeout( function() {
 605+ that.renderIteration( 3 );
 606+ }, 0 );
 607+ }
 608+};
 609+
 610+/**
 611+ * Renders text into a series of HTML elements, each a single line of wrapped text.
 612+ *
 613+ * The offset parameter can be used to reduce the amount of work involved in re-rendering the same
 614+ * text, but will be automatically ignored if the text or width of the container has changed.
 615+ *
 616+ * Rendering happens asynchronously, and yields execution between iterations. Iterative rendering
 617+ * provides the JavaScript engine an ability to process events between rendering batches of lines,
 618+ * allowing rendering to be interrupted and restarted if changes to content model are happening before
 619+ * rendering of all lines is complete.
 620+ *
 621+ * @method
 622+ * @param {Integer} [offset] Offset to re-render from, if possible
 623+ */
 624+ve.es.Content.prototype.render = function( offset ) {
 625+ this.$.html(this.getHtml(0, this.model.getContentLength()));
 626+ return;
 627+
 628+ var rs = this.renderState;
 629+ // Check if rendering is currently underway
 630+ if ( rs.timeout !== undefined ) {
 631+ // Cancel the active rendering process
 632+ clearTimeout( rs.timeout );
 633+ // Cleanup
 634+ rs.$ruler.remove();
 635+ }
 636+ // Clear caches that were specific to the previous render
 637+ this.widthCache = {};
 638+ // In case of empty content model we still want to display empty with non-breaking space inside
 639+ // This is very important for lists
 640+ if(this.model.getContentLength() === 0) {
 641+ var $line = $( '<div class="es-contentView-line" line-index="0">&nbsp;</div>' );
 642+ this.$
 643+ .children()
 644+ .remove( '.es-contentView-line' )
 645+ .end()
 646+ .append( $line );
 647+ this.lines = [{
 648+ 'text': ' ',
 649+ 'range': new ve.Range( 0,0 ),
 650+ 'width': 0,
 651+ 'height': $line.outerHeight(),
 652+ 'wordOffset': 0,
 653+ 'fractional': false
 654+ }];
 655+ this.emit( 'update' );
 656+ return;
 657+ }
 658+ /*
 659+ * Container measurement
 660+ *
 661+ * To get an accurate measurement of the inside of the container, without having to deal with
 662+ * inconsistencies between browsers and box models, we can just create an element inside the
 663+ * container and measure it.
 664+ */
 665+ rs.$ruler = $( '<div>&nbsp;</div>' ).appendTo( this.$ );
 666+ rs.width = rs.$ruler.innerWidth();
 667+ rs.ruler = rs.$ruler.addClass('es-contentView-ruler')[0];
 668+ // Ignore offset optimization if the width has changed or the text has never been flowed before
 669+ if (this.width !== rs.width) {
 670+ offset = undefined;
 671+ }
 672+ this.width = rs.width;
 673+ // Reset the render state
 674+ if ( offset ) {
 675+ var gap,
 676+ currentLine = this.lines.length - 1;
 677+ for ( var i = this.lines.length - 1; i >= 0; i-- ) {
 678+ var line = this.lines[i];
 679+ if ( line.range.start < offset && line.range.end > offset ) {
 680+ currentLine = i;
 681+ }
 682+ if ( ( line.range.end < offset && !line.fractional ) || i === 0 ) {
 683+ rs.line = i;
 684+ rs.wordOffset = line.wordOffset;
 685+ gap = currentLine - i;
 686+ break;
 687+ }
 688+ }
 689+ this.renderIteration( 2 + gap );
 690+ } else {
 691+ rs.line = 0;
 692+ rs.wordOffset = 0;
 693+ this.renderIteration( 3 );
 694+ }
 695+};
 696+
 697+/**
 698+ * Adds a line containing a given range of text to the end of the DOM and the "lines" array.
 699+ *
 700+ * @method
 701+ * @param {ve.Range} range Range of data within content model to append
 702+ * @param {Integer} start Beginning of text range for line
 703+ * @param {Integer} end Ending of text range for line
 704+ * @param {Integer} wordOffset Index within this.words which the line begins with
 705+ * @param {Boolean} fractional If the line begins in the middle of a word
 706+ */
 707+ve.es.Content.prototype.appendLine = function( range, wordOffset, fractional ) {
 708+ var rs = this.renderState,
 709+ $line = this.$.children( '[line-index=' + rs.line + ']' );
 710+ if ( !$line.length ) {
 711+ $line = $(
 712+ '<div class="es-contentView-line" line-index="' + rs.line + '"></div>'
 713+ );
 714+ this.$.append( $line );
 715+ }
 716+ $line[0].innerHTML = this.getHtml( range );
 717+ // Overwrite/append line information
 718+ this.lines[rs.line] = {
 719+ 'text': this.model.getContentText( range ),
 720+ 'range': range,
 721+ 'width': $line.outerWidth(),
 722+ 'height': $line.outerHeight(),
 723+ 'wordOffset': wordOffset,
 724+ 'fractional': fractional
 725+ };
 726+ // Disable links within rendered content
 727+ $line.find( '.es-contentView-format-object a' )
 728+ .mousedown( function( e ) {
 729+ e.preventDefault();
 730+ } )
 731+ .click( function( e ) {
 732+ e.preventDefault();
 733+ } );
 734+ rs.line++;
 735+};
 736+
 737+/**
 738+ * Gets the index of the boundary of last word that fits inside the line
 739+ *
 740+ * The "words" and "boundaries" arrays provide linear access to the offsets around non-breakable
 741+ * areas within the text. Using these, we can perform a binary-search for the best fit of words
 742+ * within a line, just as we would with characters.
 743+ *
 744+ * Results are given as an object containing both an index and a width, the later of which can be
 745+ * used to detect when the first word was too long to fit on a line. In such cases the result will
 746+ * contain the index of the boundary of the first word and it's width.
 747+ *
 748+ * TODO: Because limit is most likely given as "words.length", it may be possible to improve the
 749+ * efficiency of this code by making a best guess and working from there, rather than always
 750+ * starting with [offset .. limit], which usually results in reducing the end position in all but
 751+ * the last line, and in most cases more than 3 times, before changing directions.
 752+ *
 753+ * @method
 754+ * @param {ve.Range} range Range of data within content model to try to fit
 755+ * @param {HTMLElement} ruler Element to take measurements with
 756+ * @param {Integer} width Maximum width to allow the line to extend to
 757+ * @returns {Integer} Last index within "words" that contains a word that fits
 758+ */
 759+ve.es.Content.prototype.fitWords = function( range, ruler, width ) {
 760+ var offset = range.start,
 761+ start = range.start,
 762+ end = range.end,
 763+ charOffset = this.boundaries[offset],
 764+ middle,
 765+ charMiddle,
 766+ lineWidth,
 767+ cacheKey;
 768+ do {
 769+ // Place "middle" directly in the center of "start" and "end"
 770+ middle = Math.ceil( ( start + end ) / 2 );
 771+ charMiddle = this.boundaries[middle];
 772+ // Measure and cache width of substring
 773+ cacheKey = charOffset + ':' + charMiddle;
 774+ // Prepare the line for measurement using pre-escaped HTML
 775+ ruler.innerHTML = this.getHtml( new ve.Range( charOffset, charMiddle ) );
 776+ // Test for over/under using width of the rendered line
 777+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 778+ // Test for over/under using width of the rendered line
 779+ if ( lineWidth > width ) {
 780+ // Detect impossible fit (the first word won't fit by itself)
 781+ if (middle - offset === 1) {
 782+ start = middle;
 783+ break;
 784+ }
 785+ // Words after "middle" won't fit
 786+ end = middle - 1;
 787+ } else {
 788+ // Words before "middle" will fit
 789+ start = middle;
 790+ }
 791+ } while ( start < end );
 792+ // Check if we ended by moving end to the left of middle
 793+ if ( end === middle - 1 ) {
 794+ // A final measurement is required
 795+ var charStart = this.boundaries[start];
 796+ ruler.innerHTML = this.getHtml( new ve.Range( charOffset, charStart ) );
 797+ lineWidth = this.widthCache[charOffset + ':' + charStart] = ruler.clientWidth;
 798+ }
 799+ return { 'end': start, 'width': lineWidth };
 800+};
 801+
 802+/**
 803+ * Gets the index of the boundary of the last character that fits inside the line
 804+ *
 805+ * Results are given as an object containing both an index and a width, the later of which can be
 806+ * used to detect when the first character was too long to fit on a line. In such cases the result
 807+ * will contain the index of the first character and it's width.
 808+ *
 809+ * @method
 810+ * @param {ve.Range} range Range of data within content model to try to fit
 811+ * @param {HTMLElement} ruler Element to take measurements with
 812+ * @param {Integer} width Maximum width to allow the line to extend to
 813+ * @returns {Integer} Last index within "text" that contains a character that fits
 814+ */
 815+ve.es.Content.prototype.fitCharacters = function( range, ruler, width ) {
 816+ var offset = range.start,
 817+ start = range.start,
 818+ end = range.end,
 819+ middle,
 820+ lineWidth,
 821+ cacheKey;
 822+ do {
 823+ // Place "middle" directly in the center of "start" and "end"
 824+ middle = Math.ceil( ( start + end ) / 2 );
 825+ // Measure and cache width of substring
 826+ cacheKey = offset + ':' + middle;
 827+ if ( cacheKey in this.widthCache ) {
 828+ lineWidth = this.widthCache[cacheKey];
 829+ } else {
 830+ // Fill the line with a portion of the text, escaped as HTML
 831+ ruler.innerHTML = this.getHtml( new ve.Range( offset, middle ) );
 832+ // Test for over/under using width of the rendered line
 833+ this.widthCache[cacheKey] = lineWidth = ruler.clientWidth;
 834+ }
 835+ if ( lineWidth > width ) {
 836+ // Detect impossible fit (the first character won't fit by itself)
 837+ if (middle - offset === 1) {
 838+ start = middle - 1;
 839+ break;
 840+ }
 841+ // Words after "middle" won't fit
 842+ end = middle - 1;
 843+ } else {
 844+ // Words before "middle" will fit
 845+ start = middle;
 846+ }
 847+ } while ( start < end );
 848+ // Check if we ended by moving end to the left of middle
 849+ if ( end === middle - 1 ) {
 850+ // Try for cache hit
 851+ cacheKey = offset + ':' + start;
 852+ if ( cacheKey in this.widthCache ) {
 853+ lineWidth = this.widthCache[cacheKey];
 854+ } else {
 855+ // A final measurement is required
 856+ ruler.innerHTML = this.getHtml( new ve.Range( offset, start ) );
 857+ lineWidth = this.widthCache[cacheKey] = ruler.clientWidth;
 858+ }
 859+ }
 860+ return { 'end': start, 'width': lineWidth };
 861+};
 862+
 863+/**
 864+ * Gets an HTML rendering of a range of data within content model.
 865+ *
 866+ * @method
 867+ * @param {ve.Range} range Range of content to render
 868+ * @param {String} Rendered HTML of data within content model
 869+ */
 870+ve.es.Content.prototype.getHtml = function( range, options ) {
 871+ if ( range ) {
 872+ range.normalize();
 873+ } else {
 874+ range = { 'start': 0, 'end': undefined };
 875+ }
 876+ var data = this.contentCache.slice( range.start, range.end ),
 877+ render = ve.es.Content.renderAnnotation,
 878+ htmlChars = ve.es.Content.htmlCharacters;
 879+ var out = '',
 880+ left = '',
 881+ right,
 882+ leftPlain,
 883+ rightPlain,
 884+ stack = [],
 885+ chr,
 886+ i,
 887+ j;
 888+ for ( i = 0; i < data.length; i++ ) {
 889+ right = data[i];
 890+ leftPlain = typeof left === 'string';
 891+ rightPlain = typeof right === 'string';
 892+ if ( !leftPlain && rightPlain ) {
 893+ // [formatted][plain] pair, close any annotations for left
 894+ for ( j = 1; j < left.length; j++ ) {
 895+ out += render( 'close', left[j], stack );
 896+ }
 897+ } else if ( leftPlain && !rightPlain ) {
 898+ // [plain][formatted] pair, open any annotations for right
 899+ for ( j = 1; j < right.length; j++ ) {
 900+ out += render( 'open', right[j], stack );
 901+ }
 902+ } else if ( !leftPlain && !rightPlain ) {
 903+ // [formatted][formatted] pair, open/close any differences
 904+ for ( j = 1; j < left.length; j++ ) {
 905+ if ( ve.inArray( left[j], right ) === -1 ) {
 906+ out += render( 'close', left[j], stack );
 907+ }
 908+ }
 909+ for ( j = 1; j < right.length; j++ ) {
 910+ if ( ve.inArray( right[j], left ) === -1 ) {
 911+ out += render( 'open', right[j], stack );
 912+ }
 913+ }
 914+ }
 915+ chr = rightPlain ? right : right[0];
 916+ out += chr in htmlChars ? htmlChars[chr] : chr;
 917+ left = right;
 918+ }
 919+ // Close all remaining tags at the end of the content
 920+ if ( !rightPlain && right ) {
 921+ for ( j = 1; j < right.length; j++ ) {
 922+ out += render( 'close', right[j], stack );
 923+ }
 924+ }
 925+ return out;
 926+};
 927+
 928+/* Inheritance */
 929+
 930+ve.extendClass( ve.es.Content, ve.EventEmitter );
Index: trunk/extensions/VisualEditor/modules/ve/ce/nodes/ve.es.DocumentNode.js
@@ -0,0 +1,72 @@
 2+/**
 3+ * Creates an ve.es.DocumentNode object.
 4+ *
 5+ * @class
 6+ * @constructor
 7+ * @extends {ve.es.BranchNode}
 8+ * @param {ve.dm.DocumentNode} documentModel Document model to view
 9+ * @param {ve.es.Surface} surfaceView Surface view this view is a child of
 10+ */
 11+ve.es.DocumentNode = function( model, surfaceView ) {
 12+ // Inheritance
 13+ ve.es.BranchNode.call( this, model );
 14+
 15+ // Properties
 16+ this.surfaceView = surfaceView;
 17+
 18+ // DOM Changes
 19+ this.$.addClass( 'es-documentView' );
 20+ this.$.attr('contentEditable', 'true');
 21+};
 22+
 23+/* Static Members */
 24+
 25+
 26+/**
 27+ * Mapping of symbolic names and splitting rules.
 28+ *
 29+ * Each rule is an object with a self and children property. Each of these properties may contain
 30+ * one of two possible values:
 31+ * Boolean - Whether a split is allowed
 32+ * Null - Node is a leaf, so there's nothing to split
 33+ *
 34+ * @example Paragraph rules
 35+ * {
 36+ * 'self': true
 37+ * 'children': null
 38+ * }
 39+ * @example List rules
 40+ * {
 41+ * 'self': false,
 42+ * 'children': true
 43+ * }
 44+ * @example ListItem rules
 45+ * {
 46+ * 'self': true,
 47+ * 'children': false
 48+ * }
 49+ */
 50+ve.es.DocumentNode.splitRules = {};
 51+
 52+/* Methods */
 53+
 54+/**
 55+ * Get the document offset of a position created from passed DOM event
 56+ *
 57+ * @method
 58+ * @param e {Event} Event to create ve.Position from
 59+ * @returns {Integer} Document offset
 60+ */
 61+ve.es.DocumentNode.prototype.getOffsetFromEvent = function( e ) {
 62+ var position = ve.Position.newFromEventPagePosition( e );
 63+ return this.getOffsetFromRenderedPosition( position );
 64+};
 65+
 66+ve.es.DocumentNode.splitRules.document = {
 67+ 'self': false,
 68+ 'children': true
 69+};
 70+
 71+/* Inheritance */
 72+
 73+ve.extendClass( ve.es.DocumentNode, ve.es.BranchNode );
Index: trunk/extensions/VisualEditor/demo-ce/main.js
@@ -0,0 +1,749 @@
 2+$(document).ready( function() {
 3+ var wikidoms = {
 4+ 'Wikipedia article': {
 5+ 'type': 'document',
 6+ 'children': [
 7+ {
 8+ 'type': 'heading',
 9+ 'attributes': { 'level': 1 },
 10+ 'content': { 'text': 'Direct manipulation interface' }
 11+ },
 12+ {
 13+ 'type': 'paragraph',
 14+ 'content': {
 15+ 'text': 'In computer science, direct manipulation is a human-computer interaction style which involves continuous representation of objects of interest, and rapid, reversible, incremental actions and feedback. The intention is to allow a user to directly manipulate objects presented to them, using actions that correspond at least loosely to the physical world. An example of direct-manipulation is resizing a graphical shape, such as a rectangle, by dragging its corners or edges with a mouse.',
 16+ 'annotations': [
 17+ {
 18+ 'type': 'link/internal',
 19+ 'data': {
 20+ 'title': 'Computer_science'
 21+ },
 22+ 'range': {
 23+ 'start': 3,
 24+ 'end': 19
 25+ }
 26+ },
 27+ {
 28+ 'type': 'link/internal',
 29+ 'data': {
 30+ 'title': 'Human-computer interaction'
 31+ },
 32+ 'range': {
 33+ 'start': 46,
 34+ 'end': 72
 35+ }
 36+ }
 37+ ]
 38+ }
 39+ },
 40+ {
 41+ 'type': 'paragraph',
 42+ 'content': { 'text': 'Having real-world metaphors for objects and actions can make it easier for a user to learn and use an interface (some might say that the interface is more natural or intuitive), and rapid, incremental feedback allows a user to make fewer errors and complete tasks in less time, because they can see the results of an action before completing the action, thus evaluating the output and compensating for mistakes.' }
 43+ },
 44+ {
 45+ 'type': 'paragraph',
 46+ 'content': {
 47+ 'text': 'The term was introduced by Ben Shneiderman in 1983 within the context of office applications and the desktop metaphor. Individuals in academia and computer scientists doing research on future user interfaces often put as much or even more stress on tactile control and feedback, or sonic control and feedback than on the visual feedback given by most GUIs. As a result the term direct manipulation interface has been more widespread in these environments. ',
 48+ 'annotations': [
 49+ {
 50+ 'type': 'link/internal',
 51+ 'data': {
 52+ 'title': 'Ben_Shneiderman'
 53+ },
 54+ 'range': {
 55+ 'start': 27,
 56+ 'end': 42
 57+ }
 58+ },
 59+ {
 60+ 'type': 'link/internal',
 61+ 'data': {
 62+ 'title': 'GUI'
 63+ },
 64+ 'range': {
 65+ 'start': 352,
 66+ 'end': 356
 67+ }
 68+ },
 69+ {
 70+ 'type': 'object/hook',
 71+ 'data': {
 72+ 'html': '<sup><small><a href="#">[1]</a></small></sup>'
 73+ },
 74+ 'range': {
 75+ 'start': 118,
 76+ 'end': 119
 77+ }
 78+ },
 79+ {
 80+ 'type': 'object/template',
 81+ 'data': {
 82+ 'html': '<sup><small>[<a href="#">citation needed</a>]</small></sup>'
 83+ },
 84+ 'range': {
 85+ 'start': 456,
 86+ 'end': 457
 87+ }
 88+ }
 89+ ]
 90+ }
 91+ },
 92+ {
 93+ 'type': 'heading',
 94+ 'attributes': { 'level': 2 },
 95+ 'content': { 'text': 'In contrast to WIMP/GUI interfaces' }
 96+ },
 97+ {
 98+ 'type': 'paragraph',
 99+ 'content': {
 100+ 'text': 'Direct manipulation is closely associated with interfaces that use windows, icons, menus, and a pointing device (WIMP GUI) as these almost always incorporate direct manipulation to at least some degree. However, direct manipulation should not be confused with these other terms, as it does not imply the use of windows or even graphical output. For example, direct manipulation concepts can be applied to interfaces for blind or vision-impaired users, using a combination of tactile and sonic devices and software.',
 101+ 'annotations': [
 102+ {
 103+ 'type': 'link/internal',
 104+ 'data': {
 105+ 'title': 'WIMP_(computing)'
 106+ },
 107+ 'range': {
 108+ 'start': 113,
 109+ 'end': 117
 110+ }
 111+ }
 112+ ]
 113+ }
 114+ },
 115+ {
 116+ 'type': 'paragraph',
 117+ 'content': {
 118+ 'text': 'It is also possible to design a WIMP interface that intentionally does not make use of direct manipulation. For example, most versions of windowing interfaces (e.g. Microsoft Windows) allowed users to reposition a window by dragging it with the mouse, but would not continually redraw the complete window at intermediate positions during the drag. Instead, for example, a rectangular outline of the window might be drawn during the drag, with the complete window contents being redrawn only once the user had released the mouse button. This was necessary on older computers that lacked the memory and/or CPU power to quickly redraw data behind a window that was being dragged.',
 119+ 'annotations': [
 120+ {
 121+ 'type': 'link/internal',
 122+ 'data': {
 123+ 'title': 'Microsoft_Windows'
 124+ },
 125+ 'range': {
 126+ 'start': 165,
 127+ 'end': 182
 128+ }
 129+ }
 130+ ]
 131+ }
 132+ },
 133+ {
 134+ 'type': 'heading',
 135+ 'attributes': { 'level': 2 },
 136+ 'content': { 'text': 'In point of sale graphic interfaces' }
 137+ },
 138+ {
 139+ 'type': 'paragraph',
 140+ 'content': {
 141+ 'text': 'The ViewTouch graphic touchscreen POS (point of sale) GUI developed by Gene Mosher on the Atari ST computer and first installed in restaurants in 1986 is an early example of an application specific GUI that manifests all of the characteristics of direct manipulation.'
 142+ }
 143+ },
 144+ {
 145+ 'type': 'paragraph',
 146+ 'content': {
 147+ 'text': 'Mosher\'s POS touchscreen GUI has been widely copied and is in universal use on virtually all modern point of sale displays. Even in its earliest form it contained such features as \'lighting up\' both selected \'buttons\' (i.e., widgets) and \'tab\' buttons which indicated the user\'s current position in the transaction as the user navigated among the application\'s pages.'
 148+ }
 149+ },
 150+ {
 151+ 'type': 'paragraph',
 152+ 'content': {
 153+ 'text': 'In 1995 the ViewTouch GUI was developed into an X Window System window manager, extending the usefulness of the direct manipulation interface to users equipped with no other equipment than networked displays relying on the X network display protocol. This application is a practical and useful example of the benefit of the direct manipulation interface. Users are freed from the requirement of making use of keyboards, mice and even local computers themselves while they are simultaneously empowered to work in collaborative fashion with each other in world wide virtual workgroups by merely interacting with the framework of graphical symbols on the networked touchscreen.'
 154+ }
 155+ },
 156+ {
 157+ 'type': 'heading',
 158+ 'attributes': { 'level': 2 },
 159+ 'content': { 'text': 'In computer graphics' }
 160+ },
 161+ {
 162+ 'type': 'paragraph',
 163+ 'content': {
 164+ 'text': 'Because of the difficulty of visualizing and manipulating various aspects of computer graphics, including geometry creation and editing, animation, layout of objects and cameras, light placement, and other effects, direct manipulation is an extremely important part of 3D computer graphics. There are standard direct manipulation widgets as well as many unique widgets that are developed either as a better solution to an old problem or as a solution for a new and/or unique problem. The widgets attempt to allow the user to modify an object in any possible direction while also providing easy guides or constraints to allow the user to easily modify an object in the most common directions, while also attempting to be as intuitive as to the function of the widget as possible. The three most ubiquitous transformation widgets are mostly standardized and are:'
 165+ }
 166+ },
 167+ {
 168+ 'type': 'list',
 169+ 'children': [
 170+ {
 171+ 'type': 'listItem',
 172+ 'attributes': {
 173+ 'styles': ['bullet']
 174+ },
 175+ 'children' : [
 176+ {
 177+ 'type': 'paragraph',
 178+ 'content': { 'text': 'the translation widget, which usually consists of three arrows aligned with the orthogonal axes centered on the object to be translated. Dragging the center of the widget translates the object directly underneath the mouse pointer in the plane parallel to the camera plane, while dragging any of the three arrows translates the object along the appropriate axis. The axes may be aligned with the world-space axes, the object-space axes, or some other space.' }
 179+ }
 180+ ]
 181+ },
 182+ {
 183+ 'type': 'listItem',
 184+ 'attributes': {
 185+ 'styles': ['bullet']
 186+ },
 187+ 'children' : [
 188+ {
 189+ 'type': 'paragraph',
 190+ 'content': { 'text': 'the rotation widget, which usually consists of three circles aligned with the three orthogonal axes, and one circle aligned with the camera plane. Dragging any of the circles rotates the object around the appropriate axis, while dragging elsewhere will freely rotate the object (virtual trackball rotation).' }
 191+ }
 192+ ]
 193+ },
 194+ {
 195+ 'type': 'listItem',
 196+ 'attributes': {
 197+ 'styles': ['bullet']
 198+ },
 199+ 'children' : [
 200+ {
 201+ 'type': 'paragraph',
 202+ 'content': { 'text': 'the scale widget, which usually consists of three short lines aligned with the orthogonal axes terminating in boxes, and one box in the center of the widget. Dragging any of the three axis-aligned boxes effects a non-uniform scale along solely that axis, while dragging the center box effects a uniform scale on all three axes at once.' }
 203+ }
 204+ ]
 205+ }
 206+
 207+ ]
 208+ },
 209+ {
 210+ 'type': 'paragraph',
 211+ 'content': {
 212+ 'text': 'Depending on the specific common uses of an object, different kinds of widgets may be used. For example, a light in computer graphics is, like any other object, also defined by a transformation (translation and rotation), but it is sometimes positioned and directed simply with its endpoint positions because it may be more intuitive to define the position of the light source and then define the light\'s target, rather than rotating it around the coordinate axes in order to point it at a known position.'
 213+ }
 214+ },
 215+ {
 216+ 'type': 'paragraph',
 217+ 'content': {
 218+ 'text': 'Other widgets may be unique for a particular tool, such as edge controls to change the cone of a spotlight, points and handles to define the position and tangent vector for a spline control point, circles of variable size to define a blur filter width or paintbrush size, IK targets for hands and feet, or color wheels and swatches for quickly choosing colors. Complex widgets may even incorporate some techniques from scientific visualization to efficiently present relevant data (such as vector fields for particle effects or false color images to display vertex maps).'
 219+ }
 220+ },
 221+ {
 222+ 'type': 'paragraph',
 223+ 'content': {
 224+ 'text': 'Direct manipulation, as well as user interface design in general, for 3D computer graphics tasks, is still an active area of invention and innovation, as the process of generating CG images is generally not considered to be intuitive or easy in comparison to the difficulty of what the user wants to do, especially for complex tasks. The user interface for word processing, for example, is easy to learn for new users and is sufficient for most word processing tasks, so it is a mostly solved and standardized UI, while the user interfaces for 3D computer graphics are usually either difficult to learn and use and not sufficiently powerful for complex tasks, or sufficiently powerful but extremely difficult to learn and use, so direct manipulation and user interfaces will vary wildly from application to application.'
 225+ }
 226+ }
 227+ ]
 228+ },
 229+ 'Formatting': {
 230+ 'type': 'document',
 231+ 'children': [
 232+ {
 233+ 'type': 'heading',
 234+ 'attributes': { 'level': 1 },
 235+ 'content': {
 236+ 'text': 'This is a heading (level 1)',
 237+ 'annotations': [
 238+ {
 239+ 'type': 'textStyle/italic',
 240+ 'range': {
 241+ 'start': 10,
 242+ 'end': 17
 243+ }
 244+ }
 245+ ]
 246+ }
 247+ },
 248+ {
 249+ 'type': 'paragraph',
 250+ 'content': { 'text': 'Paragraph' }
 251+ },
 252+ {
 253+ 'type': 'heading',
 254+ 'attributes': { 'level': 2 },
 255+ 'content': {
 256+ 'text': 'This is a heading (level 2)',
 257+ 'annotations': [
 258+ {
 259+ 'type': 'textStyle/italic',
 260+ 'range': {
 261+ 'start': 10,
 262+ 'end': 17
 263+ }
 264+ }
 265+ ]
 266+ }
 267+ },
 268+ {
 269+ 'type': 'paragraph',
 270+ 'content': { 'text': 'Paragraph' }
 271+ },
 272+ {
 273+ 'type': 'heading',
 274+ 'attributes': { 'level': 3 },
 275+ 'content': {
 276+ 'text': 'This is a heading (level 3)',
 277+ 'annotations': [
 278+ {
 279+ 'type': 'textStyle/italic',
 280+ 'range': {
 281+ 'start': 10,
 282+ 'end': 17
 283+ }
 284+ }
 285+ ]
 286+ }
 287+ },
 288+ {
 289+ 'type': 'paragraph',
 290+ 'content': { 'text': 'Paragraph' }
 291+ },
 292+ {
 293+ 'type': 'heading',
 294+ 'attributes': { 'level': 4 },
 295+ 'content': {
 296+ 'text': 'This is a heading (level 4)',
 297+ 'annotations': [
 298+ {
 299+ 'type': 'textStyle/italic',
 300+ 'range': {
 301+ 'start': 10,
 302+ 'end': 17
 303+ }
 304+ }
 305+ ]
 306+ }
 307+ },
 308+ {
 309+ 'type': 'paragraph',
 310+ 'content': { 'text': 'Paragraph' }
 311+ },
 312+ {
 313+ 'type': 'heading',
 314+ 'attributes': { 'level': 5 },
 315+ 'content': {
 316+ 'text': 'This is a heading (level 5)',
 317+ 'annotations': [
 318+ {
 319+ 'type': 'textStyle/italic',
 320+ 'range': {
 321+ 'start': 10,
 322+ 'end': 17
 323+ }
 324+ }
 325+ ]
 326+ }
 327+ },
 328+ {
 329+ 'type': 'paragraph',
 330+ 'content': { 'text': 'Paragraph' }
 331+ },
 332+ {
 333+ 'type': 'heading',
 334+ 'attributes': { 'level': 6 },
 335+ 'content': {
 336+ 'text': 'This is a heading (level 6)',
 337+ 'annotations': [
 338+ {
 339+ 'type': 'textStyle/italic',
 340+ 'range': {
 341+ 'start': 10,
 342+ 'end': 17
 343+ }
 344+ }
 345+ ]
 346+ }
 347+ },
 348+ {
 349+ 'type': 'paragraph',
 350+ 'content': { 'text': 'Paragraph' }
 351+ },
 352+ {
 353+ 'type': 'pre',
 354+ 'content': { 'text': 'A lot of text goes here... and at some point it wraps.. A lot of text goes here... and at some point it wraps.. A lot of text goes here... and at some point it wraps.. A lot of text goes here... and at some point it wraps.. A lot of text goes here... and at some point it wraps..' }
 355+ },
 356+ {
 357+ 'type': 'heading',
 358+ 'attributes': { 'level': 1 },
 359+ 'content': { 'text': 'Lists' }
 360+ },
 361+ {
 362+ 'type': 'list',
 363+ 'children': [
 364+ {
 365+ 'type': 'listItem',
 366+ 'attributes': {
 367+ 'styles': ['bullet']
 368+ },
 369+ 'children' : [
 370+ {
 371+ 'type': 'paragraph',
 372+ 'content': { 'text': 'Bullet' }
 373+ }
 374+ ]
 375+ }
 376+ ]
 377+ },
 378+ {
 379+ 'type': 'paragraph',
 380+ 'content': { 'text': 'Paragraph' }
 381+ },
 382+ {
 383+ 'type': 'list',
 384+ 'children': [
 385+ {
 386+ 'type': 'listItem',
 387+ 'attributes': {
 388+ 'styles': ['bullet']
 389+ },
 390+ 'children' : [
 391+ {
 392+ 'type': 'paragraph',
 393+ 'content': { 'text': 'Bullet' }
 394+ }
 395+ ]
 396+ },
 397+ {
 398+ 'type': 'listItem',
 399+ 'attributes': {
 400+ 'styles': ['bullet', 'bullet']
 401+ },
 402+ 'children' : [
 403+ {
 404+ 'type': 'paragraph',
 405+ 'content': { 'text': 'Bullet bullet' }
 406+ }
 407+ ]
 408+ },
 409+ {
 410+ 'type': 'listItem',
 411+ 'attributes': {
 412+ 'styles': ['bullet', 'bullet', 'bullet']
 413+ },
 414+ 'children' : [
 415+ {
 416+ 'type': 'paragraph',
 417+ 'content': { 'text': 'Bullet bullet bullet' }
 418+ }
 419+ ]
 420+ },
 421+ {
 422+ 'type': 'listItem',
 423+ 'attributes': {
 424+ 'styles': ['number']
 425+ },
 426+ 'children' : [
 427+ {
 428+ 'type': 'paragraph',
 429+ 'content': { 'text': 'Number' }
 430+ }
 431+ ]
 432+ },
 433+ {
 434+ 'type': 'listItem',
 435+ 'attributes': {
 436+ 'styles': ['number', 'number']
 437+ },
 438+ 'children' : [
 439+ {
 440+ 'type': 'paragraph',
 441+ 'content': { 'text': 'Number number' }
 442+ }
 443+ ]
 444+ },
 445+ {
 446+ 'type': 'listItem',
 447+ 'attributes': {
 448+ 'styles': ['term']
 449+ },
 450+ 'children' : [
 451+ {
 452+ 'type': 'paragraph',
 453+ 'content': { 'text': 'Term' }
 454+ }
 455+ ]
 456+ },
 457+ {
 458+ 'type': 'listItem',
 459+ 'attributes': {
 460+ 'styles': ['definition']
 461+ },
 462+ 'children' : [
 463+ {
 464+ 'type': 'paragraph',
 465+ 'content': { 'text': 'Definition' }
 466+ }
 467+ ]
 468+ }
 469+ ]
 470+ }
 471+ ]
 472+ },
 473+ /*
 474+ 'Tables': {
 475+ 'type': 'document',
 476+ 'children': [
 477+ {
 478+ 'type': 'heading',
 479+ 'attributes': { 'level': 1 },
 480+ 'content': { 'text': 'Tables' }
 481+ },
 482+ {
 483+ 'type': 'table',
 484+ 'attributes': { 'html/style': 'width: 600px; border: solid 1px;' },
 485+ 'children': [
 486+ {
 487+ 'type': 'tableRow',
 488+ 'children': [
 489+ {
 490+ 'type': 'tableCell',
 491+ 'attributes': { 'html/style': 'border: solid 1px;' },
 492+ 'children': [
 493+ {
 494+ 'type': 'paragraph',
 495+ 'content': { 'text': 'row 1 & cell 1' }
 496+ },
 497+ {
 498+ 'type': 'list',
 499+ 'children': [
 500+ {
 501+ 'type': 'listItem',
 502+ 'attributes': {
 503+ 'styles': ['bullet']
 504+ },
 505+ 'children' : [
 506+ {
 507+ 'type': 'paragraph',
 508+ 'content': { 'text': 'Test 4444' }
 509+ }
 510+ ]
 511+ },
 512+ {
 513+ 'type': 'listItem',
 514+ 'attributes': {
 515+ 'styles': ['bullet', 'bullet']
 516+ },
 517+ 'children' : [
 518+ {
 519+ 'type': 'paragraph',
 520+ 'content': { 'text': 'Test 55555' }
 521+ }
 522+ ]
 523+ },
 524+ {
 525+ 'type': 'listItem',
 526+ 'attributes': {
 527+ 'styles': ['number']
 528+ },
 529+ 'children' : [
 530+ {
 531+ 'type': 'paragraph',
 532+ 'content': { 'text': 'Test 666666' }
 533+ }
 534+ ]
 535+ }
 536+ ]
 537+ }
 538+ ]
 539+ },
 540+ {
 541+ 'type': 'tableCell',
 542+ 'attributes': { 'html/style': 'border: solid 1px;' },
 543+ 'children': [
 544+ {
 545+ 'type': 'paragraph',
 546+ 'content': { 'text': 'row 1 & cell 2' }
 547+ }
 548+ ]
 549+ }
 550+ ]
 551+ }
 552+ ]
 553+ }
 554+ ]
 555+ },*/
 556+ 'New document': {
 557+ 'type': 'document',
 558+ 'children': [
 559+ {
 560+ 'type': 'paragraph',
 561+ 'content': { 'text': '' }
 562+ }
 563+ ]
 564+ }
 565+ };
 566+ window.documentModel = ve.dm.DocumentNode.newFromPlainObject( wikidoms['Wikipedia article'] );
 567+ window.surfaceModel = new ve.dm.Surface( window.documentModel );
 568+ window.surfaceView = new ve.es.Surface( $( '#es-editor' ), window.surfaceModel );
 569+ window.toolbarView = new ve.ui.Toolbar( $( '#es-toolbar' ), window.surfaceView );
 570+ window.contextView = new ve.ui.Context( window.surfaceView );
 571+ window.surfaceModel.select( new ve.Range( 1, 1 ) );
 572+
 573+ /*
 574+ * This code is responsible for switching toolbar into floating mode when scrolling (with
 575+ * keyboard or mouse).
 576+ */
 577+ var $toolbarWrapper = $( '#es-toolbar-wrapper' ),
 578+ $toolbar = $( '#es-toolbar' ),
 579+ $window = $( window );
 580+ $window.scroll( function() {
 581+ var toolbarWrapperOffset = $toolbarWrapper.offset();
 582+ if ( $window.scrollTop() > toolbarWrapperOffset.top ) {
 583+ if ( !$toolbarWrapper.hasClass( 'float' ) ) {
 584+ var left = toolbarWrapperOffset.left,
 585+ right = $window.width() - $toolbarWrapper.outerWidth() - left;
 586+ $toolbarWrapper.css( 'height', $toolbarWrapper.height() ).addClass( 'float' );
 587+ $toolbar.css( { 'left': left, 'right': right } );
 588+ }
 589+ } else {
 590+ if ( $toolbarWrapper.hasClass( 'float' ) ) {
 591+ $toolbarWrapper.css( 'height', 'auto' ).removeClass( 'float' );
 592+ $toolbar.css( { 'left': 0, 'right': 0 } );
 593+ }
 594+ }
 595+ } );
 596+
 597+ var $modeButtons = $( '.es-modes-button' ),
 598+ $panels = $( '.es-panel' ),
 599+ $base = $( '#es-base' ),
 600+ currentMode = null,
 601+ modes = {
 602+ 'wikitext': {
 603+ '$': $( '#es-mode-wikitext' ),
 604+ '$panel': $( '#es-panel-wikitext' ),
 605+ 'update': function() {
 606+ this.$panel.text(
 607+ ve.dm.WikitextSerializer.stringify( documentModel.getPlainObject() )
 608+ );
 609+ }
 610+ },
 611+ 'json': {
 612+ '$': $( '#es-mode-json' ),
 613+ '$panel': $( '#es-panel-json' ),
 614+ 'update': function() {
 615+ this.$panel.text( ve.dm.JsonSerializer.stringify( documentModel.getPlainObject(), {
 616+ 'indentWith': ' '
 617+ } ) );
 618+ }
 619+ },
 620+ 'html': {
 621+ '$': $( '#es-mode-html' ),
 622+ '$panel': $( '#es-panel-html' ),
 623+ 'update': function() {
 624+ this.$panel.text(
 625+ ve.dm.HtmlSerializer.stringify( documentModel.getPlainObject() )
 626+ );
 627+ }
 628+ },
 629+ 'render': {
 630+ '$': $( '#es-mode-render' ),
 631+ '$panel': $( '#es-panel-render' ),
 632+ 'update': function() {
 633+ this.$panel.html(
 634+ ve.dm.HtmlSerializer.stringify( documentModel.getPlainObject() )
 635+ );
 636+ }
 637+ },
 638+ 'history': {
 639+ '$': $( '#es-mode-history' ),
 640+ '$panel': $( '#es-panel-history' ),
 641+ 'update': function() {
 642+ var history = surfaceModel.getHistory(),
 643+ i = history.length,
 644+ end = Math.max( 0, i - 25 ),
 645+ j,
 646+ k,
 647+ ops,
 648+ events = '',
 649+ z = 0,
 650+ operations;
 651+
 652+ while ( --i >= end ) {
 653+ z++;
 654+ operations = [];
 655+ for ( j = 0; j < history[i].stack.length; j++) {
 656+ ops = history[i].stack[j].getOperations().slice(0);
 657+ for ( k = 0; k < ops.length; k++ ) {
 658+ data = ops[k].data || ops[k].length;
 659+ if ( ve.isArray( data ) ) {
 660+ data = data[0];
 661+ if ( ve.isArray( data ) ) {
 662+ data = data[0];
 663+ }
 664+ }
 665+ if ( typeof data !== 'string' && typeof data !== 'number' ) {
 666+ data = '-';
 667+ }
 668+ ops[k] = ops[k].type.substr( 0, 3 ) + '(' + data + ')';
 669+ }
 670+ operations.push('[' + ops.join( ', ' ) + ']');
 671+ }
 672+ events += '<div' + (z === surfaceModel.undoIndex ? ' class="es-panel-history-active"' : '') + '>' + operations.join(', ') + '</div>';
 673+ }
 674+
 675+ this.$panel.html( events );
 676+ }
 677+ },
 678+ 'help': {
 679+ '$': $( '#es-mode-help' ),
 680+ '$panel': $( '#es-panel-help' ),
 681+ 'update': function() {}
 682+ }
 683+ };
 684+ $.each( modes, function( name, mode ) {
 685+ mode.$.click( function() {
 686+ var disable = $(this).hasClass( 'es-modes-button-down' );
 687+ var visible = $base.hasClass( 'es-showData' );
 688+ $modeButtons.removeClass( 'es-modes-button-down' );
 689+ $panels.hide();
 690+ if ( disable ) {
 691+ if ( visible ) {
 692+ $base.removeClass( 'es-showData' );
 693+ $window.resize();
 694+ }
 695+ currentMode = null;
 696+ } else {
 697+ $(this).addClass( 'es-modes-button-down' );
 698+ mode.$panel.show();
 699+ if ( !visible ) {
 700+ $base.addClass( 'es-showData' );
 701+ $window.resize();
 702+ }
 703+ mode.update.call( mode );
 704+ currentMode = mode;
 705+ }
 706+ } );
 707+ } );
 708+
 709+ var $docsList = $( '#es-docs-list' );
 710+ $.each( wikidoms, function( title, wikidom ) {
 711+ $docsList.append(
 712+ $( '<li class="es-docs-listItem"></li>' )
 713+ .append(
 714+ $( '<a href="#"></a>' )
 715+ .text( title )
 716+ .click( function() {
 717+ var newDocumentModel = ve.dm.DocumentNode.newFromPlainObject( wikidom );
 718+ documentModel.data.splice( 0, documentModel.data.length );
 719+ ve.insertIntoArray( documentModel.data, 0, newDocumentModel.data );
 720+ surfaceModel.select( new ve.Range( 1, 1 ) );
 721+ documentModel.splice.apply(
 722+ documentModel,
 723+ [0, documentModel.getChildren().length]
 724+ .concat( newDocumentModel.getChildren() )
 725+ );
 726+ surfaceModel.purgeHistory();
 727+
 728+ if ( currentMode ) {
 729+ currentMode.update.call( currentMode );
 730+ }
 731+ return false;
 732+ } )
 733+ )
 734+ );
 735+ } );
 736+
 737+ surfaceModel.on( 'transact', function() {
 738+ if ( currentMode ) {
 739+ currentMode.update.call( currentMode );
 740+ }
 741+ } );
 742+ surfaceModel.on( 'select', function() {
 743+ if ( currentMode === modes.history ) {
 744+ currentMode.update.call( currentMode );
 745+ }
 746+ } );
 747+
 748+ $( '#es-docs' ).css( { 'visibility': 'visible' } );
 749+ $( '#es-base' ).css( { 'visibility': 'visible' } );
 750+} );
Index: trunk/extensions/VisualEditor/demo-ce/index.php
@@ -0,0 +1,156 @@
 2+<!DOCTYPE html>
 3+
 4+<html>
 5+ <head>
 6+ <title>ContentEditable Demo</title>
 7+ <!-- es -->
 8+ <link rel="stylesheet" href="../modules/ve/ce/styles/ve.es.Document.css">
 9+ <link rel="stylesheet" href="../modules/ve/es/styles/ve.es.Content.css">
 10+ <link rel="stylesheet" href="../modules/ve/ce/styles/ve.es.Surface.css">
 11+ <!-- ui -->
 12+ <link rel="stylesheet" href="../modules/ve/ui/styles/ve.ui.Context.css">
 13+ <link rel="stylesheet" href="../modules/ve/ui/styles/ve.ui.Inspector.css">
 14+ <link rel="stylesheet" href="../modules/ve/ui/styles/ve.ui.Menu.css">
 15+ <link rel="stylesheet" href="../modules/ve/ui/styles/ve.ui.Toolbar.css">
 16+ <!-- sandbox -->
 17+ <link rel="stylesheet" href="../modules/sandbox/sandbox.css">
 18+ <style>
 19+ body {
 20+ font-family: "Arial";
 21+ font-size: 1em;
 22+ width: 100%;
 23+ margin: 1em 0;
 24+ padding: 0;
 25+ overflow-y: scroll;
 26+ background-color: white;
 27+ }
 28+ #es-base {
 29+ margin: 2em;
 30+ margin-top: 0em;
 31+ -webkit-box-shadow: 0 0.25em 1.5em 0 #dddddd;
 32+ -moz-box-shadow: 0 0.25em 1.5em 0 #dddddd;
 33+ box-shadow: 0 0.25em 1.5em 0 #dddddd;
 34+ -webkit-border-radius: 0.5em;
 35+ -moz-border-radius: 0.5em;
 36+ -o-border-radius: 0.5em;
 37+ border-radius: 0.5em;
 38+ }
 39+ #es-panes {
 40+ border: solid 1px #cccccc;
 41+ border-top: none;
 42+ }
 43+ #es-visual {
 44+ padding-left: 1em;
 45+ padding-right: 1em;
 46+ }
 47+ #es-toolbar {
 48+ -webkit-border-radius: 0;
 49+ -moz-border-radius: 0;
 50+ -o-border-radius: 0;
 51+ border-radius: 0;
 52+ -webkit-border-top-right-radius: 0.25em;
 53+ -moz-border-top-right-radius: 0.25em;
 54+ -o-border-top-right-radius: 0.25em;
 55+ border-top-right-radius: 0.25em;
 56+ -webkit-border-top-left-radius: 0.25em;
 57+ -moz-border-top-left-radius: 0.25em;
 58+ -o-border-top-left-radius: 0.25em;
 59+ border-top-left-radius: 0.25em;
 60+ }
 61+ #es-toolbar.float {
 62+ left: 2em;
 63+ right: 2em;
 64+ top: 0;
 65+ }
 66+ #es-docs {
 67+ margin-left: 2.5em;
 68+ }
 69+ </style>
 70+ </head>
 71+ <body>
 72+<?php
 73+$modeWikitext = "Toggle wikitext view";
 74+$modeJson = "Toggle JSON view";
 75+$modeHtml = "Toggle HTML view";
 76+$modeRender = "Toggle preview";
 77+$modeHistory = "Toggle transaction history view";
 78+$modeHelp = "Toggle help view";
 79+
 80+include( '../modules/sandbox/base.php' );
 81+
 82+?>
 83+ <!-- ve -->
 84+ <script src="../modules/jquery/jquery.js"></script>
 85+ <script src="../modules/ve/ve.js"></script>
 86+ <script src="../modules/ve/ve.Position.js"></script>
 87+ <script src="../modules/ve/ve.Range.js"></script>
 88+ <script src="../modules/ve/ve.EventEmitter.js"></script>
 89+ <script src="../modules/ve/ve.Node.js"></script>
 90+ <script src="../modules/ve/ve.BranchNode.js"></script>
 91+ <script src="../modules/ve/ve.LeafNode.js"></script>
 92+
 93+ <!-- dm -->
 94+ <script src="../modules/ve/dm/ve.dm.js"></script>
 95+ <script src="../modules/ve/dm/ve.dm.Node.js"></script>
 96+ <script src="../modules/ve/dm/ve.dm.BranchNode.js"></script>
 97+ <script src="../modules/ve/dm/ve.dm.LeafNode.js"></script>
 98+ <script src="../modules/ve/dm/ve.dm.TransactionProcessor.js"></script>
 99+ <script src="../modules/ve/dm/ve.dm.Transaction.js"></script>
 100+ <script src="../modules/ve/dm/ve.dm.Surface.js"></script>
 101+
 102+ <script src="../modules/ve/dm/nodes/ve.dm.DocumentNode.js"></script>
 103+ <script src="../modules/ve/dm/nodes/ve.dm.HeadingNode.js"></script>
 104+ <script src="../modules/ve/dm/nodes/ve.dm.ParagraphNode.js"></script>
 105+ <script src="../modules/ve/dm/nodes/ve.dm.PreNode.js"></script>
 106+ <script src="../modules/ve/dm/nodes/ve.dm.ListItemNode.js"></script>
 107+ <script src="../modules/ve/dm/nodes/ve.dm.ListNode.js"></script>
 108+ <script src="../modules/ve/dm/nodes/ve.dm.TableCellNode.js"></script>
 109+ <script src="../modules/ve/dm/nodes/ve.dm.TableNode.js"></script>
 110+ <script src="../modules/ve/dm/nodes/ve.dm.TableRowNode.js"></script>
 111+
 112+ <script src="../modules/ve/dm/serializers/ve.dm.AnnotationSerializer.js"></script>
 113+ <script src="../modules/ve/dm/serializers/ve.dm.HtmlSerializer.js"></script>
 114+ <script src="../modules/ve/dm/serializers/ve.dm.JsonSerializer.js"></script>
 115+ <script src="../modules/ve/dm/serializers/ve.dm.WikitextSerializer.js"></script>
 116+
 117+ <!-- es -->
 118+ <script src="../modules/ve/es/ve.es.js"></script>
 119+ <script src="../modules/ve/es/ve.es.Node.js"></script>
 120+ <script src="../modules/ve/es/ve.es.BranchNode.js"></script>
 121+ <script src="../modules/ve/es/ve.es.LeafNode.js"></script>
 122+ <script src="../modules/ve/ce/ve.es.Content.js"></script>
 123+ <script src="../modules/ve/ce/ve.es.Surface.js"></script>
 124+
 125+ <script src="../modules/ve/ce/nodes/ve.es.DocumentNode.js"></script>
 126+ <script src="../modules/ve/es/nodes/ve.es.HeadingNode.js"></script>
 127+ <script src="../modules/ve/es/nodes/ve.es.ParagraphNode.js"></script>
 128+ <script src="../modules/ve/es/nodes/ve.es.PreNode.js"></script>
 129+ <script src="../modules/ve/es/nodes/ve.es.ListItemNode.js"></script>
 130+ <script src="../modules/ve/es/nodes/ve.es.ListNode.js"></script>
 131+ <script src="../modules/ve/es/nodes/ve.es.TableCellNode.js"></script>
 132+ <script src="../modules/ve/es/nodes/ve.es.TableNode.js"></script>
 133+ <script src="../modules/ve/es/nodes/ve.es.TableRowNode.js"></script>
 134+
 135+ <!-- ui -->
 136+ <script src="../modules/ve/ui/ve.ui.js"></script>
 137+ <script src="../modules/ve/ui/ve.ui.Inspector.js"></script>
 138+ <script src="../modules/ve/ui/ve.ui.Tool.js"></script>
 139+ <script src="../modules/ve/ui/ve.ui.Toolbar.js"></script>
 140+ <script src="../modules/ve/ui/ve.ui.Context.js"></script>
 141+ <script src="../modules/ve/ui/ve.ui.Menu.js"></script>
 142+
 143+ <script src="../modules/ve/ui/inspectors/ve.ui.LinkInspector.js"></script>
 144+
 145+ <script src="../modules/ve/ui/tools/ve.ui.ButtonTool.js"></script>
 146+ <script src="../modules/ve/ui/tools/ve.ui.AnnotationButtonTool.js"></script>
 147+ <script src="../modules/ve/ui/tools/ve.ui.ClearButtonTool.js"></script>
 148+ <script src="../modules/ve/ui/tools/ve.ui.HistoryButtonTool.js"></script>
 149+ <script src="../modules/ve/ui/tools/ve.ui.ListButtonTool.js"></script>
 150+ <script src="../modules/ve/ui/tools/ve.ui.IndentationButtonTool.js"></script>
 151+ <script src="../modules/ve/ui/tools/ve.ui.DropdownTool.js"></script>
 152+ <script src="../modules/ve/ui/tools/ve.ui.FormatDropdownTool.js"></script>
 153+
 154+ <!-- sandbox -->
 155+ <script src="main.js"></script>
 156+ </body>
 157+</html>

Status & tagging log