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