1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 /** 7 * @file Blockquote. 8 */ 9 10 (function() 11 { 12 function getState( editor, path ) 13 { 14 var firstBlock = path.block || path.blockLimit; 15 16 if ( !firstBlock || firstBlock.getName() == 'body' ) 17 return CKEDITOR.TRISTATE_OFF; 18 19 // See if the first block has a blockquote parent. 20 if ( firstBlock.getAscendant( 'blockquote', true ) ) 21 return CKEDITOR.TRISTATE_ON; 22 23 return CKEDITOR.TRISTATE_OFF; 24 } 25 26 function onSelectionChange( evt ) 27 { 28 var editor = evt.editor, 29 command = editor.getCommand( 'blockquote' ); 30 command.state = getState( editor, evt.data.path ); 31 command.fire( 'state' ); 32 } 33 34 function noBlockLeft( bqBlock ) 35 { 36 for ( var i = 0, length = bqBlock.getChildCount(), child ; i < length && ( child = bqBlock.getChild( i ) ) ; i++ ) 37 { 38 if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() ) 39 return false; 40 } 41 return true; 42 } 43 44 var commandObject = 45 { 46 exec : function( editor ) 47 { 48 var state = editor.getCommand( 'blockquote' ).state, 49 selection = editor.getSelection(), 50 range = selection && selection.getRanges()[0]; 51 52 if ( !range ) 53 return; 54 55 var bookmarks = selection.createBookmarks(); 56 57 // Kludge for #1592: if the bookmark nodes are in the beginning of 58 // blockquote, then move them to the nearest block element in the 59 // blockquote. 60 if ( CKEDITOR.env.ie ) 61 { 62 var bookmarkStart = bookmarks[0].startNode, 63 bookmarkEnd = bookmarks[0].endNode, 64 cursor; 65 66 if ( bookmarkStart && bookmarkStart.getParent().getName() == 'blockquote' ) 67 { 68 cursor = bookmarkStart; 69 while ( ( cursor = cursor.getNext() ) ) 70 { 71 if ( cursor.type == CKEDITOR.NODE_ELEMENT && 72 cursor.isBlockBoundary() ) 73 { 74 bookmarkStart.move( cursor, true ); 75 break; 76 } 77 } 78 } 79 80 if ( bookmarkEnd 81 && bookmarkEnd.getParent().getName() == 'blockquote' ) 82 { 83 cursor = bookmarkEnd; 84 while ( ( cursor = cursor.getPrevious() ) ) 85 { 86 if ( cursor.type == CKEDITOR.NODE_ELEMENT && 87 cursor.isBlockBoundary() ) 88 { 89 bookmarkEnd.move( cursor ); 90 break; 91 } 92 } 93 } 94 } 95 96 var iterator = range.createIterator(), 97 block; 98 99 if ( state == CKEDITOR.TRISTATE_OFF ) 100 { 101 var paragraphs = []; 102 while ( ( block = iterator.getNextParagraph() ) ) 103 paragraphs.push( block ); 104 105 // If no paragraphs, create one from the current selection position. 106 if ( paragraphs.length < 1 ) 107 { 108 var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ), 109 firstBookmark = bookmarks.shift(); 110 range.insertNode( para ); 111 para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) ); 112 range.moveToBookmark( firstBookmark ); 113 range.selectNodeContents( para ); 114 range.collapse( true ); 115 firstBookmark = range.createBookmark(); 116 paragraphs.push( para ); 117 bookmarks.unshift( firstBookmark ); 118 } 119 120 // Make sure all paragraphs have the same parent. 121 var commonParent = paragraphs[0].getParent(), 122 tmp = []; 123 for ( var i = 0 ; i < paragraphs.length ; i++ ) 124 { 125 block = paragraphs[i]; 126 commonParent = commonParent.getCommonAncestor( block.getParent() ); 127 } 128 129 // The common parent must not be the following tags: table, tbody, tr, ol, ul. 130 var denyTags = { table : 1, tbody : 1, tr : 1, ol : 1, ul : 1 }; 131 while ( denyTags[ commonParent.getName() ] ) 132 commonParent = commonParent.getParent(); 133 134 // Reconstruct the block list to be processed such that all resulting blocks 135 // satisfy parentNode.equals( commonParent ). 136 var lastBlock = null; 137 while ( paragraphs.length > 0 ) 138 { 139 block = paragraphs.shift(); 140 while ( !block.getParent().equals( commonParent ) ) 141 block = block.getParent(); 142 if ( !block.equals( lastBlock ) ) 143 tmp.push( block ); 144 lastBlock = block; 145 } 146 147 // If any of the selected blocks is a blockquote, remove it to prevent 148 // nested blockquotes. 149 while ( tmp.length > 0 ) 150 { 151 block = tmp.shift(); 152 if ( block.getName() == 'blockquote' ) 153 { 154 var docFrag = new CKEDITOR.dom.documentFragment( editor.document ); 155 while ( block.getFirst() ) 156 { 157 docFrag.append( block.getFirst().remove() ); 158 paragraphs.push( docFrag.getLast() ); 159 } 160 161 docFrag.replace( block ); 162 } 163 else 164 paragraphs.push( block ); 165 } 166 167 // Now we have all the blocks to be included in a new blockquote node. 168 var bqBlock = editor.document.createElement( 'blockquote' ); 169 bqBlock.insertBefore( paragraphs[0] ); 170 while ( paragraphs.length > 0 ) 171 { 172 block = paragraphs.shift(); 173 bqBlock.append( block ); 174 } 175 } 176 else if ( state == CKEDITOR.TRISTATE_ON ) 177 { 178 var moveOutNodes = [], 179 database = {}; 180 181 while ( ( block = iterator.getNextParagraph() ) ) 182 { 183 var bqParent = null, 184 bqChild = null; 185 while ( block.getParent() ) 186 { 187 if ( block.getParent().getName() == 'blockquote' ) 188 { 189 bqParent = block.getParent(); 190 bqChild = block; 191 break; 192 } 193 block = block.getParent(); 194 } 195 196 // Remember the blocks that were recorded down in the moveOutNodes array 197 // to prevent duplicates. 198 if ( bqParent && bqChild && !bqChild.getCustomData( 'blockquote_moveout' ) ) 199 { 200 moveOutNodes.push( bqChild ); 201 CKEDITOR.dom.element.setMarker( database, bqChild, 'blockquote_moveout', true ); 202 } 203 } 204 205 CKEDITOR.dom.element.clearAllMarkers( database ); 206 207 var movedNodes = [], 208 processedBlockquoteBlocks = [], 209 database = {}; 210 while ( moveOutNodes.length > 0 ) 211 { 212 var node = moveOutNodes.shift(), 213 bqBlock = node.getParent(); 214 215 // If the node is located at the beginning or the end, just take it out 216 // without splitting. Otherwise, split the blockquote node and move the 217 // paragraph in between the two blockquote nodes. 218 if ( !node.getPrevious() ) 219 node.remove().insertBefore( bqBlock ); 220 else if ( !node.getNext() ) 221 node.remove().insertAfter( bqBlock ); 222 else 223 { 224 node.breakParent( node.getParent() ); 225 processedBlockquoteBlocks.push( node.getNext() ); 226 } 227 228 // Remember the blockquote node so we can clear it later (if it becomes empty). 229 if ( !bqBlock.getCustomData( 'blockquote_processed' ) ) 230 { 231 processedBlockquoteBlocks.push( bqBlock ); 232 CKEDITOR.dom.element.setMarker( database, bqBlock, 'blockquote_processed', true ); 233 } 234 235 movedNodes.push( node ); 236 } 237 238 CKEDITOR.dom.element.clearAllMarkers( database ); 239 240 // Clear blockquote nodes that have become empty. 241 for ( var i = processedBlockquoteBlocks.length - 1 ; i >= 0 ; i-- ) 242 { 243 var bqBlock = processedBlockquoteBlocks[i]; 244 if ( noBlockLeft( bqBlock ) ) 245 bqBlock.remove(); 246 } 247 248 if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) 249 { 250 var firstTime = true; 251 while ( movedNodes.length ) 252 { 253 var node = movedNodes.shift(); 254 255 if ( node.getName() == 'div' ) 256 { 257 var docFrag = new CKEDITOR.dom.documentFragment( editor.document ), 258 needBeginBr = firstTime && node.getPrevious() && 259 !( node.getPrevious().type == CKEDITOR.NODE_ELEMENT && node.getPrevious().isBlockBoundary() ); 260 if ( needBeginBr ) 261 docFrag.append( editor.document.createElement( 'br' ) ); 262 263 var needEngBr = node.getNext() && 264 !( node.getNext().type == CKEDITOR.NODE_ELEMENT && node.getNext().isBlockBoundary() ); 265 while ( node.getFirst() ) 266 node.getFirst().remove().appendTo( docFrag ); 267 268 if ( needEndBr ) 269 docFrag.append( editor.document.createElement( 'br' ) ); 270 271 docFrag.replace( node ); 272 firstTime = false; 273 } 274 } 275 } 276 } 277 278 selection.selectBookmarks( bookmarks ); 279 editor.focus(); 280 } 281 }; 282 283 CKEDITOR.plugins.add( 'blockquote', 284 { 285 init : function( editor ) 286 { 287 editor.addCommand( 'blockquote', commandObject ); 288 289 editor.ui.addButton( 'Blockquote', 290 { 291 label : editor.lang.blockquote, 292 command : 'blockquote' 293 } ); 294 295 editor.on( 'selectionChange', onSelectionChange ); 296 }, 297 298 requires : [ 'domiterator' ] 299 } ); 300 })(); 301