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 Increse and decrease indent commands. 8 */ 9 10 (function() 11 { 12 var listNodeNames = { ol : 1, ul : 1 }; 13 14 function setState( editor, state ) 15 { 16 editor.getCommand( this.name ).setState( state ); 17 } 18 19 function onSelectionChange( evt ) 20 { 21 var elements = evt.data.path.elements, 22 listNode, listItem, 23 editor = evt.editor; 24 25 for ( var i = 0 ; i < elements.length ; i++ ) 26 { 27 if ( elements[i].getName() == 'li' ) 28 { 29 listItem = elements[i]; 30 continue; 31 } 32 if ( listNodeNames[ elements[i].getName() ] ) 33 { 34 listNode = elements[i]; 35 break; 36 } 37 } 38 39 if ( listNode ) 40 { 41 if ( this.name == 'outdent' ) 42 return setState.call( this, editor, CKEDITOR.TRISTATE_OFF ); 43 else 44 { 45 while ( listItem && ( listItem = listItem.getPrevious() ) ) 46 { 47 if ( listItem.getName && listItem.getName() == 'li' ) 48 return setState.call( this, editor, CKEDITOR.TRISTATE_OFF ); 49 } 50 return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED ); 51 } 52 } 53 54 if ( !this.useIndentClasses && this.name == 'indent' ) 55 return setState.call( this, editor, CKEDITOR.TRISTATE_OFF ); 56 57 var path = evt.data.path, 58 firstBlock = path.block || path.blockLimit; 59 if ( !firstBlock ) 60 return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED ); 61 62 if ( this.useIndentClasses ) 63 { 64 var indentClass = firstBlock.$.className.match( this.classNameRegex ), 65 indentStep = 0; 66 if ( indentClass != null ) 67 { 68 indentClass = indentClass[1]; 69 indentStep = this.indentClassMap[ indentClass ]; 70 } 71 if ( ( this.name == 'outdent' && indentStep == 0 ) || 72 ( this.name == 'indent' && indentStep == editor.config.indentClass.length ) ) 73 return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED ); 74 return setState.call( this, editor, CKEDITOR.TRISTATE_OFF ); 75 } 76 else 77 { 78 var indent = parseInt( firstBlock.getComputedStyle( this.indentCssProperty ), 10 ); 79 if ( isNaN( indent ) ) 80 indent = 0; 81 if ( indent <= 0 ) 82 return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED ); 83 return setState.call( this, editor, CKEDITOR.TRISTATE_OFF ); 84 } 85 } 86 87 function indentList( editor, range, listNode ) 88 { 89 // Our starting and ending points of the range might be inside some blocks under a list item... 90 // So before playing with the iterator, we need to expand the block to include the list items. 91 var startContainer = range.startContainer, 92 endContainer = range.endContainer; 93 while ( startContainer && !startContainer.getParent().equals( listNode ) ) 94 startContainer = startContainer.getParent(); 95 while ( endContainer && !endContainer.getParent().equals( listNode ) ) 96 endContainer = endContainer.getParent(); 97 98 if ( !startContainer || !endContainer ) 99 return; 100 101 // Now we can iterate over the individual items on the same tree depth. 102 var block = startContainer, 103 itemsToMove = [], 104 stopFlag = false; 105 while ( stopFlag == false ) 106 { 107 if ( block.equals( endContainer ) ) 108 stopFlag = true; 109 itemsToMove.push( block ); 110 block = block.getNext(); 111 } 112 if ( itemsToMove.length < 1 ) 113 return; 114 115 // Do indent or outdent operations on the array model of the list, not the 116 // list's DOM tree itself. The array model demands that it knows as much as 117 // possible about the surrounding lists, we need to feed it the further 118 // ancestor node that is still a list. 119 var listParents = listNode.getParents(); 120 for ( var i = 0 ; i < listParents.length ; i++ ) 121 { 122 if ( listParents[i].getName && listNodeNames[ listParents[i].getName() ] ) 123 { 124 listNode = listParents[i]; 125 break; 126 } 127 } 128 var indentOffset = this.name == 'indent' ? 1 : -1, 129 startItem = itemsToMove[0], 130 lastItem = itemsToMove[ itemsToMove.length - 1 ], 131 database = {}; 132 133 // Convert the list DOM tree into a one dimensional array. 134 var listArray = CKEDITOR.plugins.list.listToArray( listNode, database ); 135 136 // Apply indenting or outdenting on the array. 137 var baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent; 138 for ( var i = startItem.getCustomData( 'listarray_index' ) ; i <= lastItem.getCustomData( 'listarray_index' ) ; i++ ) 139 listArray[i].indent += indentOffset; 140 for ( var i = lastItem.getCustomData( 'listarray_index' ) + 1 ; 141 i < listArray.length && listArray[i].indent > baseIndent ; i++ ) 142 listArray[i].indent += indentOffset; 143 144 // Convert the array back to a DOM forest (yes we might have a few subtrees now). 145 // And replace the old list with the new forest. 146 var newList = CKEDITOR.plugins.list.arrayToList( listArray, null, null, editor.config.enterMode ); 147 if ( newList ) 148 newList.listNode.replace( listNode ); 149 150 // Clean up the markers. 151 CKEDITOR.dom.element.clearAllMarkers( database ); 152 } 153 154 function indentBlock( editor, range ) 155 { 156 var iterator = range.createIterator(); 157 iterator.enforceRealBlocks = true; 158 159 range.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 160 var commonParent = range.getCommonAncestor(), 161 block; 162 163 while ( ( block = iterator.getNextParagraph() ) ) 164 { 165 // We don't want to indent subtrees recursively, so only perform the indent 166 // operation if the block itself is the nearestParent, or the block's parent 167 // is the commonParent. 168 if ( !( block.equals( commonParent ) || block.getParent().equals( commonParent ) ) ) 169 continue; 170 171 if ( this.useIndentClasses ) 172 { 173 // Transform current class name to indent step index. 174 var indentClass = block.$.className.match( this.classNameRegex ), 175 indentStep = 0; 176 if ( indentClass != null ) 177 { 178 indentClass = indentClass[1]; 179 indentStep = this.indentClassMap[ indentClass ]; 180 } 181 182 // Operate on indent step index, transform indent step index back to class 183 // name. 184 if ( this.name == 'outdent' ) 185 indentStep--; 186 else 187 indentStep++; 188 indentStep = Math.min( indentStep, editor.config.indentClasses.length ); 189 indentStep = Math.max( indentStep, 0 ); 190 var className = CKEDITOR.tools.ltrim( block.$.className.replace( this.classNameRegex, '' ) ); 191 if ( indentStep < 1 ) 192 block.$.className = className; 193 else 194 block.addClass( editor.config.indentClasses[ indentStep - 1 ] ); 195 } 196 else 197 { 198 var currentOffset = parseInt( block.getComputedStyle( this.indentCssProperty ), 10 ); 199 if ( isNaN( currentOffset ) ) 200 currentOffset = 0; 201 currentOffset += ( this.name == 'indent' ? 1 : -1 ) * editor.config.indentOffset; 202 currentOffset = Math.max( currentOffset, 0 ); 203 currentOffset = Math.ceil( currentOffset / editor.config.indentOffset ) * editor.config.indentOffset; 204 block.setStyle( this.indentCssProperty, currentOffset ? currentOffset + editor.config.indentUnit : '' ); 205 if ( block.getAttribute( 'style' ) == '' ) 206 block.removeAttribute( 'style' ); 207 } 208 } 209 } 210 211 function indentCommand( editor, name ) 212 { 213 this.name = name; 214 this.useIndentClasses = editor.config.indentClasses && editor.config.indentClasses.length > 0; 215 if ( this.useIndentClasses ) 216 { 217 this.classNameRegex = new RegExp( '(?:^|\\s+)(' + editor.config.indentClasses.join( '|' ) + ')(?=$|\\s)' ); 218 this.indentClassMap = {}; 219 for ( var i = 0 ; i < editor.config.indentClasses.length ; i++ ) 220 this.indentClassMap[ editor.config.indentClasses[i] ] = i + 1; 221 } 222 else 223 this.indentCssProperty = editor.config.contentsLangDirection == 'ltr' ? 'margin-left' : 'margin-right'; 224 } 225 226 indentCommand.prototype = { 227 exec : function( editor ) 228 { 229 var selection = editor.getSelection(), 230 range = selection && selection.getRanges()[0]; 231 232 if ( !selection || !range ) 233 return; 234 235 var bookmarks = selection.createBookmarks( true ), 236 boundaryNodes = range.getBoundaryNodes(), 237 nearestListBlock = boundaryNodes.startNode.getCommonAncestor( boundaryNodes.endNode ); 238 239 while ( nearestListBlock ) 240 { 241 if ( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ nearestListBlock.getName() ] ) 242 break; 243 nearestListBlock = nearestListBlock.getParent(); 244 } 245 246 if ( nearestListBlock ) 247 indentList.call( this, editor, range, nearestListBlock ); 248 else 249 indentBlock.call( this, editor, range ); 250 251 editor.focus(); 252 editor.forceNextSelectionCheck(); 253 selection.selectBookmarks( bookmarks ); 254 } 255 }; 256 257 CKEDITOR.plugins.add( 'indent', 258 { 259 init : function( editor ) 260 { 261 // Register commands. 262 var indent = new indentCommand( editor, 'indent' ), 263 outdent = new indentCommand( editor, 'outdent' ); 264 editor.addCommand( 'indent', indent ); 265 editor.addCommand( 'outdent', outdent ); 266 267 // Register the toolbar buttons. 268 editor.ui.addButton( 'Indent', 269 { 270 label : editor.lang.indent, 271 command : 'indent' 272 }); 273 editor.ui.addButton( 'Outdent', 274 { 275 label : editor.lang.outdent, 276 command : 'outdent' 277 }); 278 279 // Register the state changing handlers. 280 editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, indent ) ); 281 editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, outdent ) ); 282 }, 283 284 requires : [ 'domiterator', 'list' ] 285 } ); 286 })(); 287 288 CKEDITOR.tools.extend( CKEDITOR.config, 289 { 290 indentOffset : 40, 291 indentUnit : 'px', 292 indentClasses : null 293 }); 294