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