diff options
Diffstat (limited to 'sources/plugins/bidi/plugin.js')
-rw-r--r-- | sources/plugins/bidi/plugin.js | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/sources/plugins/bidi/plugin.js b/sources/plugins/bidi/plugin.js new file mode 100644 index 00000000..bfe3ffcb --- /dev/null +++ b/sources/plugins/bidi/plugin.js | |||
@@ -0,0 +1,320 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
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 } ); | ||
13 | |||
14 | function setToolbarStates( editor, path ) { | ||
15 | var useComputedState = editor.config.useComputedState, | ||
16 | selectedElement; | ||
17 | |||
18 | useComputedState = useComputedState === undefined || useComputedState; | ||
19 | |||
20 | // We can use computedState provided by the browser or traverse parents manually. | ||
21 | if ( !useComputedState ) | ||
22 | selectedElement = getElementForDirection( path.lastElement, editor.editable() ); | ||
23 | |||
24 | selectedElement = selectedElement || path.block || path.blockLimit; | ||
25 | |||
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 ); | ||
30 | } | ||
31 | |||
32 | if ( !selectedElement ) | ||
33 | return; | ||
34 | |||
35 | var selectionDir = useComputedState ? selectedElement.getComputedStyle( 'direction' ) : selectedElement.getStyle( 'direction' ) || selectedElement.getAttribute( 'dir' ); | ||
36 | |||
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 ); | ||
39 | } | ||
40 | |||
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 ); | ||
47 | } | ||
48 | } | ||
49 | |||
50 | // Returns element with possibility of applying the direction. | ||
51 | // @param node | ||
52 | function getElementForDirection( node, root ) { | ||
53 | while ( node && !( node.getName() in allGuardElements || node.equals( root ) ) ) { | ||
54 | var parent = node.getParent(); | ||
55 | if ( !parent ) | ||
56 | break; | ||
57 | |||
58 | node = parent; | ||
59 | } | ||
60 | |||
61 | return node; | ||
62 | } | ||
63 | |||
64 | function switchDir( element, dir, editor, database ) { | ||
65 | if ( element.isReadOnly() || element.equals( editor.editable() ) ) | ||
66 | return; | ||
67 | |||
68 | // Mark this element as processed by switchDir. | ||
69 | CKEDITOR.dom.element.setMarker( database, element, 'bidi_processed', 1 ); | ||
70 | |||
71 | // Check whether one of the ancestors has already been styled. | ||
72 | var parent = element, | ||
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' ); | ||
79 | return; | ||
80 | } | ||
81 | } | ||
82 | |||
83 | var useComputedState = ( 'useComputedState' in editor.config ) ? editor.config.useComputedState : 1; | ||
84 | |||
85 | var elementDir = useComputedState ? element.getComputedStyle( 'direction' ) : element.getStyle( 'direction' ) || element.hasAttribute( 'dir' ); | ||
86 | |||
87 | // Stop if direction is same as present. | ||
88 | if ( elementDir == dir ) | ||
89 | return; | ||
90 | |||
91 | // Clear direction on this element. | ||
92 | element.removeStyle( 'direction' ); | ||
93 | |||
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 ); | ||
100 | } | ||
101 | } else { | ||
102 | // Set new direction for this element. | ||
103 | element.setAttribute( 'dir', dir ); | ||
104 | } | ||
105 | |||
106 | editor.forceNextSelectionCheck(); | ||
107 | |||
108 | return; | ||
109 | } | ||
110 | |||
111 | function getFullySelected( range, elements, enterMode ) { | ||
112 | var ancestor = range.getCommonAncestor( false, true ); | ||
113 | |||
114 | range = range.clone(); | ||
115 | range.enlarge( enterMode == CKEDITOR.ENTER_BR ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS ); | ||
116 | |||
117 | if ( range.checkBoundaryOfElement( ancestor, CKEDITOR.START ) && range.checkBoundaryOfElement( ancestor, CKEDITOR.END ) ) { | ||
118 | var parent; | ||
119 | while ( ancestor && ancestor.type == CKEDITOR.NODE_ELEMENT && ( parent = ancestor.getParent() ) && parent.getChildCount() == 1 && !( ancestor.getName() in elements ) ) | ||
120 | ancestor = parent; | ||
121 | |||
122 | return ancestor.type == CKEDITOR.NODE_ELEMENT && ( ancestor.getName() in elements ) && ancestor; | ||
123 | } | ||
124 | } | ||
125 | |||
126 | function bidiCommand( dir ) { | ||
127 | return { | ||
128 | // It applies to a "block-like" context. | ||
129 | context: 'p', | ||
130 | allowedContent: { | ||
131 | 'h1 h2 h3 h4 h5 h6 table ul ol blockquote div tr p div li td': { | ||
132 | propertiesOnly: true, | ||
133 | attributes: 'dir' | ||
134 | } | ||
135 | }, | ||
136 | requiredContent: 'p[dir]', | ||
137 | refresh: function( editor, path ) { | ||
138 | setToolbarStates( editor, path ); | ||
139 | handleMixedDirContent( editor, path ); | ||
140 | }, | ||
141 | exec: function( editor ) { | ||
142 | var selection = editor.getSelection(), | ||
143 | enterMode = editor.config.enterMode, | ||
144 | ranges = selection.getRanges(); | ||
145 | |||
146 | if ( ranges && ranges.length ) { | ||
147 | var database = {}; | ||
148 | |||
149 | // Creates bookmarks for selection, as we may split some blocks. | ||
150 | var bookmarks = selection.createBookmarks(); | ||
151 | |||
152 | var rangeIterator = ranges.createIterator(), | ||
153 | range, | ||
154 | i = 0; | ||
155 | |||
156 | while ( ( range = rangeIterator.getNextRange( 1 ) ) ) { | ||
157 | // Apply do directly selected elements from guardElements. | ||
158 | var selectedElement = range.getEnclosedNode(); | ||
159 | |||
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 ); | ||
163 | |||
164 | selectedElement && switchDir( selectedElement, dir, editor, database ); | ||
165 | |||
166 | var iterator, block; | ||
167 | |||
168 | // Walker searching for guardElements. | ||
169 | var walker = new CKEDITOR.dom.walker( range ); | ||
170 | |||
171 | var start = bookmarks[ i ].startNode, | ||
172 | end = bookmarks[ i++ ].endNode; | ||
173 | |||
174 | walker.evaluator = function( node ) { | ||
175 | var enterTagName = ( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); | ||
176 | |||
177 | function isNodeElement( node ) { | ||
178 | return node ? ( node.type == CKEDITOR.NODE_ELEMENT ) : false; | ||
179 | } | ||
180 | |||
181 | function isGuard( node ) { | ||
182 | return node.getName() in guardElements; | ||
183 | } | ||
184 | |||
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 ) ); | ||
189 | }; | ||
190 | |||
191 | while ( ( block = walker.next() ) ) | ||
192 | switchDir( block, dir, editor, database ); | ||
193 | |||
194 | iterator = range.createIterator(); | ||
195 | iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR; | ||
196 | |||
197 | while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) ) | ||
198 | switchDir( block, dir, editor, database ); | ||
199 | } | ||
200 | |||
201 | CKEDITOR.dom.element.clearAllMarkers( database ); | ||
202 | |||
203 | editor.forceNextSelectionCheck(); | ||
204 | // Restore selection position. | ||
205 | selection.selectBookmarks( bookmarks ); | ||
206 | |||
207 | editor.focus(); | ||
208 | } | ||
209 | } | ||
210 | }; | ||
211 | } | ||
212 | |||
213 | CKEDITOR.plugins.add( 'bidi', { | ||
214 | // jscs:disable maximumLineLength | ||
215 | lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,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 ) | ||
221 | return; | ||
222 | |||
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 ) ); | ||
227 | |||
228 | if ( editor.ui.addButton ) { | ||
229 | editor.ui.addButton( buttonName, { | ||
230 | label: buttonLabel, | ||
231 | command: commandName, | ||
232 | toolbar: 'bidi,' + order | ||
233 | } ); | ||
234 | } | ||
235 | } | ||
236 | |||
237 | var lang = editor.lang.bidi; | ||
238 | |||
239 | addButtonCommand( 'BidiLtr', lang.ltr, 'bidiltr', bidiCommand( 'ltr' ), 10 ); | ||
240 | addButtonCommand( 'BidiRtl', lang.rtl, 'bidirtl', bidiCommand( 'rtl' ), 20 ); | ||
241 | |||
242 | editor.on( 'contentDom', function() { | ||
243 | editor.document.on( 'dirChanged', function( evt ) { | ||
244 | editor.fire( 'dirChanged', { | ||
245 | node: evt.data, | ||
246 | dir: evt.data.getDirection( 1 ) | ||
247 | } ); | ||
248 | } ); | ||
249 | } ); | ||
250 | |||
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 ); | ||
255 | if ( toolbar ) | ||
256 | toolbar[ func ]( 'cke_mixed_dir_content' ); | ||
257 | } ); | ||
258 | } | ||
259 | } ); | ||
260 | |||
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(); | ||
266 | while ( el ) { | ||
267 | if ( el.equals( html ) ) | ||
268 | return false; | ||
269 | el = el.getParent(); | ||
270 | } | ||
271 | return true; | ||
272 | } | ||
273 | |||
274 | function dirChangeNotifier( org ) { | ||
275 | var isAttribute = org == elementProto.setAttribute, | ||
276 | isRemoveAttribute = org == elementProto.removeAttribute, | ||
277 | dirStyleRegexp = /\bdirection\s*:\s*(.*?)\s*(:?$|;)/; | ||
278 | |||
279 | return function( name, val ) { | ||
280 | if ( !this.isReadOnly() ) { | ||
281 | var orgDir; | ||
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 ); | ||
287 | return retval; | ||
288 | } | ||
289 | } | ||
290 | } | ||
291 | |||
292 | return org.apply( this, arguments ); | ||
293 | }; | ||
294 | } | ||
295 | |||
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 ); | ||
300 | } )(); | ||
301 | |||
302 | /** | ||
303 | * Fired when the language direction of an element is changed. | ||
304 | * | ||
305 | * @event dirChanged | ||
306 | * @member CKEDITOR.editor | ||
307 | * @param {CKEDITOR.editor} editor This editor instance. | ||
308 | * @param data | ||
309 | * @param {CKEDITOR.dom.node} data.node The element that is being changed. | ||
310 | * @param {String} data.dir The new direction. | ||
311 | */ | ||
312 | |||
313 | /** | ||
314 | * Fired when the language direction in the specific cursor position is changed | ||
315 | * | ||
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. | ||
320 | */ | ||