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