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