0751edaeed4017f70a79199250959da49e1bc79c
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / htmlarea-gecko.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2002-2004, interactivetools.com, inc.
5 * (c) 2003-2004 dynarch.com
6 * (c) 2004-2007 Stanislas Rolland <stanislas.rolland(arobas)fructifor.ca>
7 * All rights reserved
8 *
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
17 * A copy is found in the textfile GPL.txt and important notices to the license
18 * from the author is found in LICENSE.txt distributed with these scripts.
19 *
20 *
21 * This script is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * This script is a modified version of a script published under the htmlArea License.
27 * A copy of the htmlArea License may be found in the textfile HTMLAREA_LICENSE.txt.
28 *
29 * This copyright notice MUST APPEAR in all copies of the script!
30 ***************************************************************/
31 /*
32 * TYPO3 CVS ID: $Id$
33 */
34
35 /***************************************************
36 * GECKO-SPECIFIC FUNCTIONS
37 ***************************************************/
38 HTMLArea.prototype.isEditable = function() {
39 return (this._doc.designMode === "on");
40 };
41
42 /***************************************************
43 * MOZILLA/FIREFOX EDIT MODE INITILIZATION
44 ***************************************************/
45
46 HTMLArea.prototype._initEditMode = function () {
47 // We can't set designMode when we are in a hidden TYPO3 tab
48 // Then we will set it when the tab comes in the front.
49 var isNested = false;
50 var allDisplayed = true;
51
52 if (this.nested.sorted && this.nested.sorted.length) {
53 isNested = true;
54 allDisplayed = HTMLArea.allElementsAreDisplayed(this.nested.sorted);
55 }
56 if (!isNested || allDisplayed) {
57 try {
58 this._doc.designMode = "on";
59 if (this._doc.queryCommandEnabled("insertbronreturn")) this._doc.execCommand("insertbronreturn", false, this.config.disableEnterParagraphs);
60 if (this._doc.queryCommandEnabled("enableInlineTableEditing")) this._doc.execCommand("enableInlineTableEditing", false, (this.config.buttons.table && this.config.buttons.table.enableHandles) ? true : false);
61 if (this._doc.queryCommandEnabled("styleWithCSS")) this._doc.execCommand("styleWithCSS", false, this.config.useCSS);
62 else if (this._doc.queryCommandEnabled("useCSS")) this._doc.execCommand("useCSS", false, !this.config.useCSS);
63 } catch(e) {
64 if (HTMLArea.is_wamcom) {
65 this._doc.open();
66 this._doc.close();
67 this._initIframeTimer = window.setTimeout("HTMLArea.initIframe(" + this._editorNumber + ");", 500);
68 return false;
69 }
70 }
71 }
72 // When the TYPO3 TCA feature div2tab is used, the editor iframe may become hidden with style.display = "none"
73 // This breaks the editor in Mozilla/Firefox browsers: the designMode attribute needs to be resetted after the style.display of the containing div is resetted to "block"
74 // Here we rely on TYPO3 naming conventions for the div id and class name
75 if (this.nested.sorted && this.nested.sorted.length) {
76 var nestedObj, listenerFunction;
77 for (var i=0, length=this.nested.sorted.length; i < length; i++) {
78 nestedObj = document.getElementById(this.nested.sorted[i]);
79 listenerFunction = HTMLArea.NestedListener(this, nestedObj, false);
80 HTMLArea._addEvent(nestedObj, 'DOMAttrModified', listenerFunction);
81 }
82 }
83 return true;
84 };
85
86 /***************************************************
87 * SELECTIONS AND RANGES
88 ***************************************************/
89
90 /*
91 * Get the current selection object
92 */
93 HTMLArea.prototype._getSelection = function() {
94 return this._iframe.contentWindow.getSelection();
95 };
96
97 /*
98 * Empty the selection object
99 */
100 HTMLArea.prototype.emptySelection = function(selection) {
101 if (HTMLArea.is_safari) {
102 selection.empty();
103 } else {
104 selection.removeAllRanges();
105 }
106 if (HTMLArea.is_opera) {
107 this._iframe.focus();
108 }
109 };
110
111 /*
112 * Add a range to the selection
113 */
114 HTMLArea.prototype.addRangeToSelection = function(selection, range) {
115 if (HTMLArea.is_safari) {
116 selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
117 } else {
118 selection.addRange(range);
119 }
120 };
121
122 /*
123 * Create a range for the current selection
124 */
125 HTMLArea.prototype._createRange = function(sel) {
126 if (HTMLArea.is_safari) {
127 var range = this._doc.createRange();
128 if (typeof(sel) == "undefined") {
129 return range;
130 } else if (sel.baseNode == null) {
131 range.setStart(this._doc.body,0);
132 range.setEnd(this._doc.body,0);
133 return range;
134 } else {
135 range.setStart(sel.baseNode, sel.baseOffset);
136 range.setEnd(sel.extentNode, sel.extentOffset);
137 if (range.collapsed != sel.isCollapsed) {
138 range.setStart(sel.extentNode, sel.extentOffset);
139 range.setEnd(sel.baseNode, sel.baseOffset);
140 }
141 return range;
142 }
143 }
144 if (typeof(sel) == "undefined") return this._doc.createRange();
145 try {
146 return sel.getRangeAt(0);
147 } catch(e) {
148 return this._doc.createRange();
149 }
150 };
151
152 /*
153 * Select a node AND the contents inside the node
154 */
155 HTMLArea.prototype.selectNode = function(node, endPoint) {
156 this.focusEditor();
157 var selection = this._getSelection();
158 var range = this._doc.createRange();
159 if (node.nodeType == 1 && node.nodeName.toLowerCase() == "body") {
160 range.selectNodeContents(node);
161 } else {
162 range.selectNode(node);
163 }
164 if (typeof(endPoint) != "undefined") {
165 range.collapse(endPoint);
166 }
167 this.emptySelection(selection);
168 this.addRangeToSelection(selection, range);
169 };
170
171 /*
172 * Select ONLY the contents inside the given node
173 */
174 HTMLArea.prototype.selectNodeContents = function(node, endPoint) {
175 this.focusEditor();
176 var selection = this._getSelection();
177 var range = this._doc.createRange();
178 range.selectNodeContents(node);
179 if (typeof(endPoint) !== "undefined") {
180 range.collapse(endPoint);
181 }
182 this.emptySelection(selection);
183 this.addRangeToSelection(selection, range);
184 };
185
186 HTMLArea.prototype.rangeIntersectsNode = function(range, node) {
187 var nodeRange = this._doc.createRange();
188 try {
189 nodeRange.selectNode(node);
190 } catch (e) {
191 nodeRange.selectNodeContents(node);
192 }
193 // Note: sometimes Safari inverts the end points
194 return (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == -1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == 1) ||
195 (range.compareBoundaryPoints(range.END_TO_START, nodeRange) == 1 && range.compareBoundaryPoints(range.START_TO_END, nodeRange) == -1);
196 };
197
198 /*
199 * Get the selection type
200 */
201 HTMLArea.prototype.getSelectionType = function(selection) {
202 // By default set the type to "Text".
203 var type = "Text";
204 if (!selection) {
205 var selection = this._getSelection();
206 }
207 // Check if the actual selection is a Control
208 if (selection && selection.rangeCount == 1) {
209 var range = selection.getRangeAt(0) ;
210 if (range.startContainer == range.endContainer
211 && (range.endOffset - range.startOffset) == 1
212 && range.startContainer.nodeType == 1
213 && /^(img|hr|li|table|tr|td|embed|object|ol|ul)$/i.test(range.startContainer.childNodes[range.startOffset].nodeName)) {
214 type = "Control";
215 }
216 }
217 return type;
218 };
219
220 /*
221 * Retrieves the selected element (if any), just in the case that a single element (object like and image or a table) is selected.
222 */
223 HTMLArea.prototype.getSelectedElement = function(selection) {
224 var selectedElement = null;
225 if (!selection) {
226 var selection = this._getSelection();
227 }
228 if (selection && selection.anchorNode && selection.anchorNode.nodeType == 1) {
229 if (this.getSelectionType(selection) == "Control") {
230 selectedElement = selection.anchorNode.childNodes[selection.anchorOffset];
231 // For Safari, the anchor node for a control selection is the control itself
232 if (!selectedElement) {
233 selectedElement = selection.anchorNode;
234 } else if (selectedElement.nodeType != 1) {
235 return null;
236 }
237 }
238 }
239 return selectedElement;
240 };
241
242 /*
243 * Retrieve the HTML contents of selected block
244 */
245 HTMLArea.prototype.getSelectedHTML = function() {
246 var range = this._createRange(this._getSelection());
247 if (range.collapsed) return "";
248 var cloneContents = range.cloneContents();
249 if (!cloneContents) {
250 cloneContents = this._doc.createDocumentFragment();
251 }
252 return HTMLArea.getHTML(cloneContents, false, this);
253 };
254
255 /*
256 * Retrieve simply HTML contents of the selected block, IE ignoring control ranges
257 */
258 HTMLArea.prototype.getSelectedHTMLContents = function() {
259 return this.getSelectedHTML();
260 };
261
262 /*
263 * Get the deepest node that contains both endpoints of the current selection.
264 */
265 HTMLArea.prototype.getParentElement = function(selection, range) {
266 if (!selection) {
267 var selection = this._getSelection();
268 }
269 if (this.getSelectionType(selection) === "Control") {
270 return this.getSelectedElement(selection);
271 }
272 if (typeof(range) === "undefined") {
273 var range = this._createRange(selection);
274 }
275 var parentElement = range.commonAncestorContainer;
276 // For some reason, Firefox 3 may report the Document as commonAncestorContainer
277 if (parentElement.nodeType == 9) return this._doc.body;
278 while (parentElement && parentElement.nodeType == 3) {
279 parentElement = parentElement.parentNode;
280 }
281 return parentElement;
282 };
283
284 /*
285 * Get the selected element, if any. That is, the element that you have last selected in the "path"
286 * at the bottom of the editor, or a "control" (eg image)
287 *
288 * @returns null | element
289 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
290 */
291 HTMLArea.prototype._activeElement = function(selection) {
292 if (this._selectionEmpty(selection)) {
293 return null;
294 }
295 // Check if the anchor (start of selection) is an element.
296 if (selection.anchorNode.nodeType == 1) {
297 return selection.anchorNode;
298 } else {
299 return null;
300 }
301 };
302
303 /*
304 * Determine if the current selection is empty or not.
305 */
306 HTMLArea.prototype._selectionEmpty = function(sel) {
307 if (!sel) return true;
308 return sel.isCollapsed;
309 };
310
311 /*
312 * Get a bookmark
313 * Adapted from FCKeditor
314 * This is an "intrusive" way to create a bookmark. It includes <span> tags
315 * in the range boundaries. The advantage of it is that it is possible to
316 * handle DOM mutations when moving back to the bookmark.
317 */
318 HTMLArea.prototype.getBookmark = function (range) {
319 // For performance, includeNodes=true if intended to SelectBookmark.
320 // Create the bookmark info (random IDs).
321 var bookmark = {
322 startId : (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'S',
323 endId : (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'E'
324 };
325
326 var startSpan;
327 var endSpan;
328 var rangeClone = range.cloneRange();
329
330 // For collapsed ranges, add just the start marker.
331 if (!range.collapsed ) {
332 endSpan = this._doc.createElement("span");
333 endSpan.style.display = "none";
334 endSpan.id = bookmark.endId;
335 endSpan.setAttribute("HTMLArea_bookmark", true);
336 endSpan.innerHTML = "&nbsp;";
337 rangeClone.collapse(false);
338 rangeClone.insertNode(endSpan);
339 }
340
341 startSpan = this._doc.createElement("span");
342 startSpan.style.display = "none";
343 startSpan.id = bookmark.startId;
344 startSpan.setAttribute("HTMLArea_bookmark", true);
345 startSpan.innerHTML = "&nbsp;";
346 var rangeClone = range.cloneRange();
347 rangeClone.collapse(true);
348 rangeClone.insertNode(startSpan);
349 bookmark.startNode = startSpan;
350 bookmark.endNode = endSpan;
351 // Update the range position.
352 if (endSpan) {
353 range.setEndBefore(endSpan);
354 range.setStartAfter(startSpan);
355 } else {
356 range.setEndAfter(startSpan);
357 range.collapse(false);
358 }
359 return bookmark;
360 };
361
362 /*
363 * Get the end point of the bookmark
364 * Adapted from FCKeditor
365 */
366 HTMLArea.prototype.getBookmarkNode = function(bookmark, endPoint) {
367 if (endPoint) {
368 return bookmark.startNode || this._doc.getElementById(bookmark.startId);
369 } else {
370 return bookmark.endNode || this._doc.getElementById(bookmark.endId);
371 }
372 };
373
374 /*
375 * Move the range to the bookmark
376 * Adapted from FCKeditor
377 */
378 HTMLArea.prototype.moveToBookmark = function (bookmark) {
379 var startSpan = this.getBookmarkNode(bookmark, true);
380 var endSpan = this.getBookmarkNode(bookmark, false);
381
382 var range = this._createRange();
383 range.setStartBefore(startSpan);
384 HTMLArea.removeFromParent(startSpan);
385 // If collapsed, the end span will not be available.
386 if (endSpan) {
387 range.setEndBefore(endSpan);
388 HTMLArea.removeFromParent(endSpan);
389 } else {
390 range.collapse(true);
391 }
392 return range;
393 };
394
395 /*
396 * Select range
397 */
398 HTMLArea.prototype.selectRange = function (range) {
399 var selection = this._getSelection();
400 this.emptySelection(selection);
401 this.addRangeToSelection(selection, range);
402 };
403
404 /***************************************************
405 * DOM TREE MANIPULATION
406 ***************************************************/
407
408 /*
409 * Insert a node at the current position.
410 * Delete the current selection, if any.
411 * Split the text node, if needed.
412 */
413 HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
414 this.focusEditor();
415 var range = this._createRange(this._getSelection());
416 range.deleteContents();
417 var toBeSelected = (toBeInserted.nodeType === 11) ? toBeInserted.lastChild : toBeInserted;
418 range.insertNode(toBeInserted);
419 this.selectNodeContents(toBeSelected, false);
420 };
421
422 /*
423 * Insert HTML source code at the current position.
424 * Delete the current selection, if any.
425 */
426 HTMLArea.prototype.insertHTML = function(html) {
427 this.focusEditor();
428 var fragment = this._doc.createDocumentFragment();
429 var div = this._doc.createElement("div");
430 div.innerHTML = html;
431 while (div.firstChild) {
432 fragment.appendChild(div.firstChild);
433 }
434 this.insertNodeAtSelection(fragment);
435 };
436
437 /***************************************************
438 * EVENTS HANDLERS
439 ***************************************************/
440
441 /*
442 * TYPO3 hidden tab and inline event listener (gets event calls)
443 */
444 HTMLArea.NestedListener = function (editor,nestedObj,noOpenCloseAction) {
445 return (function(ev) {
446 if(!ev) var ev = window.event;
447 HTMLArea.NestedHandler(ev,editor,nestedObj,noOpenCloseAction);
448 });
449 };
450
451 /*
452 * TYPO3 hidden tab and inline event handler (performs actions on event calls)
453 */
454 HTMLArea.NestedHandler = function(ev,editor,nestedObj,noOpenCloseAction) {
455 window.setTimeout(function() {
456 var target = (ev.target) ? ev.target : ev.srcElement;
457 if(target == nestedObj && editor._editMode == "wysiwyg" && ev.attrName=='style' && (target.style.display == '' || target.style.display == 'block')) {
458 // Check if all affected nested elements are displayed (style.display!='none'):
459 if (HTMLArea.allElementsAreDisplayed(editor.nested.sorted)) {
460 window.setTimeout(function() {
461 try {
462 editor._doc.designMode = "on";
463 if (editor.config.sizeIncludesToolbar && editor._initialToolbarOffsetHeight != editor._toolbar.offsetHeight) {
464 editor.sizeIframe(-2);
465 }
466 if (editor._doc.queryCommandEnabled("insertbronreturn")) editor._doc.execCommand("insertbronreturn", false, editor.config.disableEnterParagraphs);
467 if (editor._doc.queryCommandEnabled("enableInlineTableEditing")) editor._doc.execCommand("enableInlineTableEditing", false, (editor.config.buttons.table && editor.config.buttons.table.enableHandles) ? true : false);
468 if (editor._doc.queryCommandEnabled("styleWithCSS")) editor._doc.execCommand("styleWithCSS", false, editor.config.useCSS);
469 else if (editor._doc.queryCommandEnabled("useCSS")) editor._doc.execCommand("useCSS", false, !editor.config.useCSS);
470 } catch(e) {
471 // If an event of a parent tab ("nested tabs") is triggered, the following lines should not be
472 // processed, because this causes some trouble on all event handlers...
473 if (!noOpenCloseAction) {
474 editor._doc.open();
475 editor._doc.close();
476 }
477 editor.initIframe();
478 }
479 }, 50);
480 }
481 HTMLArea._stopEvent(ev);
482 }
483 }, 50);
484 };
485
486 /*
487 * Handle statusbar element events
488 */
489 HTMLArea.statusBarHandler = function (ev) {
490 if(!ev) var ev = window.event;
491 var target = (ev.target) ? ev.target : ev.srcElement;
492 var editor = target.editor;
493 target.blur();
494 editor.selectNodeContents(target.el);
495 editor._statusBarTree.selected = target.el;
496 editor.updateToolbar(true);
497 switch (ev.type) {
498 case "click" :
499 case "mousedown" :
500 HTMLArea._stopEvent(ev);
501 return false;
502 case "contextmenu" :
503 return editor.plugins["ContextMenu"] ? editor.plugins["ContextMenu"].instance.popupMenu(ev,target.el) : false;
504 }
505 };
506
507 /*
508 * Paste exception handler
509 */
510 HTMLArea.prototype._mozillaPasteException = function(cmdID, UI, param) {
511 // Mozilla lauches an exception, but can paste anyway on ctrl-V
512 // UI is false on keyboard shortcut, and undefined on button click
513 if(typeof(UI) != "undefined") {
514 this._doc.execCommand(cmdID, UI, param);
515 if (cmdID == "Paste" && this.config.killWordOnPaste) HTMLArea._wordClean(this._doc.body);
516 } else if (this.config.enableMozillaExtension) {
517 if (confirm(HTMLArea.I18N.msg["Allow-Clipboard-Helper-Extension"])) {
518 if (InstallTrigger.enabled()) {
519 HTMLArea._mozillaXpi = new Object();
520 HTMLArea._mozillaXpi["AllowClipboard Helper"] = _editor_mozAllowClipboard_url;
521 InstallTrigger.install(HTMLArea._mozillaXpi,HTMLArea._mozillaInstallCallback);
522 } else {
523 alert(HTMLArea.I18N.msg["Mozilla-Org-Install-Not-Enabled"]);
524 HTMLArea._appendToLog("WARNING [HTMLArea::execCommand]: Mozilla install was not enabled.");
525 return;
526 }
527 }
528 } else if (confirm(HTMLArea.I18N.msg["Moz-Clipboard"])) {
529 window.open("http://mozilla.org/editor/midasdemo/securityprefs.html");
530 }
531 }
532
533 HTMLArea._mozillaInstallCallback = function(url,returnCode) {
534 if (returnCode == 0) {
535 if (HTMLArea._mozillaXpi["TYPO3 htmlArea RTE Preferences"]) alert(HTMLArea.I18N.msg["Moz-Extension-Success"]);
536 else alert(HTMLArea.I18N.msg["Allow-Clipboard-Helper-Extension-Success"]);
537 return;
538 } else {
539 alert(HTMLArea.I18N.msg["Moz-Extension-Failure"]);
540 HTMLArea._appendToLog("WARNING [HTMLArea::execCommand]: Mozilla install return code was: " + returnCode + ".");
541 return;
542 }
543 };
544
545 /*
546 * Backspace event handler
547 */
548 HTMLArea.prototype._checkBackspace = function() {
549 var self = this;
550 self.focusEditor();
551 var sel = self._getSelection();
552 var range = self._createRange(sel);
553 var SC = range.startContainer;
554 var SO = range.startOffset;
555 var EC = range.endContainer;
556 var EO = range.endOffset;
557 var newr = SC.nextSibling;
558 while (SC.nodeType == 3 || /^a$/i.test(SC.tagName)) SC = SC.parentNode;
559 if (!self.config.disableEnterParagraphs && /^td$/i.test(SC.parentNode.tagName) && SC.parentNode.firstChild == SC && SO == 0 && range.collapsed) return true;
560 window.setTimeout(function() {
561 // Remove br tag inserted by Mozilla
562 if (!self.config.disableEnterParagraphs && (/^p$/i.test(SC.tagName) || !/\S/.test(SC.tagName)) && SO == 0) {
563 if (SC.firstChild && /^br$/i.test(SC.firstChild.tagName)) {
564 HTMLArea.removeFromParent(SC.firstChild);
565 return true;
566 }
567 }
568 if (!/\S/.test(SC.tagName)) {
569 var p = document.createElement("p");
570 while (SC.firstChild) p.appendChild(SC.firstChild);
571 SC.parentNode.insertBefore(p, SC);
572 HTMLArea.removeFromParent(SC);
573 var r = range.cloneRange();
574 r.setStartBefore(newr);
575 r.setEndAfter(newr);
576 r.extractContents();
577 this.emptySelection(sel);
578 this.addRangeToSelection(sel, r);
579 return true;
580 }
581 },10);
582 return false;
583 };
584
585 /*
586 * Enter event handler
587 */
588 HTMLArea.prototype._checkInsertP = function() {
589 var editor = this;
590 this.focusEditor();
591 var i, left, right, rangeClone,
592 sel = this._getSelection(),
593 range = this._createRange(sel),
594 p = this.getAllAncestors(),
595 block = null,
596 a = null,
597 doc = this._doc;
598 for (i = 0; i < p.length; ++i) {
599 if (HTMLArea.isBlockElement(p[i]) && !/^(html|body|table|tbody|thead|tr)$/i.test(p[i].nodeName)) {
600 block = p[i];
601 break;
602 }
603 }
604 if (block && /^(td|th|table|tbody|thead|tr)$/i.test(block.nodeName) && this.config.buttons.table && this.config.buttons.table.disableEnterParagraphs) return false;
605 if (!range.collapsed) {
606 range.deleteContents();
607 }
608 this.emptySelection(sel);
609 if (!block || /^(td|div)$/i.test(block.nodeName)) {
610 if (!block) var block = doc.body;
611 if (block.hasChildNodes()) {
612 rangeClone = range.cloneRange();
613 rangeClone.setStartBefore(block.firstChild);
614 // Working around Opera issue: The following gives a range exception
615 // rangeClone.surroundContents(left = doc.createElement("p"));
616 left = doc.createElement("p");
617 left.appendChild(rangeClone.extractContents());
618 if (!left.textContent && !left.getElementsByTagName("img") && !left.getElementsByTagName("table")) {
619 left.innerHTML = "<br />";
620 }
621 if (block.hasChildNodes()) {
622 left = block.insertBefore(left, block.firstChild);
623 } else {
624 left = block.appendChild(left);
625 }
626 left.normalize();
627 range.setEndAfter(block.lastChild);
628 range.setStartAfter(left);
629 // Working around Safari issue: The following gives a range exception
630 // range.surroundContents(right = doc.createElement("p"));
631 right = doc.createElement("p");
632 right.appendChild(range.extractContents());
633 if (!right.textContent && !left.getElementsByTagName("img") && !left.getElementsByTagName("table")) {
634 right.innerHTML = "<br />";
635 }
636 block.appendChild(right);
637 right.normalize();
638 } else {
639 var first = block.firstChild;
640 if (first) block.removeChild(first);
641 right = doc.createElement("p");
642 if (HTMLArea.is_safari || HTMLArea.is_opera) {
643 right.innerHTML = "<br />";
644 }
645 right = block.appendChild(right);
646 }
647 this.selectNodeContents(right, true);
648 } else {
649 range.setEndAfter(block);
650 var df = range.extractContents(), left_empty = false;
651 if (!/\S/.test(block.innerHTML)) {
652 if (!HTMLArea.is_opera) {
653 block.innerHTML = "<br />";
654 }
655 left_empty = true;
656 }
657 p = df.firstChild;
658 if (p) {
659 if (!/\S/.test(p.innerHTML)) {
660 if (/^h[1-6]$/i.test(p.nodeName)) {
661 p = this.convertNode(p, "p");
662 }
663 if (!HTMLArea.is_opera) {
664 p.innerHTML = "<br />";
665 }
666 }
667 if(/^li$/i.test(p.nodeName) && left_empty && !block.nextSibling) {
668 left = block.parentNode;
669 left.removeChild(block);
670 range.setEndAfter(left);
671 range.collapse(false);
672 p = this.convertNode(p, /^li$/i.test(left.parentNode.nodeName) ? "br" : "p");
673 }
674 range.insertNode(df);
675 // Remove any anchor created empty
676 if (p.previousSibling) {
677 var a = p.previousSibling.lastChild;
678 if (a && /^a$/i.test(a.nodeName) && !/\S/.test(a.innerHTML)) HTMLArea.removeFromParent(a);
679 }
680 this.selectNodeContents(p, true);
681 } else {
682 if (block.nodeName.toLowerCase() === "li") {
683 p = doc.createElement("li");
684 } else {
685 p = doc.createElement("p");
686 }
687 if (!HTMLArea.is_opera) {
688 p.innerHTML = "<br />";
689 }
690 if (block.nextSibling) {
691 p = block.parentNode.insertBefore(p, block.nextSibling);
692 } else {
693 p = block.parentNode.appendChild(p);
694 }
695 this.selectNodeContents(p, true);
696 }
697 }
698 this.scrollToCaret();
699 return true;
700 };
701
702 /*
703 * Detect emails and urls as they are typed in Mozilla
704 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
705 */
706 HTMLArea.prototype._detectURL = function(ev) {
707 var editor = this;
708 var s = this._getSelection();
709 var autoWrap = function (textNode, tag) {
710 var rightText = textNode.nextSibling;
711 if (typeof(tag) == 'string') tag = editor._doc.createElement(tag);
712 var a = textNode.parentNode.insertBefore(tag, rightText);
713 HTMLArea.removeFromParent(textNode);
714 a.appendChild(textNode);
715 rightText.data += " ";
716 s.collapse(rightText, rightText.data.length);
717 HTMLArea._stopEvent(ev);
718
719 editor._unLink = function() {
720 var t = a.firstChild;
721 a.removeChild(t);
722 a.parentNode.insertBefore(t, a);
723 HTMLArea.removeFromParent(a);
724 t.parentNode.normalize();
725 editor._unLink = null;
726 editor._unlinkOnUndo = false;
727 };
728
729 editor._unlinkOnUndo = true;
730 return a;
731 };
732
733 switch(ev.which) {
734 // Space or Enter or >, see if the text just typed looks like a URL, or email address and link it accordingly
735 case 13: // Enter
736 if(ev.shiftKey || editor.config.disableEnterParagraphs) break;
737 //Space
738 case 32:
739 if(s && s.isCollapsed && s.anchorNode.nodeType == 3 && s.anchorNode.data.length > 3 && s.anchorNode.data.indexOf('.') >= 0) {
740 var midStart = s.anchorNode.data.substring(0,s.anchorOffset).search(/[a-zA-Z0-9]+\S{3,}$/);
741 if(midStart == -1) break;
742 if(this._getFirstAncestor(s, 'a')) break; // already in an anchor
743 var matchData = s.anchorNode.data.substring(0,s.anchorOffset).replace(/^.*?(\S*)$/, '$1');
744 if (matchData.indexOf('@') != -1) {
745 var m = matchData.match(HTMLArea.RE_email);
746 if(m) {
747 var leftText = s.anchorNode;
748 var rightText = leftText.splitText(s.anchorOffset);
749 var midText = leftText.splitText(midStart);
750 var midEnd = midText.data.search(/[^a-zA-Z0-9\.@_\-]/);
751 if (midEnd != -1) var endText = midText.splitText(midEnd);
752 autoWrap(midText, 'a').href = 'mailto:' + m[0];
753 break;
754 }
755 }
756 var m = matchData.match(HTMLArea.RE_url);
757 if(m) {
758 var leftText = s.anchorNode;
759 var rightText = leftText.splitText(s.anchorOffset);
760 var midText = leftText.splitText(midStart);
761 var midEnd = midText.data.search(/[^a-zA-Z0-9\._\-\/\&\?=:@]/);
762 if (midEnd != -1) var endText = midText.splitText(midEnd);
763 autoWrap(midText, 'a').href = (m[1] ? m[1] : 'http://') + m[2];
764 break;
765 }
766 }
767 break;
768 default:
769 if(ev.keyCode == 27 || (editor._unlinkOnUndo && ev.ctrlKey && ev.which == 122) ) {
770 if(this._unLink) {
771 this._unLink();
772 HTMLArea._stopEvent(ev);
773 }
774 break;
775 } else if(ev.which || ev.keyCode == 8 || ev.keyCode == 46) {
776 this._unlinkOnUndo = false;
777 if(s.anchorNode && s.anchorNode.nodeType == 3) {
778 // See if we might be changing a link
779 var a = this._getFirstAncestor(s, 'a');
780 if(!a) break; // not an anchor
781 if(!a._updateAnchTimeout) {
782 if(s.anchorNode.data.match(HTMLArea.RE_email) && (a.href.match('mailto:' + s.anchorNode.data.trim()))) {
783 var textNode = s.anchorNode;
784 var fn = function() {
785 a.href = 'mailto:' + textNode.data.trim();
786 a._updateAnchTimeout = setTimeout(fn, 250);
787 };
788 a._updateAnchTimeout = setTimeout(fn, 250);
789 break;
790 }
791 var m = s.anchorNode.data.match(HTMLArea.RE_url);
792 if(m && a.href.match(s.anchorNode.data.trim())) {
793 var textNode = s.anchorNode;
794 var fn = function() {
795 var m = textNode.data.match(HTMLArea.RE_url);
796 a.href = (m[1] ? m[1] : 'http://') + m[2];
797 a._updateAnchTimeout = setTimeout(fn, 250);
798 }
799 a._updateAnchTimeout = setTimeout(fn, 250);
800 }
801 }
802 }
803 }
804 break;
805 }
806 };