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 * Creation and initialization of the "FCK" object. This is the main
22 * object that represents an editor instance.
23 * (Gecko specific implementations)
24 */
25
26FCK.Description = "FCKeditor for Gecko Browsers" ;
27
28FCK.InitializeBehaviors = function()
29{
30	// When calling "SetData", the editing area IFRAME gets a fixed height. So we must recalculate it.
31	if ( FCKBrowserInfo.IsGecko )		// Not for Safari/Opera.
32		Window_OnResize() ;
33
34	FCKFocusManager.AddWindow( this.EditorWindow ) ;
35
36	this.ExecOnSelectionChange = function()
37	{
38		FCK.Events.FireEvent( "OnSelectionChange" ) ;
39	}
40
41	this._ExecDrop = function( evt )
42	{
43		if ( FCK.MouseDownFlag )
44		{
45			FCK.MouseDownFlag = false ;
46			return ;
47		}
48		if ( FCKConfig.ForcePasteAsPlainText )
49		{
50			if ( evt.dataTransfer )
51			{
52				var text = evt.dataTransfer.getData( 'Text' ) ;
53				text = FCKTools.HTMLEncode( text ) ;
54				text = FCKTools.ProcessLineBreaks( window, FCKConfig, text ) ;
55				FCK.InsertHtml( text ) ;
56			}
57			else if ( FCKConfig.ShowDropDialog )
58				FCK.PasteAsPlainText() ;
59		}
60		else if ( FCKConfig.ShowDropDialog )
61			FCKDialog.OpenDialog( 'FCKDialog_Paste', FCKLang.Paste, 'dialog/fck_paste.html', 400, 330, 'Security' ) ;
62		evt.preventDefault() ;
63		evt.stopPropagation() ;
64	}
65
66	this._ExecCheckCaret = function( evt )
67	{
68		if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG )
69			return ;
70
71		if ( evt.type == 'keypress' )
72		{
73			var keyCode = evt.keyCode ;
74			// ignore if positioning key is not pressed.
75			// left or up arrow keys need to be processed as well, since <a> links can be expanded in Gecko's editor
76			// when the caret moved left or up from another block element below.
77			if ( keyCode < 33 || keyCode > 40 )
78				return ;
79		}
80
81		var blockEmptyStop = function( node )
82		{
83			if ( node.nodeType != 1 )
84				return false ;
85			var tag = node.tagName.toLowerCase() ;
86			return ( FCKListsLib.BlockElements[tag] || FCKListsLib.EmptyElements[tag] ) ;
87		}
88
89		var moveCursor = function()
90		{
91			var selection = FCK.EditorWindow.getSelection() ;
92			var range = selection.getRangeAt(0) ;
93			if ( ! range || ! range.collapsed )
94				return ;
95
96			var node = range.endContainer ;
97
98			// only perform the patched behavior if we're at the end of a text node.
99			if ( node.nodeType != 3 )
100				return ;
101
102			if ( node.nodeValue.length != range.endOffset )
103				return ;
104
105			// only perform the patched behavior if we're in an <a> tag, or the End key is pressed.
106			var parentTag = node.parentNode.tagName.toLowerCase() ;
107			if ( ! (  parentTag == 'a' ||
108					( ! ( FCKListsLib.BlockElements[parentTag] || FCKListsLib.NonEmptyBlockElements[parentTag] )
109					  && keyCode == 35 ) ) )
110				return ;
111
112			// our caret has moved to just after the last character of a text node under an unknown tag, how to proceed?
113			// first, see if there are other text nodes by DFS walking from this text node.
114			// 	- if the DFS has scanned all nodes under my parent, then go the next step.
115			//	- if there is a text node after me but still under my parent, then do nothing and return.
116			var nextTextNode = FCKTools.GetNextTextNode( node, node.parentNode, blockEmptyStop ) ;
117			if ( nextTextNode )
118				return ;
119
120			// we're pretty sure we need to move the caret forcefully from here.
121			range = FCK.EditorDocument.createRange() ;
122
123			nextTextNode = FCKTools.GetNextTextNode( node, node.parentNode.parentNode, blockEmptyStop ) ;
124			if ( nextTextNode )
125			{
126				// Opera thinks the dummy empty text node we append beyond the end of <a> nodes occupies a caret
127				// position. So if the user presses the left key and we reset the caret position here, the user
128				// wouldn't be able to go back.
129				if ( FCKBrowserInfo.IsOpera && keyCode == 37 )
130					return ;
131
132				// now we want to get out of our current parent node, adopt the next parent, and move the caret to
133				// the appropriate text node under our new parent.
134				// our new parent might be our current parent's siblings if we are lucky.
135				range.setStart( nextTextNode, 0 ) ;
136				range.setEnd( nextTextNode, 0 ) ;
137			}
138			else
139			{
140				// no suitable next siblings under our grandparent! what to do next?
141				while ( node.parentNode
142					&& node.parentNode != FCK.EditorDocument.body
143					&& node.parentNode != FCK.EditorDocument.documentElement
144					&& node == node.parentNode.lastChild
145					&& ( ! FCKListsLib.BlockElements[node.parentNode.tagName.toLowerCase()] ) )
146					node = node.parentNode ;
147
148
149				if ( FCKListsLib.BlockElements[ parentTag ]
150						|| FCKListsLib.EmptyElements[ parentTag ]
151						|| node == FCK.EditorDocument.body )
152				{
153					// if our parent is a block node, move to the end of our parent.
154					range.setStart( node, node.childNodes.length ) ;
155					range.setEnd( node, node.childNodes.length ) ;
156				}
157				else
158				{
159					// things are a little bit more interesting if our parent is not a block node
160					// due to the weired ways how Gecko's caret acts...
161					var stopNode = node.nextSibling ;
162
163					// find out the next block/empty element at our grandparent, we'll
164					// move the caret just before it.
165					while ( stopNode )
166					{
167						if ( stopNode.nodeType != 1 )
168						{
169							stopNode = stopNode.nextSibling ;
170							continue ;
171						}
172
173						var stopTag = stopNode.tagName.toLowerCase() ;
174						if ( FCKListsLib.BlockElements[stopTag] || FCKListsLib.EmptyElements[stopTag] )
175							break ;
176						stopNode = stopNode.nextSibling ;
177					}
178
179					// note that the dummy marker below is NEEDED, otherwise the caret's behavior will
180					// be broken in Gecko.
181					var marker = FCK.EditorDocument.createTextNode( '' ) ;
182					if ( stopNode )
183						node.parentNode.insertBefore( marker, stopNode ) ;
184					else
185						node.parentNode.appendChild( marker ) ;
186					range.setStart( marker, 0 ) ;
187					range.setEnd( marker, 0 ) ;
188				}
189			}
190
191			selection.removeAllRanges() ;
192			selection.addRange( range ) ;
193			FCK.Events.FireEvent( "OnSelectionChange" ) ;
194		}
195
196		setTimeout( moveCursor, 1 ) ;
197	}
198
199	this._FillEmptyBlock = function( emptyBlockNode )
200	{
201		if ( ! emptyBlockNode || emptyBlockNode.nodeType != 1 )
202			return ;
203		var nodeTag = emptyBlockNode.tagName.toLowerCase() ;
204		if ( nodeTag != 'p' && nodeTag != 'div' )
205			return ;
206		if ( emptyBlockNode.firstChild )
207			return ;
208		FCKTools.AppendBogusBr( emptyBlockNode ) ;
209	}
210
211	this._ExecCheckEmptyBlock = function()
212	{
213		FCK._FillEmptyBlock( FCK.EditorDocument.body.firstChild ) ;
214		var sel = FCK.EditorWindow.getSelection() ;
215		if ( !sel || sel.rangeCount < 1 )
216			return ;
217		var range = sel.getRangeAt( 0 );
218		FCK._FillEmptyBlock( range.startContainer ) ;
219	}
220
221	this.ExecOnSelectionChangeTimer = function()
222	{
223		if ( FCK.LastOnChangeTimer )
224			window.clearTimeout( FCK.LastOnChangeTimer ) ;
225
226		FCK.LastOnChangeTimer = window.setTimeout( FCK.ExecOnSelectionChange, 100 ) ;
227	}
228
229	this.EditorDocument.addEventListener( 'mouseup', this.ExecOnSelectionChange, false ) ;
230
231	// On Gecko, firing the "OnSelectionChange" event on every key press started to be too much
232	// slow. So, a timer has been implemented to solve performance issues when typing to quickly.
233	this.EditorDocument.addEventListener( 'keyup', this.ExecOnSelectionChangeTimer, false ) ;
234
235	this._DblClickListener = function( e )
236	{
237		FCK.OnDoubleClick( e.target ) ;
238		e.stopPropagation() ;
239	}
240	this.EditorDocument.addEventListener( 'dblclick', this._DblClickListener, true ) ;
241
242	// Record changes for the undo system when there are key down events.
243	this.EditorDocument.addEventListener( 'keydown', this._KeyDownListener, false ) ;
244
245	// Hooks for data object drops
246	if ( FCKBrowserInfo.IsGecko )
247	{
248		this.EditorWindow.addEventListener( 'dragdrop', this._ExecDrop, true ) ;
249	}
250	else if ( FCKBrowserInfo.IsSafari )
251	{
252		var cancelHandler = function( evt ){ if ( ! FCK.MouseDownFlag ) evt.returnValue = false ; }
253		this.EditorDocument.addEventListener( 'dragenter', cancelHandler, true ) ;
254		this.EditorDocument.addEventListener( 'dragover', cancelHandler, true ) ;
255		this.EditorDocument.addEventListener( 'drop', this._ExecDrop, true ) ;
256		this.EditorDocument.addEventListener( 'mousedown',
257			function( ev )
258			{
259				var element = ev.srcElement ;
260
261				if ( element.nodeName.IEquals( 'IMG', 'HR', 'INPUT', 'TEXTAREA', 'SELECT' ) )
262				{
263					FCKSelection.SelectNode( element ) ;
264				}
265			}, true ) ;
266
267		this.EditorDocument.addEventListener( 'mouseup',
268			function( ev )
269			{
270				if ( ev.srcElement.nodeName.IEquals( 'INPUT', 'TEXTAREA', 'SELECT' ) )
271					ev.preventDefault()
272			}, true ) ;
273
274		this.EditorDocument.addEventListener( 'click',
275			function( ev )
276			{
277				if ( ev.srcElement.nodeName.IEquals( 'INPUT', 'TEXTAREA', 'SELECT' ) )
278					ev.preventDefault()
279			}, true ) ;
280	}
281
282	// Kludge for buggy Gecko caret positioning logic (Bug #393 and #1056)
283	if ( FCKBrowserInfo.IsGecko || FCKBrowserInfo.IsOpera )
284	{
285		this.EditorDocument.addEventListener( 'keypress', this._ExecCheckCaret, false ) ;
286		this.EditorDocument.addEventListener( 'click', this._ExecCheckCaret, false ) ;
287	}
288	if ( FCKBrowserInfo.IsGecko )
289		this.AttachToOnSelectionChange( this._ExecCheckEmptyBlock ) ;
290
291	// Reset the context menu.
292	FCK.ContextMenu._InnerContextMenu.SetMouseClickWindow( FCK.EditorWindow ) ;
293	FCK.ContextMenu._InnerContextMenu.AttachToElement( FCK.EditorDocument ) ;
294}
295
296FCK.MakeEditable = function()
297{
298	this.EditingArea.MakeEditable() ;
299}
300
301// Disable the context menu in the editor (outside the editing area).
302function Document_OnContextMenu( e )
303{
304	if ( !e.target._FCKShowContextMenu )
305		e.preventDefault() ;
306}
307document.oncontextmenu = Document_OnContextMenu ;
308
309// GetNamedCommandState overload for Gecko.
310FCK._BaseGetNamedCommandState = FCK.GetNamedCommandState ;
311FCK.GetNamedCommandState = function( commandName )
312{
313	switch ( commandName )
314	{
315		case 'Unlink' :
316			return FCKSelection.HasAncestorNode('A') ? FCK_TRISTATE_OFF : FCK_TRISTATE_DISABLED ;
317		default :
318			return FCK._BaseGetNamedCommandState( commandName ) ;
319	}
320}
321
322// Named commands to be handled by this browsers specific implementation.
323FCK.RedirectNamedCommands =
324{
325	Print	: true,
326	Paste	: true,
327
328	Cut	: true,
329	Copy	: true
330} ;
331
332// ExecuteNamedCommand overload for Gecko.
333FCK.ExecuteRedirectedNamedCommand = function( commandName, commandParameter )
334{
335	switch ( commandName )
336	{
337		case 'Print' :
338			FCK.EditorWindow.print() ;
339			break ;
340		case 'Paste' :
341			try
342			{
343				// Force the paste dialog for Safari (#50).
344				if ( FCKBrowserInfo.IsSafari )
345					throw '' ;
346
347				if ( FCK.Paste() )
348					FCK.ExecuteNamedCommand( 'Paste', null, true ) ;
349			}
350			catch (e)	{ FCKDialog.OpenDialog( 'FCKDialog_Paste', FCKLang.Paste, 'dialog/fck_paste.html', 400, 330, 'Security' ) ; }
351			break ;
352		case 'Cut' :
353			try			{ FCK.ExecuteNamedCommand( 'Cut', null, true ) ; }
354			catch (e)	{ alert(FCKLang.PasteErrorCut) ; }
355			break ;
356		case 'Copy' :
357			try			{ FCK.ExecuteNamedCommand( 'Copy', null, true ) ; }
358			catch (e)	{ alert(FCKLang.PasteErrorCopy) ; }
359			break ;
360		default :
361			FCK.ExecuteNamedCommand( commandName, commandParameter ) ;
362	}
363}
364
365FCK._ExecPaste = function()
366{
367	// Save a snapshot for undo before actually paste the text
368	FCKUndo.SaveUndoStep() ;
369
370	if ( FCKConfig.ForcePasteAsPlainText )
371	{
372		FCK.PasteAsPlainText() ;
373		return false ;
374	}
375
376	/* For now, the AutoDetectPasteFromWord feature is IE only. */
377	return true ;
378}
379
380//**
381// FCK.InsertHtml: Inserts HTML at the current cursor location. Deletes the
382// selected content if any.
383FCK.InsertHtml = function( html )
384{
385	html = FCKConfig.ProtectedSource.Protect( html ) ;
386	html = FCK.ProtectEvents( html ) ;
387	html = FCK.ProtectUrls( html ) ;
388	html = FCK.ProtectTags( html ) ;
389
390	// Save an undo snapshot first.
391	FCKUndo.SaveUndoStep() ;
392
393	// Insert the HTML code.
394	this.EditorDocument.execCommand( 'inserthtml', false, html ) ;
395	this.Focus() ;
396
397	// For some strange reason the SaveUndoStep() call doesn't activate the undo button at the first InsertHtml() call.
398	this.Events.FireEvent( "OnSelectionChange" ) ;
399}
400
401FCK.PasteAsPlainText = function()
402{
403	// TODO: Implement the "Paste as Plain Text" code.
404
405	// If the function is called immediately Firefox 2 does automatically paste the contents as soon as the new dialog is created
406	// so we run it in a Timeout and the paste event can be cancelled
407	FCKTools.RunFunction( FCKDialog.OpenDialog, FCKDialog, ['FCKDialog_Paste', FCKLang.PasteAsText, 'dialog/fck_paste.html', 400, 330, 'PlainText'] ) ;
408
409/*
410	var sText = FCKTools.HTMLEncode( clipboardData.getData("Text") ) ;
411	sText = sText.replace( /\n/g, '<BR>' ) ;
412	this.InsertHtml( sText ) ;
413*/
414}
415/*
416FCK.PasteFromWord = function()
417{
418	// TODO: Implement the "Paste as Plain Text" code.
419
420	FCKDialog.OpenDialog( 'FCKDialog_Paste', FCKLang.PasteFromWord, 'dialog/fck_paste.html', 400, 330, 'Word' ) ;
421
422//	FCK.CleanAndPaste( FCK.GetClipboardHTML() ) ;
423}
424*/
425FCK.GetClipboardHTML = function()
426{
427	return '' ;
428}
429
430FCK.CreateLink = function( url, noUndo )
431{
432	// Creates the array that will be returned. It contains one or more created links (see #220).
433	var aCreatedLinks = new Array() ;
434
435	FCK.ExecuteNamedCommand( 'Unlink', null, false, !!noUndo ) ;
436
437	if ( url.length > 0 )
438	{
439		// Generate a temporary name for the link.
440		var sTempUrl = 'javascript:void(0);/*' + ( new Date().getTime() ) + '*/' ;
441
442		// Use the internal "CreateLink" command to create the link.
443		FCK.ExecuteNamedCommand( 'CreateLink', sTempUrl, false, !!noUndo ) ;
444
445		// Retrieve the just created links using XPath.
446		var oLinksInteractor = this.EditorDocument.evaluate("//a[@href='" + sTempUrl + "']", this.EditorDocument.body, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null) ;
447
448		// Add all links to the returning array.
449		for ( var i = 0 ; i < oLinksInteractor.snapshotLength ; i++ )
450		{
451			var oLink = oLinksInteractor.snapshotItem( i ) ;
452			oLink.href = url ;
453			aCreatedLinks.push( oLink ) ;
454		}
455	}
456
457	return aCreatedLinks ;
458}
459