/** * VisualEditor content editable TextNode class. * * @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * ContentEditable node for text. * * @class * @constructor * @extends {ve.ce.LeafNode} * @param model {ve.dm.TextNode} Model to observe */ ve.ce.TextNode = function ( model ) { // Inheritance ve.ce.LeafNode.call( this, 'text', model, $( document.createTextNode('') ) ); // Events this.model.addListenerMethod( this, 'update', 'onUpdate' ); // Intialization this.onUpdate( true ); }; /* Static Members */ /** * Node rules. * * @see ve.ce.NodeFactory * @static * @member */ ve.ce.TextNode.rules = { 'canBeSplit': true }; /** * Mapping of character and HTML entities or renderings. * * @static * @member */ ve.ce.TextNode.htmlCharacters = { '&': '&', '<': '<', '>': '>', '\'': ''', '"': '"', '\n': '↵', '\t': '➞' }; /** * List of annotation rendering implementations. * * Each supported annotation renderer must have an open and close property, each either a string or * a function which accepts a data argument. * * @static * @member */ ve.ce.TextNode.annotationRenderers = { 'textStyle/italic': { 'open': '', 'close': '' }, 'textStyle/bold': { 'open': '', 'close': '' }, 'textStyle/underline': { 'open': '', 'close': '' }, 'textStyle/strike': { 'open': '', 'close': '' }, 'textStyle/small': { 'open': '', 'close': '' }, 'textStyle/big': { 'open': '', 'close': '' }, 'textStyle/span': { // TODO recognize attributes 'open': '', 'close': '' }, 'textStyle/strong': { 'open': '', 'close': '' }, 'textStyle/emphasize': { 'open': '', 'close': '' }, 'textStyle/superScript': { 'open': '', 'close': '' }, 'textStyle/subScript': { 'open': '', 'close': '' }, 'link/ExtLink': { 'open': function ( data ) { return ''; }, 'close': '' }, 'link/NumberedExtLink': { 'open': function ( data ) { return ''; }, 'close': '' }, 'link/UrlLink': { 'open': function ( data ) { return ''; }, 'close': '' }, 'link/WikiLink': { 'open': function ( data ) { return ''; }, 'close': '' }, 'link/SimpleWikiLink': { 'open': function ( data ) { return ''; }, 'close': '' }, 'link/unknown': { 'open': function ( data ) { return ''; }, 'close': '' } }; /* Methods */ /** * Responds to model update events. * * If the source changed since last update the image's src attribute will be updated accordingly. * * @method */ ve.ce.TextNode.prototype.onUpdate = function ( force ) { if ( !force && !this.root.getSurface ) { throw new Error( 'Can not update a text node that is not attached to a document' ); } if ( force === true || this.root.getSurface().render === true ) { var $new = $( '' ).html( this.getHtml() ).contents(); if ( $new.length === 0 ) { $new = $new.add( document.createTextNode( '' ) ); } this.$.replaceWith( $new ); this.$ = $new; if ( this.parent ) { this.parent.clean(); if ( ve.debug ) { this.parent.$.css( 'backgroundColor', '#F6F6F6' ); setTimeout( ve.proxy( function () { this.parent.$.css( 'backgroundColor', 'transparent' ); }, this ), 350 ); } } } }; /** * Gets an HTML rendering of data within content model. * * @method * @param {String} Rendered HTML of data within content model */ ve.ce.TextNode.prototype.getHtml = function () { var data = this.model.getDocument().getDataFromNode( this.model ), htmlChars = ve.ce.TextNode.htmlCharacters, renderers = ve.ce.TextNode.annotationRenderers, out = '', i, j, hash, left = '', right, character, nextCharacter, open, close, index, leftPlain, rightPlain, hashStack = [], annotationStack = {}, chr; function replaceWithNonBreakingSpace( index, data ) { if ( ve.isArray( data[index] ) ) { data[index][0] = ' '; } else { data[index] = ' '; } } if ( data.length > 0 ) { character = data[0]; if ( ve.isArray( character ) ? character[0] === ' ' : character === ' ' ) { replaceWithNonBreakingSpace( 0, data ); } } if ( data.length > 1 ) { character = data[data.length - 1]; if ( ve.isArray( character ) ? character[0] === ' ' : character === ' ' ) { replaceWithNonBreakingSpace( data.length - 1, data ); } } if ( data.length > 2 ) { for ( i = 1; i < data.length - 1; i++ ) { character = data[i]; nextCharacter = data[i + 1]; if ( ( ve.isArray( character ) ? character[0] === ' ' : character === ' ' ) && ( ve.isArray( nextCharacter ) ? nextCharacter[0] === ' ' : nextCharacter === ' ' ) ) { replaceWithNonBreakingSpace( i + 1, data ); i++; } } } function openAnnotations( annotations ) { var out = '', annotation, hash; for ( hash in annotations ) { annotation = annotations[hash]; out += typeof renderers[annotation.type].open === 'function' ? renderers[annotation.type].open( annotation.data ) : renderers[annotation.type].open; hashStack.push( hash ); annotationStack[hash] = annotation; } return out; } function closeAnnotations( annotations ) { var out = '', annotation, hash; for ( hash in annotations ) { annotation = annotations[hash]; out += typeof renderers[annotation.type].close === 'function' ? renderers[annotation.type].close( annotation.data ) : renderers[annotation.type].close; hashStack.pop(); delete annotationStack[hash]; } return out; } for ( i = 0; i < data.length; i++ ) { right = data[i]; leftPlain = typeof left === 'string'; rightPlain = typeof right === 'string'; if ( !leftPlain && rightPlain ) { // [formatted][plain] close = {}; for ( j = hashStack.length - 1; j >= 0; j-- ) { close[hashStack[j]] = annotationStack[hashStack[j]]; } out += closeAnnotations( close ); } else if ( leftPlain && !rightPlain ) { // [plain][formatted] out += openAnnotations( right[1] ); } else if ( !leftPlain && !rightPlain ) { // [formatted][formatted] // setting index to undefined is is necessary to it does not use value from // the previous iteration open = {}, index = undefined; for ( hash in left[1] ) { if ( !( hash in right[1] ) ) { index = ( index === undefined ) ? hashStack.indexOf( hash ) : Math.min( index, hashStack.indexOf( hash ) ); } } if ( index !== undefined ) { close = {}; for ( j = hashStack.length - 1; j >= index; j-- ) { close[hashStack[j]] = annotationStack[hashStack[j]]; } for ( j = index; j < hashStack.length; j++ ) { if ( hashStack[j] in right[1] && hashStack[j] in left[1] ) { open[hashStack[j]] = annotationStack[hashStack[j]]; } } out += closeAnnotations( close ); } for ( hash in right[1] ) { if ( !( hash in left[1] ) ) { open[hash] = right[1][hash]; } } out += openAnnotations( open ); } chr = rightPlain ? right : right[0]; out += chr in htmlChars ? htmlChars[chr] : chr; left = right; } close = {}; for ( j = hashStack.length - 1; j >= 0; j-- ) { close[hashStack[j]] = annotationStack[hashStack[j]]; } out += closeAnnotations( close ); return out; }; /* Registration */ ve.ce.nodeFactory.register( 'text', ve.ce.TextNode ); /* Inheritance */ ve.extendClass( ve.ce.TextNode, ve.ce.LeafNode );