2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 var guardElements
= { table: 1, ul: 1, ol: 1, blockquote: 1, div: 1 },
8 directSelectionGuardElements
= {},
9 // All guard elements which can have a direction applied on them.
10 allGuardElements
= {};
11 CKEDITOR
.tools
.extend( directSelectionGuardElements
, guardElements
, { tr: 1, p: 1, div: 1, li: 1 } );
12 CKEDITOR
.tools
.extend( allGuardElements
, directSelectionGuardElements
, { td: 1 } );
14 function setToolbarStates( editor
, path
) {
15 var useComputedState
= editor
.config
.useComputedState
,
18 useComputedState
= useComputedState
=== undefined || useComputedState
;
20 // We can use computedState provided by the browser or traverse parents manually.
21 if ( !useComputedState
)
22 selectedElement
= getElementForDirection( path
.lastElement
, editor
.editable() );
24 selectedElement
= selectedElement
|| path
.block
|| path
.blockLimit
;
26 // If we're having BODY here, user probably done CTRL+A, let's try to get the enclosed node, if any.
27 if ( selectedElement
.equals( editor
.editable() ) ) {
28 var enclosedNode
= editor
.getSelection().getRanges()[ 0 ].getEnclosedNode();
29 enclosedNode
&& enclosedNode
.type
== CKEDITOR
.NODE_ELEMENT
&& ( selectedElement
= enclosedNode
);
32 if ( !selectedElement
)
35 var selectionDir
= useComputedState
? selectedElement
.getComputedStyle( 'direction' ) : selectedElement
.getStyle( 'direction' ) || selectedElement
.getAttribute( 'dir' );
37 editor
.getCommand( 'bidirtl' ).setState( selectionDir
== 'rtl' ? CKEDITOR
.TRISTATE_ON : CKEDITOR
.TRISTATE_OFF
);
38 editor
.getCommand( 'bidiltr' ).setState( selectionDir
== 'ltr' ? CKEDITOR
.TRISTATE_ON : CKEDITOR
.TRISTATE_OFF
);
41 function handleMixedDirContent( editor
, path
) {
42 var directionNode
= path
.block
|| path
.blockLimit
|| editor
.editable();
43 var pathDir
= directionNode
.getDirection( 1 );
44 if ( pathDir
!= ( editor
._
.selDir
|| editor
.lang
.dir
) ) {
45 editor
._
.selDir
= pathDir
;
46 editor
.fire( 'contentDirChanged', pathDir
);
50 // Returns element with possibility of applying the direction.
52 function getElementForDirection( node
, root
) {
53 while ( node
&& !( node
.getName() in allGuardElements
|| node
.equals( root
) ) ) {
54 var parent
= node
.getParent();
64 function switchDir( element
, dir
, editor
, database
) {
65 if ( element
.isReadOnly() || element
.equals( editor
.editable() ) )
68 // Mark this element as processed by switchDir.
69 CKEDITOR
.dom
.element
.setMarker( database
, element
, 'bidi_processed', 1 );
71 // Check whether one of the ancestors has already been styled.
73 editable
= editor
.editable();
74 while ( ( parent
= parent
.getParent() ) && !parent
.equals( editable
) ) {
75 if ( parent
.getCustomData( 'bidi_processed' ) ) {
76 // Ancestor style must dominate.
77 element
.removeStyle( 'direction' );
78 element
.removeAttribute( 'dir' );
83 var useComputedState
= ( 'useComputedState' in editor
.config
) ? editor
.config
.useComputedState : 1;
85 var elementDir
= useComputedState
? element
.getComputedStyle( 'direction' ) : element
.getStyle( 'direction' ) || element
.hasAttribute( 'dir' );
87 // Stop if direction is same as present.
88 if ( elementDir
== dir
)
91 // Clear direction on this element.
92 element
.removeStyle( 'direction' );
94 // Do the second check when computed state is ON, to check
95 // if we need to apply explicit direction on this element.
96 if ( useComputedState
) {
97 element
.removeAttribute( 'dir' );
98 if ( dir
!= element
.getComputedStyle( 'direction' ) ) {
99 element
.setAttribute( 'dir', dir
);
102 // Set new direction for this element.
103 element
.setAttribute( 'dir', dir
);
106 editor
.forceNextSelectionCheck();
111 function getFullySelected( range
, elements
, enterMode
) {
112 var ancestor
= range
.getCommonAncestor( false, true );
114 range
= range
.clone();
115 range
.enlarge( enterMode
== CKEDITOR
.ENTER_BR
? CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR
.ENLARGE_BLOCK_CONTENTS
);
117 if ( range
.checkBoundaryOfElement( ancestor
, CKEDITOR
.START
) && range
.checkBoundaryOfElement( ancestor
, CKEDITOR
.END
) ) {
119 while ( ancestor
&& ancestor
.type
== CKEDITOR
.NODE_ELEMENT
&& ( parent
= ancestor
.getParent() ) && parent
.getChildCount() == 1 && !( ancestor
.getName() in elements
) )
122 return ancestor
.type
== CKEDITOR
.NODE_ELEMENT
&& ( ancestor
.getName() in elements
) && ancestor
;
126 function bidiCommand( dir
) {
128 // It applies to a "block-like" context.
131 'h1 h2 h3 h4 h5 h6 table ul ol blockquote div tr p div li td': {
132 propertiesOnly: true,
136 requiredContent: 'p[dir]',
137 refresh: function( editor
, path
) {
138 setToolbarStates( editor
, path
);
139 handleMixedDirContent( editor
, path
);
141 exec: function( editor
) {
142 var selection
= editor
.getSelection(),
143 enterMode
= editor
.config
.enterMode
,
144 ranges
= selection
.getRanges();
146 if ( ranges
&& ranges
.length
) {
149 // Creates bookmarks for selection, as we may split some blocks.
150 var bookmarks
= selection
.createBookmarks();
152 var rangeIterator
= ranges
.createIterator(),
156 while ( ( range
= rangeIterator
.getNextRange( 1 ) ) ) {
157 // Apply do directly selected elements from guardElements.
158 var selectedElement
= range
.getEnclosedNode();
160 // If this is not our element of interest, apply to fully selected elements from guardElements.
161 if ( !selectedElement
|| selectedElement
&& !( selectedElement
.type
== CKEDITOR
.NODE_ELEMENT
&& selectedElement
.getName() in directSelectionGuardElements
) )
162 selectedElement
= getFullySelected( range
, guardElements
, enterMode
);
164 selectedElement
&& switchDir( selectedElement
, dir
, editor
, database
);
168 // Walker searching for guardElements.
169 var walker
= new CKEDITOR
.dom
.walker( range
);
171 var start
= bookmarks
[ i
].startNode
,
172 end
= bookmarks
[ i
++ ].endNode
;
174 walker
.evaluator = function( node
) {
175 var enterTagName
= ( enterMode
== CKEDITOR
.ENTER_P
? 'p' : 'div' );
177 function isNodeElement( node
) {
178 return node
? ( node
.type
== CKEDITOR
.NODE_ELEMENT
) : false;
181 function isGuard( node
) {
182 return node
.getName() in guardElements
;
185 return !!( isNodeElement( node
) && isGuard( node
) && !( node
.is( enterTagName
) && isNodeElement( node
.getParent() ) && node
.getParent().is( 'blockquote' ) ) &&
186 // Element must be fully included in the range as well. (#6485).
187 node
.getPosition( start
) & CKEDITOR
.POSITION_FOLLOWING
&&
188 ( ( node
.getPosition( end
) & CKEDITOR
.POSITION_PRECEDING
+ CKEDITOR
.POSITION_CONTAINS
) == CKEDITOR
.POSITION_PRECEDING
) );
191 while ( ( block
= walker
.next() ) )
192 switchDir( block
, dir
, editor
, database
);
194 iterator
= range
.createIterator();
195 iterator
.enlargeBr
= enterMode
!= CKEDITOR
.ENTER_BR
;
197 while ( ( block
= iterator
.getNextParagraph( enterMode
== CKEDITOR
.ENTER_P
? 'p' : 'div' ) ) )
198 switchDir( block
, dir
, editor
, database
);
201 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
203 editor
.forceNextSelectionCheck();
204 // Restore selection position.
205 selection
.selectBookmarks( bookmarks
);
213 CKEDITOR
.plugins
.add( 'bidi', {
214 // jscs:disable maximumLineLength
215 lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
216 // jscs:enable maximumLineLength
217 icons: 'bidiltr,bidirtl', // %REMOVE_LINE_CORE%
218 hidpi: true, // %REMOVE_LINE_CORE%
219 init: function( editor
) {
220 if ( editor
.blockless
)
223 // All buttons use the same code to register. So, to avoid
224 // duplications, let's use this tool function.
225 function addButtonCommand( buttonName
, buttonLabel
, commandName
, commandDef
, order
) {
226 editor
.addCommand( commandName
, new CKEDITOR
.command( editor
, commandDef
) );
228 if ( editor
.ui
.addButton
) {
229 editor
.ui
.addButton( buttonName
, {
231 command: commandName
,
232 toolbar: 'bidi,' + order
237 var lang
= editor
.lang
.bidi
;
239 addButtonCommand( 'BidiLtr', lang
.ltr
, 'bidiltr', bidiCommand( 'ltr' ), 10 );
240 addButtonCommand( 'BidiRtl', lang
.rtl
, 'bidirtl', bidiCommand( 'rtl' ), 20 );
242 editor
.on( 'contentDom', function() {
243 editor
.document
.on( 'dirChanged', function( evt
) {
244 editor
.fire( 'dirChanged', {
246 dir: evt
.data
.getDirection( 1 )
251 // Indicate that the current selection is in different direction than the UI.
252 editor
.on( 'contentDirChanged', function( evt
) {
253 var func
= ( editor
.lang
.dir
!= evt
.data
? 'add' : 'remove' ) + 'Class';
254 var toolbar
= editor
.ui
.space( editor
.config
.toolbarLocation
);
256 toolbar
[ func
]( 'cke_mixed_dir_content' );
261 // If the element direction changed, we need to switch the margins of
262 // the element and all its children, so it will get really reflected
263 // like a mirror. (#5910)
264 function isOffline( el
) {
265 var html
= el
.getDocument().getBody().getParent();
267 if ( el
.equals( html
) )
274 function dirChangeNotifier( org
) {
275 var isAttribute
= org
== elementProto
.setAttribute
,
276 isRemoveAttribute
= org
== elementProto
.removeAttribute
,
277 dirStyleRegexp
= /\bdirection\s*:\s*(.*?)\s*(:?$|;)/;
279 return function( name
, val
) {
280 if ( !this.isReadOnly() ) {
282 if ( ( name
== ( isAttribute
|| isRemoveAttribute
? 'dir' : 'direction' ) || name
== 'style' && ( isRemoveAttribute
|| dirStyleRegexp
.test( val
) ) ) && !isOffline( this ) ) {
283 orgDir
= this.getDirection( 1 );
284 var retval
= org
.apply( this, arguments
);
285 if ( orgDir
!= this.getDirection( 1 ) ) {
286 this.getDocument().fire( 'dirChanged', this );
292 return org
.apply( this, arguments
);
296 var elementProto
= CKEDITOR
.dom
.element
.prototype,
297 methods
= [ 'setStyle', 'removeStyle', 'setAttribute', 'removeAttribute' ];
298 for ( var i
= 0; i
< methods
.length
; i
++ )
299 elementProto
[ methods
[ i
] ] = CKEDITOR
.tools
.override( elementProto
[ methods
[ i
] ], dirChangeNotifier
);
303 * Fired when the language direction of an element is changed.
306 * @member CKEDITOR.editor
307 * @param {CKEDITOR.editor} editor This editor instance.
309 * @param {CKEDITOR.dom.node} data.node The element that is being changed.
310 * @param {String} data.dir The new direction.
314 * Fired when the language direction in the specific cursor position is changed
316 * @event contentDirChanged
317 * @member CKEDITOR.editor
318 * @param {CKEDITOR.editor} editor This editor instance.
319 * @param {String} data The direction in the current position.