1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 (function() 7 { 8 var guardElements = { table:1, ul:1, ol:1, blockquote:1, div:1 }, 9 directSelectionGuardElements = {}, 10 // All guard elements which can have a direction applied on them. 11 allGuardElements = {}; 12 CKEDITOR.tools.extend( directSelectionGuardElements, guardElements, { tr:1, p:1, div:1, li:1 } ); 13 CKEDITOR.tools.extend( allGuardElements, directSelectionGuardElements, { td:1 } ); 14 15 function onSelectionChange( e ) 16 { 17 setToolbarStates( e ); 18 handleMixedDirContent( e ); 19 } 20 21 function setToolbarStates( evt ) 22 { 23 var editor = evt.editor, 24 path = evt.data.path; 25 26 if ( editor.readOnly ) 27 return; 28 29 var useComputedState = editor.config.useComputedState, 30 selectedElement; 31 32 useComputedState = useComputedState === undefined || useComputedState; 33 34 // We can use computedState provided by the browser or traverse parents manually. 35 if ( !useComputedState ) 36 selectedElement = getElementForDirection( path.lastElement ); 37 38 selectedElement = selectedElement || path.block || path.blockLimit; 39 40 // If we're having BODY here, user probably done CTRL+A, let's try to get the enclosed node, if any. 41 if ( selectedElement.is( 'body' ) ) 42 { 43 var enclosedNode = editor.getSelection().getRanges()[ 0 ].getEnclosedNode(); 44 enclosedNode && enclosedNode.type == CKEDITOR.NODE_ELEMENT && ( selectedElement = enclosedNode ); 45 } 46 47 if ( !selectedElement ) 48 return; 49 50 var selectionDir = useComputedState ? 51 selectedElement.getComputedStyle( 'direction' ) : 52 selectedElement.getStyle( 'direction' ) || selectedElement.getAttribute( 'dir' ); 53 54 editor.getCommand( 'bidirtl' ).setState( selectionDir == 'rtl' ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF ); 55 editor.getCommand( 'bidiltr' ).setState( selectionDir == 'ltr' ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF ); 56 } 57 58 function handleMixedDirContent( evt ) 59 { 60 var editor = evt.editor, 61 directionNode = evt.data.path.block || evt.data.path.blockLimit; 62 63 editor.fire( 'contentDirChanged', directionNode ? directionNode.getComputedStyle( 'direction' ) : editor.lang.dir ); 64 } 65 66 /** 67 * Returns element with possibility of applying the direction. 68 * @param node 69 */ 70 function getElementForDirection( node ) 71 { 72 while ( node && !( node.getName() in allGuardElements || node.is( 'body' ) ) ) 73 { 74 var parent = node.getParent(); 75 if ( !parent ) 76 break; 77 78 node = parent; 79 } 80 81 return node; 82 } 83 84 function switchDir( element, dir, editor, database ) 85 { 86 if ( element.isReadOnly() ) 87 return; 88 89 // Mark this element as processed by switchDir. 90 CKEDITOR.dom.element.setMarker( database, element, 'bidi_processed', 1 ); 91 92 // Check whether one of the ancestors has already been styled. 93 var parent = element; 94 while ( ( parent = parent.getParent() ) && !parent.is( 'body' ) ) 95 { 96 if ( parent.getCustomData( 'bidi_processed' ) ) 97 { 98 // Ancestor style must dominate. 99 element.removeStyle( 'direction' ); 100 element.removeAttribute( 'dir' ); 101 return; 102 } 103 } 104 105 var useComputedState = ( 'useComputedState' in editor.config ) ? editor.config.useComputedState : 1; 106 107 var elementDir = useComputedState ? element.getComputedStyle( 'direction' ) 108 : element.getStyle( 'direction' ) || element.hasAttribute( 'dir' ); 109 110 // Stop if direction is same as present. 111 if ( elementDir == dir ) 112 return; 113 114 // Clear direction on this element. 115 element.removeStyle( 'direction' ); 116 117 // Do the second check when computed state is ON, to check 118 // if we need to apply explicit direction on this element. 119 if ( useComputedState ) 120 { 121 element.removeAttribute( 'dir' ); 122 if ( dir != element.getComputedStyle( 'direction' ) ) 123 element.setAttribute( 'dir', dir ); 124 } 125 else 126 // Set new direction for this element. 127 element.setAttribute( 'dir', dir ); 128 129 editor.forceNextSelectionCheck(); 130 131 return; 132 } 133 134 function getFullySelected( range, elements, enterMode ) 135 { 136 var ancestor = range.getCommonAncestor( false, true ); 137 138 range = range.clone(); 139 range.enlarge( enterMode == CKEDITOR.ENTER_BR ? 140 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS 141 : CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 142 143 if ( range.checkBoundaryOfElement( ancestor, CKEDITOR.START ) 144 && range.checkBoundaryOfElement( ancestor, CKEDITOR.END ) ) 145 { 146 var parent; 147 while ( ancestor && ancestor.type == CKEDITOR.NODE_ELEMENT 148 && ( parent = ancestor.getParent() ) 149 && parent.getChildCount() == 1 150 && !( ancestor.getName() in elements ) ) 151 ancestor = parent; 152 153 return ancestor.type == CKEDITOR.NODE_ELEMENT 154 && ( ancestor.getName() in elements ) 155 && ancestor; 156 } 157 } 158 159 function bidiCommand( dir ) 160 { 161 return function( editor ) 162 { 163 var selection = editor.getSelection(), 164 enterMode = editor.config.enterMode, 165 ranges = selection.getRanges(); 166 167 if ( ranges && ranges.length ) 168 { 169 var database = {}; 170 171 // Creates bookmarks for selection, as we may split some blocks. 172 var bookmarks = selection.createBookmarks(); 173 174 var rangeIterator = ranges.createIterator(), 175 range, 176 i = 0; 177 178 while ( ( range = rangeIterator.getNextRange( 1 ) ) ) 179 { 180 // Apply do directly selected elements from guardElements. 181 var selectedElement = range.getEnclosedNode(); 182 183 // If this is not our element of interest, apply to fully selected elements from guardElements. 184 if ( !selectedElement || selectedElement 185 && !( selectedElement.type == CKEDITOR.NODE_ELEMENT && selectedElement.getName() in directSelectionGuardElements ) 186 ) 187 selectedElement = getFullySelected( range, guardElements, enterMode ); 188 189 selectedElement && switchDir( selectedElement, dir, editor, database ); 190 191 var iterator, 192 block; 193 194 // Walker searching for guardElements. 195 var walker = new CKEDITOR.dom.walker( range ); 196 197 var start = bookmarks[ i ].startNode, 198 end = bookmarks[ i++ ].endNode; 199 200 walker.evaluator = function( node ) 201 { 202 return !! ( node.type == CKEDITOR.NODE_ELEMENT 203 && node.getName() in guardElements 204 && !( node.getName() == ( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) 205 && node.getParent().type == CKEDITOR.NODE_ELEMENT 206 && node.getParent().getName() == 'blockquote' ) 207 // Element must be fully included in the range as well. (#6485). 208 && node.getPosition( start ) & CKEDITOR.POSITION_FOLLOWING 209 && ( ( node.getPosition( end ) & CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_CONTAINS ) == CKEDITOR.POSITION_PRECEDING ) ); 210 }; 211 212 while ( ( block = walker.next() ) ) 213 switchDir( block, dir, editor, database ); 214 215 iterator = range.createIterator(); 216 iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR; 217 218 while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) 219 switchDir( block, dir, editor, database ); 220 } 221 222 CKEDITOR.dom.element.clearAllMarkers( database ); 223 224 editor.forceNextSelectionCheck(); 225 // Restore selection position. 226 selection.selectBookmarks( bookmarks ); 227 228 editor.focus(); 229 } 230 }; 231 } 232 233 CKEDITOR.plugins.add( 'bidi', 234 { 235 requires : [ 'styles', 'button' ], 236 237 init : function( editor ) 238 { 239 // All buttons use the same code to register. So, to avoid 240 // duplications, let's use this tool function. 241 var addButtonCommand = function( buttonName, buttonLabel, commandName, commandExec ) 242 { 243 editor.addCommand( commandName, new CKEDITOR.command( editor, { exec : commandExec }) ); 244 245 editor.ui.addButton( buttonName, 246 { 247 label : buttonLabel, 248 command : commandName 249 }); 250 }; 251 252 var lang = editor.lang.bidi; 253 254 addButtonCommand( 'BidiLtr', lang.ltr, 'bidiltr', bidiCommand( 'ltr' ) ); 255 addButtonCommand( 'BidiRtl', lang.rtl, 'bidirtl', bidiCommand( 'rtl' ) ); 256 257 editor.on( 'selectionChange', onSelectionChange ); 258 editor.on( 'contentDom', function() 259 { 260 editor.document.on( 'dirChanged', function( evt ) 261 { 262 editor.fire( 'dirChanged', 263 { 264 node : evt.data, 265 dir : evt.data.getDirection( 1 ) 266 } ); 267 }); 268 }); 269 } 270 }); 271 272 // If the element direction changed, we need to switch the margins of 273 // the element and all its children, so it will get really reflected 274 // like a mirror. (#5910) 275 function isOffline( el ) 276 { 277 var html = el.getDocument().getBody().getParent(); 278 while ( el ) 279 { 280 if ( el.equals( html ) ) 281 return false; 282 el = el.getParent(); 283 } 284 return true; 285 } 286 function dirChangeNotifier( org ) 287 { 288 var isAttribute = org == elementProto.setAttribute, 289 isRemoveAttribute = org == elementProto.removeAttribute, 290 dirStyleRegexp = /\bdirection\s*:\s*(.*?)\s*(:?$|;)/; 291 292 return function( name, val ) 293 { 294 if ( !this.getDocument().equals( CKEDITOR.document ) ) 295 { 296 var orgDir; 297 if ( ( name == ( isAttribute || isRemoveAttribute ? 'dir' : 'direction' ) || 298 name == 'style' && ( isRemoveAttribute || dirStyleRegexp.test( val ) ) ) && !isOffline( this ) ) 299 { 300 orgDir = this.getDirection( 1 ); 301 var retval = org.apply( this, arguments ); 302 if ( orgDir != this.getDirection( 1 ) ) 303 { 304 this.getDocument().fire( 'dirChanged', this ); 305 return retval; 306 } 307 } 308 } 309 310 return org.apply( this, arguments ); 311 }; 312 } 313 314 var elementProto = CKEDITOR.dom.element.prototype, 315 methods = [ 'setStyle', 'removeStyle', 'setAttribute', 'removeAttribute' ]; 316 for ( var i = 0; i < methods.length; i++ ) 317 elementProto[ methods[ i ] ] = CKEDITOR.tools.override( elementProto[ methods [ i ] ], dirChangeNotifier ); 318 })(); 319 320 /** 321 * Fired when the language direction of an element is changed 322 * @name CKEDITOR.editor#dirChanged 323 * @event 324 * @param {CKEDITOR.editor} editor This editor instance. 325 * @param {Object} eventData.node The element that is being changed. 326 * @param {String} eventData.dir The new direction. 327 */ 328 329 /** 330 * Fired when the language direction in the specific cursor position is changed 331 * @name CKEDITOR.editor#contentDirChanged 332 * @event 333 * @param {String} eventData The direction in the current position. 334 */ 335