124e1d7ea29ec84ab3119c0130999700848d6f49
[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, 2005, 2006 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
54 if (!HTMLArea.is_wamcom) {
55 try {
56 if (!isNested || allDisplayed) this._doc.designMode = "on";
57 } catch(e) { }
58 } else {
59 try {
60 this._doc.designMode = "on";
61 } catch(e) {
62 if (!isNested || allDisplayed) {
63 this._doc.open();
64 this._doc.close();
65 this._initIframeTimer = window.setTimeout("HTMLArea.initIframe(" + this._editorNumber + ");", 500);
66 return false;
67 }
68 }
69 }
70 // When the TYPO3 TCA feature div2tab is used, the editor iframe may become hidden with style.display = "none"
71 // 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"
72 // Here we rely on TYPO3 naming conventions for the div id and class name
73 if (this.nested.sorted && this.nested.sorted.length) {
74 var nestedObj, listenerFunction;
75 for (var i=0, length=this.nested.sorted.length; i < length; i++) {
76 nestedObj = document.getElementById(this.nested.sorted[i]);
77 listenerFunction = HTMLArea.NestedListener(this, nestedObj, false);
78 HTMLArea._addEvent(nestedObj, 'DOMAttrModified', listenerFunction);
79 }
80 }
81
82 return true;
83 };
84
85 /***************************************************
86 * SELECTIONS AND RANGES
87 ***************************************************/
88
89 /*
90 * Get the current selection object
91 */
92 HTMLArea.prototype._getSelection = function() {
93 if (HTMLArea.is_safari) return window.getSelection();
94 return this._iframe.contentWindow.getSelection();
95 };
96
97 /*
98 * Create a range for the current selection
99 */
100 HTMLArea.prototype._createRange = function(sel) {
101 if (HTMLArea.is_safari) {
102 var range = this._doc.createRange();
103 if (typeof(sel) == "undefined") return range;
104 switch (sel.type) {
105 case "Range":
106 range.setStart(sel.baseNode,sel.baseOffset);
107 range.setEnd(sel.extentNode,sel.extentOffset);
108 break;
109 case "Caret":
110 range.setStart(sel.baseNode,sel.baseOffset);
111 range.setEnd(sel.baseNode,sel.baseOffset);
112 break;
113 case "None":
114 range.setStart(this._doc.body,0);
115 range.setEnd(this._doc.body,0);
116 }
117 return range;
118 }
119 if (typeof(sel) == "undefined") return this._doc.createRange();
120 try {
121 return sel.getRangeAt(0);
122 } catch(e) {
123 return this._doc.createRange();
124 }
125 };
126
127 /*
128 * Select a node AND the contents inside the node
129 */
130 HTMLArea.prototype.selectNode = function(node,pos) {
131 this.focusEditor();
132 var sel = this._getSelection();
133 var range = this._doc.createRange();
134 if (node.nodeType == 1 && node.tagName.toLowerCase() == "body") range.selectNodeContents(node);
135 else range.selectNode(node);
136 if ((typeof(pos) != "undefined")) range.collapse(pos);
137 if (HTMLArea.is_safari) {
138 sel.empty();
139 sel.setBaseAndExtent(range.startContainer,range.startOffset,range.endContainer,range.endOffset);
140 } else {
141 sel.removeAllRanges();
142 sel.addRange(range);
143 }
144 };
145
146 /*
147 * Select ONLY the contents inside the given node
148 */
149 HTMLArea.prototype.selectNodeContents = function(node,pos) {
150 this.focusEditor();
151 var sel = this._getSelection();
152 var range = this._doc.createRange();
153 range.selectNodeContents(node);
154 if ((typeof(pos) != "undefined")) range.collapse(pos);
155 if (HTMLArea.is_safari) {
156 sel.empty();
157 sel.setBaseAndExtent(range.startContainer,range.startOffset,range.endContainer,range.endOffset);
158 } else {
159 sel.removeAllRanges();
160 sel.addRange(range);
161 }
162 };
163
164 /*
165 * Retrieve the HTML contents of selected block
166 */
167 HTMLArea.prototype.getSelectedHTML = function() {
168 var sel = this._getSelection();
169 var range = this._createRange(sel);
170 var cloneContents = "";
171 try {cloneContents = range.cloneContents();} catch(e) { }
172 return (cloneContents ? HTMLArea.getHTML(cloneContents,false,this) : "");
173 };
174
175 /*
176 * Retrieve simply HTML contents of the selected block, IE ignoring control ranges
177 */
178 HTMLArea.prototype.getSelectedHTMLContents = function() {
179 return this.getSelectedHTML();
180 };
181
182 /*
183 * Get the deepest node that contains both endpoints of the current selection.
184 */
185 HTMLArea.prototype.getParentElement = function(sel,range) {
186 if(!sel) var sel = this._getSelection();
187 if (typeof(range) == "undefined") var range = this._createRange(sel);
188 try {
189 var p = range.commonAncestorContainer;
190 if(!range.collapsed && range.startContainer == range.endContainer &&
191 range.startOffset - range.endOffset <= 1 && range.startContainer.hasChildNodes())
192 p = range.startContainer.childNodes[range.startOffset];
193 while (p.nodeType == 3) {p = p.parentNode;}
194 return p;
195 } catch (e) {
196 return this._doc.body;
197 }
198 };
199
200 /*
201 * Get the selected element, if any. That is, the element that you have last selected in the "path"
202 * at the bottom of the editor, or a "control" (eg image)
203 *
204 * @returns null | element
205 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
206 */
207 HTMLArea.prototype._activeElement = function(sel) {
208 if(sel == null) return null;
209 if(this._selectionEmpty(sel)) return null;
210 // Check if the selection is not collapsed (something is selected) and if the anchor (start of selection) is an element.
211 if(!sel.isCollapsed && sel.anchorNode.nodeType == 1) return sel.anchorNode;
212 else return null;
213 };
214
215 /*
216 * Determine if the current selection is empty or not.
217 */
218 HTMLArea.prototype._selectionEmpty = function(sel) {
219 if (!sel) return true;
220 if (typeof(sel.isCollapsed) != 'undefined') {
221 if (HTMLArea.is_opera) this._createRange(sel).collapsed;
222 else sel.isCollapsed;
223 } else {
224 return true;
225 }
226 };
227
228 /***************************************************
229 * DOM TREE MANIPULATION
230 ***************************************************/
231
232 /*
233 * Insert a node at the current position.
234 * Delete the current selection, if any.
235 * Split the text node, if needed.
236 */
237 HTMLArea.prototype.insertNodeAtSelection = function(toBeInserted) {
238 this.focusEditor();
239 var sel = this._getSelection(),
240 range = this._createRange(sel),
241 node = range.startContainer,
242 pos = range.startOffset,
243 selnode = toBeInserted;
244 if (HTMLArea.is_safari) sel.empty();
245 else sel.removeAllRanges();
246 range.deleteContents();
247 switch (node.nodeType) {
248 case 3: // Node.TEXT_NODE: we have to split it at the caret position.
249 if(toBeInserted.nodeType == 3) {
250 node.insertData(pos,toBeInserted.data);
251 range = this._createRange();
252 range.setEnd(node, pos + toBeInserted.length);
253 range.setStart(node, pos + toBeInserted.length);
254 if (HTMLArea.is_safari) sel.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
255 else sel.addRange(range);
256 } else {
257 node = node.splitText(pos);
258 if (toBeInserted.nodeType == 11) selnode = selnode.lastChild;
259 node = node.parentNode.insertBefore(toBeInserted, node);
260 this.selectNode(selnode, false);
261 this.updateToolbar();
262 }
263 break;
264 case 1:
265 if (toBeInserted.nodeType == 11) selnode = selnode.lastChild;
266 node = node.insertBefore(toBeInserted, node.childNodes[pos]);
267 this.selectNode(selnode, false);
268 this.updateToolbar();
269 break;
270 }
271 };
272
273 /*
274 * Insert HTML source code at the current position.
275 * Delete the current selection, if any.
276 */
277 HTMLArea.prototype.insertHTML = function(html) {
278 this.focusEditor();
279 var fragment = this._doc.createDocumentFragment();
280 var div = this._doc.createElement("div");
281 div.innerHTML = html;
282 while (div.firstChild) {fragment.appendChild(div.firstChild);}
283 this.insertNodeAtSelection(fragment);
284 };
285
286 /***************************************************
287 * EVENTS HANDLERS
288 ***************************************************/
289
290 /*
291 * TYPO3 hidden tab and inline event listener (gets event calls)
292 */
293 HTMLArea.NestedListener = function (editor,nestedObj,noOpenCloseAction) {
294 return (function(ev) {
295 if(!ev) var ev = window.event;
296 HTMLArea.NestedHandler(ev,editor,nestedObj,noOpenCloseAction);
297 });
298 };
299
300 /*
301 * TYPO3 hidden tab and inline event handler (performs actions on event calls)
302 */
303 HTMLArea.NestedHandler = function(ev,editor,nestedObj,noOpenCloseAction) {
304 window.setTimeout(function() {
305 var target = (ev.target) ? ev.target : ev.srcElement;
306 if(target == nestedObj && editor._editMode == "wysiwyg" && ev.attrName=='style' && (target.style.display == '' || target.style.display == 'block')) {
307 // Check if all affected nested elements are displayed (style.display!='none'):
308 if (HTMLArea.allElementsAreDisplayed(editor.nested.sorted)) {
309 window.setTimeout(function() {
310 try {
311 editor._doc.designMode = "on";
312 if (editor.config.sizeIncludesToolbar && editor._initialToolbarOffsetHeight != editor._toolbar.offsetHeight) {
313 editor.sizeIframe(-2);
314 }
315 } catch(e) {
316 // If an event of a parent tab ("nested tabs") is triggered, the following lines should not be
317 // processed, because this causes some trouble on all event handlers...
318 if (!noOpenCloseAction) {
319 editor._doc.open();
320 editor._doc.close();
321 }
322 editor.initIframe();
323 }
324 }, 50);
325 }
326 HTMLArea._stopEvent(ev);
327 }
328 }, 50);
329 };
330
331 /*
332 * Handle statusbar element events
333 */
334 HTMLArea.statusBarHandler = function (ev) {
335 if(!ev) var ev = window.event;
336 var target = (ev.target) ? ev.target : ev.srcElement;
337 var editor = target.editor;
338 target.blur();
339 editor.selectNode(target.el);
340 editor.updateToolbar(true);
341 switch (ev.type) {
342 case "click" :
343 case "mousedown" :
344 HTMLArea._stopEvent(ev);
345 return false;
346 case "contextmenu" :
347 return editor.plugins["ContextMenu"] ? editor.plugins["ContextMenu"].instance.popupMenu(ev,target.el) : false;
348 }
349 };
350
351 /*
352 * Paste exception handler
353 */
354 HTMLArea.prototype._mozillaPasteException = function(cmdID, UI, param) {
355 // Mozilla lauches an exception, but can paste anyway on ctrl-V
356 // UI is false on keyboard shortcut, and undefined on button click
357 if(typeof(UI) != "undefined") {
358 this._doc.execCommand(cmdID, UI, param);
359 if (cmdID == "Paste" && this.config.killWordOnPaste) HTMLArea._wordClean(this._doc.body);
360 } else if (this.config.enableMozillaExtension) {
361 if (confirm(HTMLArea.I18N.msg["Allow-Clipboard-Helper-Extension"])) {
362 if (InstallTrigger.enabled()) {
363 HTMLArea._mozillaXpi = new Object();
364 HTMLArea._mozillaXpi["AllowClipboard Helper"] = _editor_mozAllowClipboard_url;
365 InstallTrigger.install(HTMLArea._mozillaXpi,HTMLArea._mozillaInstallCallback);
366 } else {
367 alert(HTMLArea.I18N.msg["Mozilla-Org-Install-Not-Enabled"]);
368 HTMLArea._appendToLog("WARNING [HTMLArea::execCommand]: Mozilla install was not enabled.");
369 return;
370 }
371 }
372 } else if (confirm(HTMLArea.I18N.msg["Moz-Clipboard"])) {
373 window.open("http://mozilla.org/editor/midasdemo/securityprefs.html");
374 }
375 }
376
377 HTMLArea._mozillaInstallCallback = function(url,returnCode) {
378 if (returnCode == 0) {
379 if (HTMLArea._mozillaXpi["TYPO3 htmlArea RTE Preferences"]) alert(HTMLArea.I18N.msg["Moz-Extension-Success"]);
380 else alert(HTMLArea.I18N.msg["Allow-Clipboard-Helper-Extension-Success"]);
381 return;
382 } else {
383 alert(HTMLArea.I18N.msg["Moz-Extension-Failure"]);
384 HTMLArea._appendToLog("WARNING [HTMLArea::execCommand]: Mozilla install return code was: " + returnCode + ".");
385 return;
386 }
387 };
388
389 /*
390 * Backspace event handler
391 */
392 HTMLArea.prototype._checkBackspace = function() {
393 var self = this;
394 self.focusEditor();
395 var sel = self._getSelection();
396 var range = self._createRange(sel);
397 var SC = range.startContainer;
398 var SO = range.startOffset;
399 var EC = range.endContainer;
400 var EO = range.endOffset;
401 var newr = SC.nextSibling;
402 while (SC.nodeType == 3 || /^a$/i.test(SC.tagName)) SC = SC.parentNode;
403 if (!self.config.disableEnterParagraphs && /^td$/i.test(SC.parentNode.tagName) && SC.parentNode.firstChild == SC && SO == 0 && range.collapsed) return true;
404 window.setTimeout(function() {
405 // Remove br tag inserted by Mozilla
406 if (!self.config.disableEnterParagraphs && (/^p$/i.test(SC.tagName) || !/\S/.test(SC.tagName)) && SO == 0) {
407 if (SC.firstChild && /^br$/i.test(SC.firstChild.tagName)) {
408 HTMLArea.removeFromParent(SC.firstChild);
409 return true;
410 }
411 }
412 if (!/\S/.test(SC.tagName)) {
413 var p = document.createElement("p");
414 while (SC.firstChild) p.appendChild(SC.firstChild);
415 SC.parentNode.insertBefore(p, SC);
416 HTMLArea.removeFromParent(SC);
417 var r = range.cloneRange();
418 r.setStartBefore(newr);
419 r.setEndAfter(newr);
420 r.extractContents();
421 if(HTMLArea.is_safari) {
422 sel.empty();
423 sel.setBaseAndExtent(r.startContainer,r.startOffset,r.endContainer,r.endOffset);
424 } else {
425 sel.removeAllRanges();
426 sel.addRange(r);
427 }
428 return true;
429 }
430 },10);
431 return false;
432 };
433
434 /*
435 * Enter event handler
436 */
437 HTMLArea.prototype._checkInsertP = function() {
438 this.focusEditor();
439 var i, SC, left, right, r2,
440 sel = this._getSelection(),
441 r = this._createRange(sel),
442 p = this.getAllAncestors(),
443 block = null,
444 a = null,
445 doc = this._doc,
446 body = doc.body;
447
448 for (i = 0; i < p.length; ++i) {
449 if (HTMLArea.isBlockElement(p[i]) && !/html|body|table|tbody|tr/i.test(p[i].tagName)) {
450 block = p[i];
451 break;
452 }
453 }
454 if (!r.collapsed) r.deleteContents();
455 if (HTMLArea.is_safari) sel.empty();
456 else sel.removeAllRanges();
457 SC = r.startContainer;
458 if (HTMLArea.is_opera && SC.nodeType == 3 && SC.data.length == 0) SC = HTMLArea.getPrevNode(SC);
459 if (!block || /^(td|div)$/i.test(block.tagName)) {
460 left = SC;
461 for (i = SC; i && !HTMLArea.isBlockElement(i); i = HTMLArea.getPrevNode(i)) { left = i; }
462 right = SC;
463 if (HTMLArea.is_opera && right.nodeType == 3 && right.data.length == 0) right = HTMLArea.getNextNode(right);
464 if(left != body && right != body && !(block && left == block ) && !(block && right == block )) {
465 r2 = r.cloneRange();
466 if (HTMLArea.is_opera) r2.setStart(left,0);
467 else r2.setStartBefore(left);
468 r2.surroundContents(block = doc.createElement('p'));
469 if (!/\S/.test(HTMLArea.getInnerText(block))) {
470 // Remove any anchor created empty
471 a = block.lastChild;
472 if (a && /^a$/i.test(a.tagName) && !/\S/.test(a.innerHTML)) HTMLArea.removeFromParent(a);
473 block.appendChild(doc.createElement('br'));
474 }
475 block.normalize();
476 if (HTMLArea.is_opera) {
477 SC = HTMLArea.getNextNode(block);
478 for (i = SC; i && !HTMLArea.isBlockElement(i); i = HTMLArea.getNextNode(i)) { right = i; }
479 r.setStart(SC, 0);
480 r.setEnd(right, right.length);
481 } else {
482 r.setEndAfter(right);
483 }
484 r.surroundContents(block = doc.createElement('p'));
485 // Remove any anchor created empty
486 a = block.previousSibling;
487 if (a && /^a$/i.test(a.tagName) && !/\S/.test(a.innerHTML)) HTMLArea.removeFromParent(a);
488 if (!/\S/.test(HTMLArea.getInnerText(block))) {
489 // Remove any anchor created empty
490 a = block.lastChild;
491 if (a && /^a$/i.test(a.tagName) && !/\S/.test(a.innerHTML)) HTMLArea.removeFromParent(a);
492 block.appendChild(doc.createElement('br'));
493 }
494 block.normalize();
495 } else {
496 if (!block) block = body;
497 r = doc.createRange();
498 r.setStart(block, 0);
499 r.setEnd(block, 0);
500 r.insertNode(block = doc.createElement('p'));
501 block.appendChild(doc.createElement('br'));
502 }
503 if (HTMLArea.is_opera) r.selectNode(block);
504 else r.selectNodeContents(block);
505 } else {
506 if (!HTMLArea.is_opera || /\S/.test(HTMLArea.getInnerText(block))) r.setEndAfter(block);
507 var df = r.extractContents(), left_empty = false;
508 if(!/\S/.test(HTMLArea.getInnerText(block))) {
509 block.innerHTML = "<br />";
510 left_empty = true;
511 }
512 p = df.firstChild;
513 if (p) {
514 if(!/\S/.test(HTMLArea.getInnerText(p))) {
515 if (/^h[1-6]$/i.test(p.tagName)) p = this.convertNode(p,"p");
516 p.innerHTML = "<br />";
517 }
518 if(/^li$/i.test(p.tagName) && left_empty && !block.nextSibling) {
519 left = block.parentNode;
520 left.removeChild(block);
521 r.setEndAfter(left);
522 r.collapse(false);
523 p = this.convertNode(p, /^li$/i.test(left.parentNode.tagName) ? "br" : "p");
524 }
525 r.insertNode(df);
526 // Remove any anchor created empty
527 var a = p.previousSibling.lastChild;
528 if (a && /^a$/i.test(a.tagName) && !/\S/.test(a.innerHTML)) HTMLArea.removeFromParent(a);
529 if (HTMLArea.is_opera) r.selectNode(p);
530 else r.selectNodeContents(p);
531 }
532 }
533 r.collapse(true);
534 if (HTMLArea.is_safari) sel.setBaseAndExtent(r.startContainer,r.startOffset,r.endContainer,r.endOffset);
535 else sel.addRange(r);
536 this.scrollToCaret();
537 };
538
539 /*
540 * Detect emails and urls as they are typed in Mozilla
541 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
542 */
543 HTMLArea.prototype._detectURL = function(ev) {
544 var editor = this;
545 var s = this._getSelection();
546 var autoWrap = function (textNode, tag) {
547 var rightText = textNode.nextSibling;
548 if (typeof(tag) == 'string') tag = editor._doc.createElement(tag);
549 var a = textNode.parentNode.insertBefore(tag, rightText);
550 HTMLArea.removeFromParent(textNode);
551 a.appendChild(textNode);
552 rightText.data += " ";
553 s.collapse(rightText, rightText.data.length);
554 HTMLArea._stopEvent(ev);
555
556 editor._unLink = function() {
557 var t = a.firstChild;
558 a.removeChild(t);
559 a.parentNode.insertBefore(t, a);
560 HTMLArea.removeFromParent(a);
561 t.parentNode.normalize();
562 editor._unLink = null;
563 editor._unlinkOnUndo = false;
564 };
565
566 editor._unlinkOnUndo = true;
567 return a;
568 };
569
570 switch(ev.which) {
571 // Space or Enter or >, see if the text just typed looks like a URL, or email address and link it accordingly
572 case 13: // Enter
573 if(ev.shiftKey || editor.config.disableEnterParagraphs) break;
574 //Space
575 case 32:
576 if(s && s.isCollapsed && s.anchorNode.nodeType == 3 && s.anchorNode.data.length > 3 && s.anchorNode.data.indexOf('.') >= 0) {
577 var midStart = s.anchorNode.data.substring(0,s.anchorOffset).search(/[a-zA-Z0-9]+\S{3,}$/);
578 if(midStart == -1) break;
579 if(this._getFirstAncestor(s, 'a')) break; // already in an anchor
580 var matchData = s.anchorNode.data.substring(0,s.anchorOffset).replace(/^.*?(\S*)$/, '$1');
581 if (matchData.indexOf('@') != -1) {
582 var m = matchData.match(HTMLArea.RE_email);
583 if(m) {
584 var leftText = s.anchorNode;
585 var rightText = leftText.splitText(s.anchorOffset);
586 var midText = leftText.splitText(midStart);
587 var midEnd = midText.data.search(/[^a-zA-Z0-9\.@_\-]/);
588 if (midEnd != -1) var endText = midText.splitText(midEnd);
589 autoWrap(midText, 'a').href = 'mailto:' + m[0];
590 break;
591 }
592 }
593 var m = matchData.match(HTMLArea.RE_url);
594 if(m) {
595 var leftText = s.anchorNode;
596 var rightText = leftText.splitText(s.anchorOffset);
597 var midText = leftText.splitText(midStart);
598 var midEnd = midText.data.search(/[^a-zA-Z0-9\._\-\/\&\?=:@]/);
599 if (midEnd != -1) var endText = midText.splitText(midEnd);
600 autoWrap(midText, 'a').href = (m[1] ? m[1] : 'http://') + m[2];
601 break;
602 }
603 }
604 break;
605 default:
606 if(ev.keyCode == 27 || (editor._unlinkOnUndo && ev.ctrlKey && ev.which == 122) ) {
607 if(this._unLink) {
608 this._unLink();
609 HTMLArea._stopEvent(ev);
610 }
611 break;
612 } else if(ev.which || ev.keyCode == 8 || ev.keyCode == 46) {
613 this._unlinkOnUndo = false;
614 if(s.anchorNode && s.anchorNode.nodeType == 3) {
615 // See if we might be changing a link
616 var a = this._getFirstAncestor(s, 'a');
617 if(!a) break; // not an anchor
618 if(!a._updateAnchTimeout) {
619 if(s.anchorNode.data.match(HTMLArea.RE_email) && (a.href.match('mailto:' + s.anchorNode.data.trim()))) {
620 var textNode = s.anchorNode;
621 var fn = function() {
622 a.href = 'mailto:' + textNode.data.trim();
623 a._updateAnchTimeout = setTimeout(fn, 250);
624 };
625 a._updateAnchTimeout = setTimeout(fn, 250);
626 break;
627 }
628 var m = s.anchorNode.data.match(HTMLArea.RE_url);
629 if(m && a.href.match(s.anchorNode.data.trim())) {
630 var textNode = s.anchorNode;
631 var fn = function() {
632 var m = textNode.data.match(HTMLArea.RE_url);
633 a.href = (m[1] ? m[1] : 'http://') + m[2];
634 a._updateAnchTimeout = setTimeout(fn, 250);
635 }
636 a._updateAnchTimeout = setTimeout(fn, 250);
637 }
638 }
639 }
640 }
641 break;
642 }
643 };