plugin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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. ( function() {
  6. var guardElements = { table: 1, ul: 1, ol: 1, blockquote: 1, div: 1 },
  7. directSelectionGuardElements = {},
  8. // All guard elements which can have a direction applied on them.
  9. allGuardElements = {};
  10. CKEDITOR.tools.extend( directSelectionGuardElements, guardElements, { tr: 1, p: 1, div: 1, li: 1 } );
  11. CKEDITOR.tools.extend( allGuardElements, directSelectionGuardElements, { td: 1 } );
  12. function setToolbarStates( editor, path ) {
  13. var useComputedState = editor.config.useComputedState,
  14. selectedElement;
  15. useComputedState = useComputedState === undefined || useComputedState;
  16. // We can use computedState provided by the browser or traverse parents manually.
  17. if ( !useComputedState )
  18. selectedElement = getElementForDirection( path.lastElement, editor.editable() );
  19. selectedElement = selectedElement || path.block || path.blockLimit;
  20. // If we're having BODY here, user probably done CTRL+A, let's try to get the enclosed node, if any.
  21. if ( selectedElement.equals( editor.editable() ) ) {
  22. var enclosedNode = editor.getSelection().getRanges()[ 0 ].getEnclosedNode();
  23. enclosedNode && enclosedNode.type == CKEDITOR.NODE_ELEMENT && ( selectedElement = enclosedNode );
  24. }
  25. if ( !selectedElement )
  26. return;
  27. var selectionDir = useComputedState ? selectedElement.getComputedStyle( 'direction' ) : selectedElement.getStyle( 'direction' ) || selectedElement.getAttribute( 'dir' );
  28. editor.getCommand( 'bidirtl' ).setState( selectionDir == 'rtl' ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
  29. editor.getCommand( 'bidiltr' ).setState( selectionDir == 'ltr' ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
  30. }
  31. function handleMixedDirContent( editor, path ) {
  32. var directionNode = path.block || path.blockLimit || editor.editable();
  33. var pathDir = directionNode.getDirection( 1 );
  34. if ( pathDir != ( editor._.selDir || editor.lang.dir ) ) {
  35. editor._.selDir = pathDir;
  36. editor.fire( 'contentDirChanged', pathDir );
  37. }
  38. }
  39. // Returns element with possibility of applying the direction.
  40. // @param node
  41. function getElementForDirection( node, root ) {
  42. while ( node && !( node.getName() in allGuardElements || node.equals( root ) ) ) {
  43. var parent = node.getParent();
  44. if ( !parent )
  45. break;
  46. node = parent;
  47. }
  48. return node;
  49. }
  50. function switchDir( element, dir, editor, database ) {
  51. if ( element.isReadOnly() || element.equals( editor.editable() ) )
  52. return;
  53. // Mark this element as processed by switchDir.
  54. CKEDITOR.dom.element.setMarker( database, element, 'bidi_processed', 1 );
  55. // Check whether one of the ancestors has already been styled.
  56. var parent = element,
  57. editable = editor.editable();
  58. while ( ( parent = parent.getParent() ) && !parent.equals( editable ) ) {
  59. if ( parent.getCustomData( 'bidi_processed' ) ) {
  60. // Ancestor style must dominate.
  61. element.removeStyle( 'direction' );
  62. element.removeAttribute( 'dir' );
  63. return;
  64. }
  65. }
  66. var useComputedState = ( 'useComputedState' in editor.config ) ? editor.config.useComputedState : 1;
  67. var elementDir = useComputedState ? element.getComputedStyle( 'direction' ) : element.getStyle( 'direction' ) || element.hasAttribute( 'dir' );
  68. // Stop if direction is same as present.
  69. if ( elementDir == dir )
  70. return;
  71. // Clear direction on this element.
  72. element.removeStyle( 'direction' );
  73. // Do the second check when computed state is ON, to check
  74. // if we need to apply explicit direction on this element.
  75. if ( useComputedState ) {
  76. element.removeAttribute( 'dir' );
  77. if ( dir != element.getComputedStyle( 'direction' ) ) {
  78. element.setAttribute( 'dir', dir );
  79. }
  80. } else {
  81. // Set new direction for this element.
  82. element.setAttribute( 'dir', dir );
  83. }
  84. editor.forceNextSelectionCheck();
  85. return;
  86. }
  87. function getFullySelected( range, elements, enterMode ) {
  88. var ancestor = range.getCommonAncestor( false, true );
  89. range = range.clone();
  90. range.enlarge( enterMode == CKEDITOR.ENTER_BR ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
  91. if ( range.checkBoundaryOfElement( ancestor, CKEDITOR.START ) && range.checkBoundaryOfElement( ancestor, CKEDITOR.END ) ) {
  92. var parent;
  93. while ( ancestor && ancestor.type == CKEDITOR.NODE_ELEMENT && ( parent = ancestor.getParent() ) && parent.getChildCount() == 1 && !( ancestor.getName() in elements ) )
  94. ancestor = parent;
  95. return ancestor.type == CKEDITOR.NODE_ELEMENT && ( ancestor.getName() in elements ) && ancestor;
  96. }
  97. }
  98. function bidiCommand( dir ) {
  99. return {
  100. // It applies to a "block-like" context.
  101. context: 'p',
  102. allowedContent: {
  103. 'h1 h2 h3 h4 h5 h6 table ul ol blockquote div tr p div li td': {
  104. propertiesOnly: true,
  105. attributes: 'dir'
  106. }
  107. },
  108. requiredContent: 'p[dir]',
  109. refresh: function( editor, path ) {
  110. setToolbarStates( editor, path );
  111. handleMixedDirContent( editor, path );
  112. },
  113. exec: function( editor ) {
  114. var selection = editor.getSelection(),
  115. enterMode = editor.config.enterMode,
  116. ranges = selection.getRanges();
  117. if ( ranges && ranges.length ) {
  118. var database = {};
  119. // Creates bookmarks for selection, as we may split some blocks.
  120. var bookmarks = selection.createBookmarks();
  121. var rangeIterator = ranges.createIterator(),
  122. range,
  123. i = 0;
  124. while ( ( range = rangeIterator.getNextRange( 1 ) ) ) {
  125. // Apply do directly selected elements from guardElements.
  126. var selectedElement = range.getEnclosedNode();
  127. // If this is not our element of interest, apply to fully selected elements from guardElements.
  128. if ( !selectedElement || selectedElement && !( selectedElement.type == CKEDITOR.NODE_ELEMENT && selectedElement.getName() in directSelectionGuardElements ) )
  129. selectedElement = getFullySelected( range, guardElements, enterMode );
  130. selectedElement && switchDir( selectedElement, dir, editor, database );
  131. var iterator, block;
  132. // Walker searching for guardElements.
  133. var walker = new CKEDITOR.dom.walker( range );
  134. var start = bookmarks[ i ].startNode,
  135. end = bookmarks[ i++ ].endNode;
  136. walker.evaluator = function( node ) {
  137. var enterTagName = ( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
  138. function isNodeElement( node ) {
  139. return node ? ( node.type == CKEDITOR.NODE_ELEMENT ) : false;
  140. }
  141. function isGuard( node ) {
  142. return node.getName() in guardElements;
  143. }
  144. return !!( isNodeElement( node ) && isGuard( node ) && !( node.is( enterTagName ) && isNodeElement( node.getParent() ) && node.getParent().is( 'blockquote' ) ) &&
  145. // Element must be fully included in the range as well. (#6485).
  146. node.getPosition( start ) & CKEDITOR.POSITION_FOLLOWING &&
  147. ( ( node.getPosition( end ) & CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_CONTAINS ) == CKEDITOR.POSITION_PRECEDING ) );
  148. };
  149. while ( ( block = walker.next() ) )
  150. switchDir( block, dir, editor, database );
  151. iterator = range.createIterator();
  152. iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
  153. while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) )
  154. switchDir( block, dir, editor, database );
  155. }
  156. CKEDITOR.dom.element.clearAllMarkers( database );
  157. editor.forceNextSelectionCheck();
  158. // Restore selection position.
  159. selection.selectBookmarks( bookmarks );
  160. editor.focus();
  161. }
  162. }
  163. };
  164. }
  165. CKEDITOR.plugins.add( 'bidi', {
  166. // jscs:disable maximumLineLength
  167. 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%
  168. // jscs:enable maximumLineLength
  169. icons: 'bidiltr,bidirtl', // %REMOVE_LINE_CORE%
  170. hidpi: true, // %REMOVE_LINE_CORE%
  171. init: function( editor ) {
  172. if ( editor.blockless )
  173. return;
  174. // All buttons use the same code to register. So, to avoid
  175. // duplications, let's use this tool function.
  176. function addButtonCommand( buttonName, buttonLabel, commandName, commandDef, order ) {
  177. editor.addCommand( commandName, new CKEDITOR.command( editor, commandDef ) );
  178. if ( editor.ui.addButton ) {
  179. editor.ui.addButton( buttonName, {
  180. label: buttonLabel,
  181. command: commandName,
  182. toolbar: 'bidi,' + order
  183. } );
  184. }
  185. }
  186. var lang = editor.lang.bidi;
  187. addButtonCommand( 'BidiLtr', lang.ltr, 'bidiltr', bidiCommand( 'ltr' ), 10 );
  188. addButtonCommand( 'BidiRtl', lang.rtl, 'bidirtl', bidiCommand( 'rtl' ), 20 );
  189. editor.on( 'contentDom', function() {
  190. editor.document.on( 'dirChanged', function( evt ) {
  191. editor.fire( 'dirChanged', {
  192. node: evt.data,
  193. dir: evt.data.getDirection( 1 )
  194. } );
  195. } );
  196. } );
  197. // Indicate that the current selection is in different direction than the UI.
  198. editor.on( 'contentDirChanged', function( evt ) {
  199. var func = ( editor.lang.dir != evt.data ? 'add' : 'remove' ) + 'Class';
  200. var toolbar = editor.ui.space( editor.config.toolbarLocation );
  201. if ( toolbar )
  202. toolbar[ func ]( 'cke_mixed_dir_content' );
  203. } );
  204. }
  205. } );
  206. // If the element direction changed, we need to switch the margins of
  207. // the element and all its children, so it will get really reflected
  208. // like a mirror. (#5910)
  209. function isOffline( el ) {
  210. var html = el.getDocument().getBody().getParent();
  211. while ( el ) {
  212. if ( el.equals( html ) )
  213. return false;
  214. el = el.getParent();
  215. }
  216. return true;
  217. }
  218. function dirChangeNotifier( org ) {
  219. var isAttribute = org == elementProto.setAttribute,
  220. isRemoveAttribute = org == elementProto.removeAttribute,
  221. dirStyleRegexp = /\bdirection\s*:\s*(.*?)\s*(:?$|;)/;
  222. return function( name, val ) {
  223. if ( !this.isReadOnly() ) {
  224. var orgDir;
  225. if ( ( name == ( isAttribute || isRemoveAttribute ? 'dir' : 'direction' ) || name == 'style' && ( isRemoveAttribute || dirStyleRegexp.test( val ) ) ) && !isOffline( this ) ) {
  226. orgDir = this.getDirection( 1 );
  227. var retval = org.apply( this, arguments );
  228. if ( orgDir != this.getDirection( 1 ) ) {
  229. this.getDocument().fire( 'dirChanged', this );
  230. return retval;
  231. }
  232. }
  233. }
  234. return org.apply( this, arguments );
  235. };
  236. }
  237. var elementProto = CKEDITOR.dom.element.prototype,
  238. methods = [ 'setStyle', 'removeStyle', 'setAttribute', 'removeAttribute' ];
  239. for ( var i = 0; i < methods.length; i++ )
  240. elementProto[ methods[ i ] ] = CKEDITOR.tools.override( elementProto[ methods[ i ] ], dirChangeNotifier );
  241. } )();
  242. /**
  243. * Fired when the language direction of an element is changed.
  244. *
  245. * @event dirChanged
  246. * @member CKEDITOR.editor
  247. * @param {CKEDITOR.editor} editor This editor instance.
  248. * @param data
  249. * @param {CKEDITOR.dom.node} data.node The element that is being changed.
  250. * @param {String} data.dir The new direction.
  251. */
  252. /**
  253. * Fired when the language direction in the specific cursor position is changed
  254. *
  255. * @event contentDirChanged
  256. * @member CKEDITOR.editor
  257. * @param {CKEDITOR.editor} editor This editor instance.
  258. * @param {String} data The direction in the current position.
  259. */