| 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 | }); |