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 | + '&': '&',
|
| 140 | + '<': '<',
|
| 141 | + '>': '>',
|
| 142 | + '\'': ''',
|
| 143 | + '"': '"',
|
| 144 | + '\n': '<span class="es-contentView-whitespace">¶</span>',
|
| 145 | + '\t': '<span class="es-contentView-whitespace">⇾</span>',
|
| 146 | + //' ': ' '
|
| 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"> </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> </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>
|