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