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