1/*
2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben
4 *
5 * == BEGIN LICENSE ==
6 *
7 * Licensed under the terms of any of the following licenses at your
8 * choice:
9 *
10 *  - GNU General Public License Version 2 or later (the "GPL")
11 *    http://www.gnu.org/licenses/gpl.html
12 *
13 *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
14 *    http://www.gnu.org/licenses/lgpl.html
15 *
16 *  - Mozilla Public License Version 1.1 or later (the "MPL")
17 *    http://www.mozilla.org/MPL/MPL-1.1.html
18 *
19 * == END LICENSE ==
20 *
21 * Controls the [Enter] keystroke behavior in a document.
22 */
23
24/*
25 *	Constructor.
26 *		@targetDocument : the target document.
27 *		@enterMode : the behavior for the <Enter> keystroke.
28 *			May be "p", "div", "br". Default is "p".
29 *		@shiftEnterMode : the behavior for the <Shift>+<Enter> keystroke.
30 *			May be "p", "div", "br". Defaults to "br".
31 */
32var FCKEnterKey = function( targetWindow, enterMode, shiftEnterMode, tabSpaces )
33{
34	this.Window			= targetWindow ;
35	this.EnterMode		= enterMode || 'p' ;
36	this.ShiftEnterMode	= shiftEnterMode || 'br' ;
37
38	// Setup the Keystroke Handler.
39	var oKeystrokeHandler = new FCKKeystrokeHandler( false ) ;
40	oKeystrokeHandler._EnterKey = this ;
41	oKeystrokeHandler.OnKeystroke = FCKEnterKey_OnKeystroke ;
42
43	oKeystrokeHandler.SetKeystrokes( [
44		[ 13		, 'Enter' ],
45		[ SHIFT + 13, 'ShiftEnter' ],
46		[ 9 		, 'Tab' ],
47		[ 8			, 'Backspace' ],
48		[ CTRL + 8		, 'CtrlBackspace' ],
49		[ 46		, 'Delete' ]
50	] ) ;
51
52	if ( tabSpaces > 0 )
53	{
54		this.TabText = '' ;
55		while ( tabSpaces-- > 0 )
56			this.TabText += '\xa0' ;
57	}
58
59	oKeystrokeHandler.AttachToElement( targetWindow.document ) ;
60}
61
62
63function FCKEnterKey_OnKeystroke(  keyCombination, keystrokeValue )
64{
65	var oEnterKey = this._EnterKey ;
66
67	try
68	{
69		switch ( keystrokeValue )
70		{
71			case 'Enter' :
72				return oEnterKey.DoEnter() ;
73				break ;
74			case 'ShiftEnter' :
75				return oEnterKey.DoShiftEnter() ;
76				break ;
77			case 'Backspace' :
78				return oEnterKey.DoBackspace() ;
79				break ;
80			case 'Delete' :
81				return oEnterKey.DoDelete() ;
82				break ;
83			case 'Tab' :
84				return oEnterKey.DoTab() ;
85				break ;
86			case 'CtrlBackspace' :
87				return oEnterKey.DoCtrlBackspace() ;
88				break ;
89		}
90	}
91	catch (e)
92	{
93		// If for any reason we are not able to handle it, go
94		// ahead with the browser default behavior.
95	}
96
97	return false ;
98}
99
100/*
101 * Executes the <Enter> key behavior.
102 */
103FCKEnterKey.prototype.DoEnter = function( mode, hasShift )
104{
105	// Save an undo snapshot before doing anything
106	FCKUndo.SaveUndoStep() ;
107
108	this._HasShift = ( hasShift === true ) ;
109
110	var parentElement = FCKSelection.GetParentElement() ;
111	var parentPath = new FCKElementPath( parentElement ) ;
112	var sMode = mode || this.EnterMode ;
113
114	if ( sMode == 'br' || parentPath.Block && parentPath.Block.tagName.toLowerCase() == 'pre' )
115		return this._ExecuteEnterBr() ;
116	else
117		return this._ExecuteEnterBlock( sMode ) ;
118}
119
120/*
121 * Executes the <Shift>+<Enter> key behavior.
122 */
123FCKEnterKey.prototype.DoShiftEnter = function()
124{
125	return this.DoEnter( this.ShiftEnterMode, true ) ;
126}
127
128/*
129 * Executes the <Backspace> key behavior.
130 */
131FCKEnterKey.prototype.DoBackspace = function()
132{
133	var bCustom = false ;
134
135	// Get the current selection.
136	var oRange = new FCKDomRange( this.Window ) ;
137	oRange.MoveToSelection() ;
138
139	// Kludge for #247
140	if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) )
141	{
142		this._FixIESelectAllBug( oRange ) ;
143		return true ;
144	}
145
146	var isCollapsed = oRange.CheckIsCollapsed() ;
147
148	if ( !isCollapsed )
149	{
150		// Bug #327, Backspace with an img selection would activate the default action in IE.
151		// Let's override that with our logic here.
152		if ( FCKBrowserInfo.IsIE && this.Window.document.selection.type.toLowerCase() == "control" )
153		{
154			var controls = this.Window.document.selection.createRange() ;
155			for ( var i = controls.length - 1 ; i >= 0 ; i-- )
156			{
157				var el = controls.item( i ) ;
158				el.parentNode.removeChild( el ) ;
159			}
160			return true ;
161		}
162
163		return false ;
164	}
165
166	var oStartBlock = oRange.StartBlock ;
167	var oEndBlock = oRange.EndBlock ;
168
169	// The selection boundaries must be in the same "block limit" element
170	if ( oRange.StartBlockLimit == oRange.EndBlockLimit && oStartBlock && oEndBlock )
171	{
172		if ( !isCollapsed )
173		{
174			var bEndOfBlock = oRange.CheckEndOfBlock() ;
175
176			oRange.DeleteContents() ;
177
178			if ( oStartBlock != oEndBlock )
179			{
180				oRange.SetStart(oEndBlock,1) ;
181				oRange.SetEnd(oEndBlock,1) ;
182
183//				if ( bEndOfBlock )
184//					oEndBlock.parentNode.removeChild( oEndBlock ) ;
185			}
186
187			oRange.Select() ;
188
189			bCustom = ( oStartBlock == oEndBlock ) ;
190		}
191
192		if ( oRange.CheckStartOfBlock() )
193		{
194			var oCurrentBlock = oRange.StartBlock ;
195
196			var ePrevious = FCKDomTools.GetPreviousSourceElement( oCurrentBlock, true, [ 'BODY', oRange.StartBlockLimit.nodeName ], ['UL','OL'] ) ;
197
198			bCustom = this._ExecuteBackspace( oRange, ePrevious, oCurrentBlock ) ;
199		}
200		else if ( FCKBrowserInfo.IsGeckoLike )
201		{
202			// Firefox and Opera (#1095) loose the selection when executing
203			// CheckStartOfBlock, so we must reselect.
204			oRange.Select() ;
205		}
206	}
207
208	oRange.Release() ;
209	return bCustom ;
210}
211
212FCKEnterKey.prototype.DoCtrlBackspace = function()
213{
214	FCKUndo.SaveUndoStep() ;
215	var oRange = new FCKDomRange( this.Window ) ;
216	oRange.MoveToSelection() ;
217	if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) )
218	{
219		this._FixIESelectAllBug( oRange ) ;
220		return true ;
221	}
222	return false ;
223}
224
225FCKEnterKey.prototype._ExecuteBackspace = function( range, previous, currentBlock )
226{
227	var bCustom = false ;
228
229	// We could be in a nested LI.
230	if ( !previous && currentBlock && currentBlock.nodeName.IEquals( 'LI' ) && currentBlock.parentNode.parentNode.nodeName.IEquals( 'LI' ) )
231	{
232		this._OutdentWithSelection( currentBlock, range ) ;
233		return true ;
234	}
235
236	if ( previous && previous.nodeName.IEquals( 'LI' ) )
237	{
238		var oNestedList = FCKDomTools.GetLastChild( previous, ['UL','OL'] ) ;
239
240		while ( oNestedList )
241		{
242			previous = FCKDomTools.GetLastChild( oNestedList, 'LI' ) ;
243			oNestedList = FCKDomTools.GetLastChild( previous, ['UL','OL'] ) ;
244		}
245	}
246
247	if ( previous && currentBlock )
248	{
249		// If we are in a LI, and the previous block is not an LI, we must outdent it.
250		if ( currentBlock.nodeName.IEquals( 'LI' ) && !previous.nodeName.IEquals( 'LI' ) )
251		{
252			this._OutdentWithSelection( currentBlock, range ) ;
253			return true ;
254		}
255
256		// Take a reference to the parent for post processing cleanup.
257		var oCurrentParent = currentBlock.parentNode ;
258
259		var sPreviousName = previous.nodeName.toLowerCase() ;
260		if ( FCKListsLib.EmptyElements[ sPreviousName ] != null || sPreviousName == 'table' )
261		{
262			FCKDomTools.RemoveNode( previous ) ;
263			bCustom = true ;
264		}
265		else
266		{
267			// Remove the current block.
268			FCKDomTools.RemoveNode( currentBlock ) ;
269
270			// Remove any empty tag left by the block removal.
271			while ( oCurrentParent.innerHTML.Trim().length == 0 )
272			{
273				var oParent = oCurrentParent.parentNode ;
274				oParent.removeChild( oCurrentParent ) ;
275				oCurrentParent = oParent ;
276			}
277
278			// Cleanup the previous and the current elements.
279			FCKDomTools.LTrimNode( currentBlock ) ;
280			FCKDomTools.RTrimNode( previous ) ;
281
282			// Append a space to the previous.
283			// Maybe it is not always desirable...
284			// previous.appendChild( this.Window.document.createTextNode( ' ' ) ) ;
285
286			// Set the range to the end of the previous element and bookmark it.
287			range.SetStart( previous, 2, true ) ;
288			range.Collapse( true ) ;
289			var oBookmark = range.CreateBookmark() ;
290
291			// Move the contents of the block to the previous element and delete it.
292			// But for some block types (e.g. table), moving the children to the previous block makes no sense.
293			// So a check is needed. (See #1081)
294			if ( ! currentBlock.tagName.IEquals( [ 'TABLE' ] ) )
295				FCKDomTools.MoveChildren( currentBlock, previous ) ;
296
297			// Place the selection at the bookmark.
298			range.MoveToBookmark( oBookmark ) ;
299			range.Select() ;
300
301			bCustom = true ;
302		}
303	}
304
305	return bCustom ;
306}
307
308/*
309 * Executes the <Delete> key behavior.
310 */
311FCKEnterKey.prototype.DoDelete = function()
312{
313	// Save an undo snapshot before doing anything
314	// This is to conform with the behavior seen in MS Word
315	FCKUndo.SaveUndoStep() ;
316
317	// The <Delete> has the same effect as the <Backspace>, so we have the same
318	// results if we just move to the next block and apply the same <Backspace> logic.
319
320	var bCustom = false ;
321
322	// Get the current selection.
323	var oRange = new FCKDomRange( this.Window ) ;
324	oRange.MoveToSelection() ;
325
326	// Kludge for #247
327	if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) )
328	{
329		this._FixIESelectAllBug( oRange ) ;
330		return true ;
331	}
332
333	// There is just one special case for collapsed selections at the end of a block.
334	if ( oRange.CheckIsCollapsed() && oRange.CheckEndOfBlock( FCKBrowserInfo.IsGeckoLike ) )
335	{
336		var oCurrentBlock = oRange.StartBlock ;
337		var eCurrentCell = FCKTools.GetElementAscensor( oCurrentBlock, 'td' );
338
339		var eNext = FCKDomTools.GetNextSourceElement( oCurrentBlock, true, [ oRange.StartBlockLimit.nodeName ],
340				['UL','OL','TR'] ) ;
341
342		// Bug #1323 : if we're in a table cell, and the next node belongs to a different cell, then don't
343		// delete anything.
344		if ( eCurrentCell )
345		{
346			var eNextCell = FCKTools.GetElementAscensor( eNext, 'td' );
347			if ( eNextCell != eCurrentCell )
348				return true ;
349		}
350
351		bCustom = this._ExecuteBackspace( oRange, oCurrentBlock, eNext ) ;
352	}
353
354	oRange.Release() ;
355	return bCustom ;
356}
357
358/*
359 * Executes the <Tab> key behavior.
360 */
361FCKEnterKey.prototype.DoTab = function()
362{
363	var oRange = new FCKDomRange( this.Window );
364	oRange.MoveToSelection() ;
365
366	// If the user pressed <tab> inside a table, we should give him the default behavior ( moving between cells )
367	// instead of giving him more non-breaking spaces. (Bug #973)
368	var node = oRange._Range.startContainer ;
369	while ( node )
370	{
371		if ( node.nodeType == 1 )
372		{
373			var tagName = node.tagName.toLowerCase() ;
374			if ( tagName == "tr" || tagName == "td" || tagName == "th" || tagName == "tbody" || tagName == "table" )
375				return false ;
376			else
377				break ;
378		}
379		node = node.parentNode ;
380	}
381
382	if ( this.TabText )
383	{
384		oRange.DeleteContents() ;
385		oRange.InsertNode( this.Window.document.createTextNode( this.TabText ) ) ;
386		oRange.Collapse( false ) ;
387		oRange.Select() ;
388	}
389	return true ;
390}
391
392FCKEnterKey.prototype._ExecuteEnterBlock = function( blockTag, range )
393{
394	// Get the current selection.
395	var oRange = range || new FCKDomRange( this.Window ) ;
396
397	var oSplitInfo = oRange.SplitBlock() ;
398
399	if ( oSplitInfo )
400	{
401		// Get the current blocks.
402		var ePreviousBlock	= oSplitInfo.PreviousBlock ;
403		var eNextBlock		= oSplitInfo.NextBlock ;
404
405		var bIsStartOfBlock	= oSplitInfo.WasStartOfBlock ;
406		var bIsEndOfBlock	= oSplitInfo.WasEndOfBlock ;
407
408		// If we have both the previous and next blocks, it means that the
409		// boundaries were on separated blocks, or none of them where on the
410		// block limits (start/end).
411		if ( !oSplitInfo.WasStartOfBlock && !oSplitInfo.WasEndOfBlock )
412		{
413			// Move the selection to the end block.
414			if ( eNextBlock )
415				oRange.MoveToElementEditStart( eNextBlock ) ;
416		}
417		else
418		{
419			if ( bIsStartOfBlock && bIsEndOfBlock && ePreviousBlock.tagName.toUpperCase() == 'LI' )
420			{
421				oRange.MoveToElementStart( ePreviousBlock ) ;
422				this._OutdentWithSelection( ePreviousBlock, oRange ) ;
423				oRange.Release() ;
424				return true ;
425			}
426
427			var eNewBlock ;
428
429			if ( ePreviousBlock )
430			{
431				var sPreviousBlockTag = ePreviousBlock.tagName.toUpperCase() ;
432
433				// If is a header tag, or we are in a Shift+Enter (#77),
434				// create a new block element.
435				if ( this._HasShift || (/^H[1-6]$/).test( sPreviousBlockTag ) )
436					eNewBlock = this.Window.document.createElement( blockTag ) ;
437				else
438				{
439					// Otherwise, duplicate the previous block.
440					eNewBlock = FCKDomTools.CloneElement( ePreviousBlock ) ;
441
442					this._RecreateEndingTree( ePreviousBlock, eNewBlock ) ;
443				}
444			}
445			else if ( eNextBlock )
446			{
447				eNewBlock = FCKDomTools.CloneElement( eNextBlock ) ;
448			}
449			else
450				eNewBlock = this.Window.document.createElement( blockTag ) ;
451
452			if ( FCKBrowserInfo.IsGeckoLike )
453				FCKTools.AppendBogusBr( eNewBlock ) ;
454
455			oRange.InsertNode( eNewBlock ) ;
456
457			// This is tricky, but to make the new block visible correctly
458			// we must select it.
459			if ( FCKBrowserInfo.IsIE )
460			{
461				// Move the selection to the new block.
462				oRange.MoveToNodeContents( eNewBlock ) ;
463				oRange.Select() ;
464			}
465
466			oRange.MoveToElementEditStart( bIsStartOfBlock && !bIsEndOfBlock ? eNextBlock : eNewBlock ) ;
467
468			if ( FCKBrowserInfo.IsGeckoLike )
469				eNewBlock.scrollIntoView( false ) ;
470		}
471
472		oRange.Select() ;
473	}
474
475	// Release the resources used by the range.
476	oRange.Release() ;
477
478	return true ;
479}
480
481FCKEnterKey.prototype._ExecuteEnterBr = function( blockTag )
482{
483	// Get the current selection.
484	var oRange = new FCKDomRange( this.Window ) ;
485	oRange.MoveToSelection() ;
486
487	// The selection boundaries must be in the same "block limit" element.
488	if ( oRange.StartBlockLimit == oRange.EndBlockLimit )
489	{
490		oRange.DeleteContents() ;
491
492		// Get the new selection (it is collapsed at this point).
493		oRange.MoveToSelection() ;
494
495		var bIsStartOfBlock	= oRange.CheckStartOfBlock() ;
496		var bIsEndOfBlock	= oRange.CheckEndOfBlock() ;
497
498		var sStartBlockTag = oRange.StartBlock ? oRange.StartBlock.tagName.toUpperCase() : '' ;
499
500		var bHasShift = this._HasShift ;
501
502		if ( !bHasShift && sStartBlockTag == 'LI' )
503			return this._ExecuteEnterBlock( null, oRange ) ;
504
505		// If we are at the end of a header block.
506		if ( !bHasShift && bIsEndOfBlock && (/^H[1-6]$/).test( sStartBlockTag ) )
507		{
508			// Insert a BR after the current paragraph.
509			FCKDomTools.InsertAfterNode( oRange.StartBlock, this.Window.document.createElement( 'br' ) ) ;
510
511			// The space is required by Gecko only to make the cursor blink.
512			if ( FCKBrowserInfo.IsGecko )
513				FCKDomTools.InsertAfterNode( oRange.StartBlock, this.Window.document.createTextNode( '' ) ) ;
514
515			// IE and Gecko have different behaviors regarding the position.
516			oRange.SetStart( oRange.StartBlock.nextSibling, FCKBrowserInfo.IsIE ? 3 : 1 ) ;
517		}
518		else
519		{
520			var eLineBreak = null ;
521			if ( sStartBlockTag.IEquals( 'pre' ) )
522				eLineBreak = this.Window.document.createTextNode( FCKBrowserInfo.IsIE ? '\r' : '\n' ) ;
523			else
524				eLineBreak = this.Window.document.createElement( 'br' ) ;
525
526			oRange.InsertNode( eLineBreak ) ;
527
528			// The space is required by Gecko only to make the cursor blink.
529			if ( FCKBrowserInfo.IsGecko )
530				FCKDomTools.InsertAfterNode( eLineBreak, this.Window.document.createTextNode( '' ) ) ;
531
532			// If we are at the end of a block, we must be sure the bogus node is available in that block.
533			if ( bIsEndOfBlock && FCKBrowserInfo.IsGeckoLike )
534				FCKTools.AppendBogusBr( eLineBreak.parentNode ) ;
535
536			if ( FCKBrowserInfo.IsIE )
537				oRange.SetStart( eLineBreak, 4 ) ;
538			else
539				oRange.SetStart( eLineBreak.nextSibling, 1 ) ;
540
541			if ( ! FCKBrowserInfo.IsIE )
542			{
543				var dummy = null ;
544				if ( FCKBrowserInfo.IsOpera )
545					dummy = this.Window.document.createElement( 'span' ) ;
546				else
547					dummy = this.Window.document.createElement( 'br' ) ;
548				eLineBreak.parentNode.insertBefore( dummy, eLineBreak.nextSibling ) ;
549				dummy.scrollIntoView( false ) ;
550				dummy.parentNode.removeChild( dummy ) ;
551			}
552		}
553
554		// This collapse guarantees the cursor will be blinking.
555		oRange.Collapse( true ) ;
556
557		oRange.Select() ;
558	}
559
560	// Release the resources used by the range.
561	oRange.Release() ;
562
563	return true ;
564}
565
566// Recreate the elements tree at the end of the source block, at the beginning
567// of the target block. Eg.:
568//	If source = <p><u>Some</u> sample <b><i>text</i></b></p> then target = <p><b><i></i></b></p>
569//	If source = <p><u>Some</u> sample text</p> then target = <p></p>
570FCKEnterKey.prototype._RecreateEndingTree = function( source, target )
571{
572	while ( ( source = source.lastChild ) && source.nodeType == 1 && FCKListsLib.InlineChildReqElements[ source.nodeName.toLowerCase() ] != null )
573		target = target.insertBefore( FCKDomTools.CloneElement( source ), target.firstChild ) ;
574}
575
576// Outdents a LI, maintaining the selection defined on a range.
577FCKEnterKey.prototype._OutdentWithSelection = function( li, range )
578{
579	var oBookmark = range.CreateBookmark() ;
580
581	FCKListHandler.OutdentListItem( li ) ;
582
583	range.MoveToBookmark( oBookmark ) ;
584	range.Select() ;
585}
586
587// Is all the contents under a node included by a range?
588FCKEnterKey.prototype._CheckIsAllContentsIncluded = function( range, node )
589{
590	var startOk = false ;
591	var endOk = false ;
592
593	/*
594	FCKDebug.Output( 'sc='+range.StartContainer.nodeName+
595			',so='+range._Range.startOffset+
596			',ec='+range.EndContainer.nodeName+
597			',eo='+range._Range.endOffset ) ;
598	*/
599	if ( range.StartContainer == node || range.StartContainer == node.firstChild )
600		startOk = ( range._Range.startOffset == 0 ) ;
601
602	if ( range.EndContainer == node || range.EndContainer == node.lastChild )
603	{
604		var nodeLength = range.EndContainer.nodeType == 3 ? range.EndContainer.length : range.EndContainer.childNodes.length ;
605		endOk = ( range._Range.endOffset == nodeLength ) ;
606	}
607
608	return startOk && endOk ;
609}
610
611// Kludge for #247
612FCKEnterKey.prototype._FixIESelectAllBug = function( range )
613{
614	var doc = this.Window.document ;
615	doc.body.innerHTML = '' ;
616	var editBlock ;
617	if ( FCKConfig.EnterMode.IEquals( ['div', 'p'] ) )
618	{
619		editBlock = doc.createElement( FCKConfig.EnterMode ) ;
620		doc.body.appendChild( editBlock ) ;
621	}
622	else
623		editBlock = doc.body ;
624
625	range.MoveToNodeContents( editBlock ) ;
626	range.Collapse( true ) ;
627	range.Select() ;
628	range.Release() ;
629}
630