3b1bbf96010dbf2790037bbea4e1c0b6bfd39ec5
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / DOM / HTMLArea.DOM.Selection.js
1 /***************************************************
2 * HTMLArea.DOM.Selection: Selection object
3 ***************************************************/
4 HTMLArea.DOM.Selection = function (config) {
5 };
6 HTMLArea.DOM.Selection = Ext.extend(HTMLArea.DOM.Selection, {
7 /*
8 * Reference to the editor MUST be set in config
9 */
10 editor: null,
11 /*
12 * Reference to the editor document
13 */
14 document: null,
15 /*
16 * Reference to the editor iframe window
17 */
18 window: null,
19 /*
20 * The current selection
21 */
22 selection: null,
23 /*
24 * HTMLArea.DOM.Selection constructor
25 */
26 constructor: function (config) {
27 // Apply config
28 Ext.apply(this, config);
29 // Initialize references
30 this.document = this.editor.document;
31 this.window = this.editor.iframe.getEl().dom.contentWindow;
32 // Set current selection
33 this.get();
34 },
35 /*
36 * Get the current selection object
37 *
38 * @return object this
39 */
40 get: function () {
41 this.editor.focus();
42 this.selection = this.window.getSelection ? this.window.getSelection() : this.document.selection;
43 return this;
44 },
45 /*
46 * Get the type of the current selection
47 *
48 * @return string the type of selection ("None", "Text" or "Control")
49 */
50 getType: function() {
51 // By default set the type to "Text"
52 var type = 'Text';
53 this.get();
54 if (!Ext.isEmpty(this.selection)) {
55 if (Ext.isFunction(this.selection.getRangeAt)) {
56 // Check if the current selection is a Control
57 if (this.selection && this.selection.rangeCount == 1) {
58 var range = this.selection.getRangeAt(0);
59 if (range.startContainer.nodeType === HTMLArea.DOM.ELEMENT_NODE) {
60 if (
61 // Gecko
62 (range.startContainer == range.endContainer && (range.endOffset - range.startOffset) == 1) ||
63 // Opera and WebKit
64 (range.endContainer.nodeType === HTMLArea.DOM.TEXT_NODE && range.endOffset == 0 && range.startContainer.childNodes[range.startOffset].nextSibling == range.endContainer)
65 ) {
66 if (/^(img|hr|li|table|tr|td|embed|object|ol|ul|dl)$/i.test(range.startContainer.childNodes[range.startOffset].nodeName)) {
67 type = 'Control';
68 }
69 }
70 }
71 }
72 } else {
73 // IE8 or IE7
74 type = this.selection.type;
75 }
76 }
77 return type;
78 },
79 /*
80 * Empty the current selection
81 *
82 * @return object this
83 */
84 empty: function () {
85 this.get();
86 if (!Ext.isEmpty(this.selection)) {
87 if (Ext.isFunction(this.selection.removeAllRanges)) {
88 this.selection.removeAllRanges();
89 } else {
90 // IE8, IE7 or old version of WebKit
91 this.selection.empty();
92 }
93 if (Ext.isOpera) {
94 this.editor.focus();
95 }
96 }
97 return this;
98 },
99 /*
100 * Determine whether the current selection is empty or not
101 *
102 * @return boolean true, if the selection is empty
103 */
104 isEmpty: function () {
105 var isEmpty = true;
106 this.get();
107 if (!Ext.isEmpty(this.selection)) {
108 if (HTMLArea.isIEBeforeIE9) {
109 switch (this.selection.type) {
110 case 'None':
111 isEmpty = true;
112 break;
113 case 'Text':
114 isEmpty = !this.createRange().text;
115 break;
116 default:
117 isEmpty = !this.createRange().htmlText;
118 break;
119 }
120 } else {
121 isEmpty = this.selection.isCollapsed;
122 }
123 }
124 return isEmpty;
125 },
126 /*
127 * Get a range corresponding to the current selection
128 *
129 * @return object the range of the selection
130 */
131 createRange: function () {
132 var range;
133 this.get();
134 if (HTMLArea.isIEBeforeIE9) {
135 range = this.selection.createRange();
136 } else {
137 if (Ext.isEmpty(this.selection)) {
138 range = this.document.createRange();
139 } else {
140 // Older versions of WebKit did not support getRangeAt
141 if (Ext.isWebKit && !Ext.isFunction(this.selection.getRangeAt)) {
142 range = this.document.createRange();
143 if (this.selection.baseNode == null) {
144 range.setStart(this.document.body, 0);
145 range.setEnd(this.document.body, 0);
146 } else {
147 range.setStart(this.selection.baseNode, this.selection.baseOffset);
148 range.setEnd(this.selection.extentNode, this.selection.extentOffset);
149 if (range.collapsed != this.selection.isCollapsed) {
150 range.setStart(this.selection.extentNode, this.selection.extentOffset);
151 range.setEnd(this.selection.baseNode, this.selection.baseOffset);
152 }
153 }
154 } else {
155 try {
156 range = this.selection.getRangeAt(0);
157 } catch (e) {
158 range = this.document.createRange();
159 }
160 }
161 }
162 }
163 return range;
164 },
165 /*
166 * Return the ranges of the selection
167 *
168 * @return array array of ranges
169 */
170 getRanges: function () {
171 this.get();
172 var ranges = [];
173 // Older versions of WebKit, IE7 and IE8 did not support getRangeAt
174 if (!Ext.isEmpty(this.selection) && Ext.isFunction(this.selection.getRangeAt)) {
175 for (var i = this.selection.rangeCount; --i >= 0;) {
176 ranges.push(this.selection.getRangeAt(i));
177 }
178 } else {
179 ranges.push(this.createRange());
180 }
181 return ranges;
182 },
183 /*
184 * Add a range to the selection
185 *
186 * @param object range: the range to be added to the selection
187 *
188 * @return object this
189 */
190 addRange: function (range) {
191 this.get();
192 if (!Ext.isEmpty(this.selection)) {
193 if (Ext.isFunction(this.selection.addRange)) {
194 this.selection.addRange(range);
195 } else if (Ext.isWebKit) {
196 this.selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
197 }
198 }
199 return this;
200 },
201 /*
202 * Set the ranges of the selection
203 *
204 * @param array ranges: array of range to be added to the selection
205 *
206 * @return object this
207 */
208 setRanges: function (ranges) {
209 this.get();
210 this.empty();
211 for (var i = ranges.length; --i >= 0;) {
212 this.addRange(ranges[i]);
213 }
214 return this;
215 },
216 /*
217 * Set the selection to a given range
218 *
219 * @param object range: the range to be selected
220 *
221 * @return object this
222 */
223 selectRange: function (range) {
224 this.get();
225 if (!Ext.isEmpty(this.selection)) {
226 if (Ext.isFunction(this.selection.getRangeAt)) {
227 this.empty().addRange(range);
228 } else {
229 // IE8 or IE7
230 range.select();
231 }
232 }
233 return this;
234 },
235 /*
236 * Set the selection to a given node
237 *
238 * @param object node: the node to be selected
239 * @param boolean endPoint: collapse the selection at the start point (true) or end point (false) of the node
240 *
241 * @return object this
242 */
243 selectNode: function (node, endPoint) {
244 this.get();
245 if (!Ext.isEmpty(this.selection)) {
246 if (HTMLArea.isIEBeforeIE9) {
247 // IE8/7/6 cannot set this type of selection
248 this.selectNodeContents(node, endPoint);
249 } else if (Ext.isWebKit && /^(img)$/i.test(node.nodeName)) {
250 this.selection.setBaseAndExtent(node, 0, node, 1);
251 } else {
252 var range = this.document.createRange();
253 if (node.nodeType === HTMLArea.DOM.ELEMENT_NODE && /^(body)$/i.test(node.nodeName)) {
254 if (Ext.isWebKit) {
255 range.setStart(node, 0);
256 range.setEnd(node, node.childNodes.length);
257 } else {
258 range.selectNodeContents(node);
259 }
260 } else {
261 range.selectNode(node);
262 }
263 if (typeof endPoint !== 'undefined') {
264 range.collapse(endPoint);
265 }
266 this.selectRange(range);
267 }
268 }
269 return this;
270 },
271 /*
272 * Set the selection to the inner contents of a given node
273 *
274 * @param object node: the node of which the contents are to be selected
275 * @param boolean endPoint: collapse the selection at the start point (true) or end point (false)
276 *
277 * @return object this
278 */
279 selectNodeContents: function (node, endPoint) {
280 var range;
281 this.get();
282 if (!Ext.isEmpty(this.selection)) {
283 if (HTMLArea.isIEBeforeIE9) {
284 range = this.document.body.createTextRange();
285 range.moveToElementText(node);
286 } else {
287 range = this.document.createRange();
288 if (Ext.isWebKit) {
289 range.setStart(node, 0);
290 if (node.nodeType === HTMLArea.DOM.TEXT_NODE || node.nodeType === HTMLArea.DOM.COMMENT_NODE || node.nodeType === HTMLArea.DOM.CDATA_SECTION_NODE) {
291 range.setEnd(node, node.textContent.length);
292 } else {
293 range.setEnd(node, node.childNodes.length);
294 }
295 } else {
296 range.selectNodeContents(node);
297 }
298 }
299 if (typeof endPoint !== 'undefined') {
300 range.collapse(endPoint);
301 }
302 this.selectRange(range);
303 }
304 return this;
305 },
306 /*
307 * Get the deepest node that contains both endpoints of the current selection.
308 *
309 * @return object the deepest node that contains both endpoints of the current selection.
310 */
311 getParentElement: function () {
312 var parentElement,
313 range;
314 this.get();
315 if (HTMLArea.isIEBeforeIE9) {
316 range = this.createRange();
317 switch (this.selection.type) {
318 case 'Text':
319 case 'None':
320 parentElement = range.parentElement();
321 if (/^(form)$/i.test(parentElement.nodeName)) {
322 parentElement = this.document.body;
323 } else if (/^(li)$/i.test(parentElement.nodeName) && range.htmlText.replace(/\s/g, '') == parentElement.parentNode.outerHTML.replace(/\s/g, '')) {
324 parentElement = parentElement.parentNode;
325 }
326 break;
327 case 'Control':
328 parentElement = range.item(0);
329 break;
330 default:
331 parentElement = this.document.body;
332 break;
333 }
334 } else {
335 if (this.getType() === 'Control') {
336 parentElement = this.getElement();
337 } else {
338 range = this.createRange();
339 parentElement = range.commonAncestorContainer;
340 // Firefox 3 may report the document as commonAncestorContainer
341 if (parentElement.nodeType === HTMLArea.DOM.DOCUMENT_NODE) {
342 parentElement = this.document.body;
343 } else {
344 while (parentElement && parentElement.nodeType === HTMLArea.DOM.TEXT_NODE) {
345 parentElement = parentElement.parentNode;
346 }
347 }
348 }
349 }
350 return parentElement;
351 },
352 /*
353 * Get the selected element (if any), in the case that a single element (object like and image or a table) is selected
354 * In IE language, we have a range of type 'Control'
355 *
356 * @return object the selected node
357 */
358 getElement: function () {
359 var element = null;
360 this.get();
361 if (!Ext.isEmpty(this.selection) && this.selection.anchorNode && this.selection.anchorNode.nodeType === HTMLArea.DOM.ELEMENT_NODE && this.getType() == 'Control') {
362 element = this.selection.anchorNode.childNodes[this.selection.anchorOffset];
363 // For Safari, the anchor node for a control selection is the control itself
364 if (!element) {
365 element = this.selection.anchorNode;
366 } else if (element.nodeType !== HTMLArea.DOM.ELEMENT_NODE) {
367 element = null;
368 }
369 }
370 return element;
371 },
372 /*
373 * Get the deepest element ancestor of the selection that is of one of the specified types
374 *
375 * @param array types: an array of nodeNames
376 *
377 * @return object the found ancestor of one of the given types or null
378 */
379 getFirstAncestorOfType: function (types) {
380 var node = this.getParentElement();
381 return HTMLArea.DOM.getFirstAncestorOfType(node, types);
382 },
383 /*
384 * Get an array with all the ancestor nodes of the current selection
385 *
386 * @return array the ancestor nodes
387 */
388 getAllAncestors: function () {
389 var parent = this.getParentElement(),
390 ancestors = [];
391 while (parent && parent.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
392 ancestors.push(parent);
393 parent = parent.parentNode;
394 }
395 ancestors.push(this.document.body);
396 return ancestors;
397 },
398 /*
399 * Get an array with the parent elements of a multiple selection
400 *
401 * @return array the selected elements
402 */
403 getElements: function () {
404 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null,
405 elements = [];
406 if (statusBarSelection) {
407 elements.push(statusBarSelection);
408 } else {
409 var ranges = this.getRanges();
410 parent;
411 if (ranges.length > 1) {
412 for (var i = ranges.length; --i >= 0;) {
413 parent = range[i].commonAncestorContainer;
414 // Firefox 3 may report the document as commonAncestorContainer
415 if (parent.nodeType === HTMLArea.DOM.DOCUMENT_NODE) {
416 parent = this.document.body;
417 } else {
418 while (parent && parent.nodeType === HTMLArea.DOM.TEXT_NODE) {
419 parent = parent.parentNode;
420 }
421 }
422 elements.push(parent);
423 }
424 } else {
425 elements.push(this.getParentElement());
426 }
427 }
428 return elements;
429 },
430 /*
431 * Get the node whose contents are currently fully selected
432 *
433 * @return object the fully selected node, if any, null otherwise
434 */
435 getFullySelectedNode: function () {
436 var node = null,
437 isFullySelected = false;
438 this.get();
439 if (!this.isEmpty()) {
440 var type = this.getType();
441 var range = this.createRange();
442 var ancestors = this.getAllAncestors();
443 for (var i = 0, n = ancestors.length; i < n; i++) {
444 var ancestor = ancestors[i];
445 if (HTMLArea.isIEBeforeIE9) {
446 isFullySelected = (type !== 'Control' && ancestor.innerText == range.text) || (type === 'Control' && ancestor.innerText == range.item(0).text);
447 } else {
448 isFullySelected = (ancestor.textContent == range.toString());
449 }
450 if (isFullySelected) {
451 node = ancestor;
452 break;
453 }
454 }
455 // Working around bug with WebKit selection
456 if (Ext.isWebKit && !isFullySelected) {
457 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
458 if (statusBarSelection && statusBarSelection.textContent == range.toString()) {
459 isFullySelected = true;
460 node = statusBarSelection;
461 }
462 }
463 }
464 return node;
465 },
466 /*
467 * Get the block elements containing the start and the end points of the selection
468 *
469 * @return object object with properties start and end set to the end blocks of the selection
470 */
471 getEndBlocks: function () {
472 var range = this.createRange(),
473 parentStart,
474 parentEnd;
475 if (HTMLArea.isIEBeforeIE9) {
476 if (this.getType() === 'Control') {
477 parentStart = range.item(0);
478 parentEnd = parentStart;
479 } else {
480 var rangeEnd = range.duplicate();
481 range.collapse(true);
482 parentStart = range.parentElement();
483 rangeEnd.collapse(false);
484 parentEnd = rangeEnd.parentElement();
485 }
486 } else {
487 parentStart = range.startContainer;
488 if (/^(body)$/i.test(parentStart.nodeName)) {
489 parentStart = parentStart.firstChild;
490 }
491 parentEnd = range.endContainer;
492 if (/^(body)$/i.test(parentEnd.nodeName)) {
493 parentEnd = parentEnd.lastChild;
494 }
495 }
496 while (parentStart && !HTMLArea.DOM.isBlockElement(parentStart)) {
497 parentStart = parentStart.parentNode;
498 }
499 while (parentEnd && !HTMLArea.DOM.isBlockElement(parentEnd)) {
500 parentEnd = parentEnd.parentNode;
501 }
502 return {
503 start: parentStart,
504 end: parentEnd
505 };
506 },
507 /*
508 * Determine whether the end poins of the current selection are within the same block
509 *
510 * @return boolean true if the end points of the current selection are in the same block
511 */
512 endPointsInSameBlock: function() {
513 var endPointsInSameBlock = true;
514 this.get();
515 if (!this.isEmpty()) {
516 var parent = this.getParentElement();
517 var endBlocks = this.getEndBlocks();
518 endPointsInSameBlock = (endBlocks.start === endBlocks.end && !/^(table|thead|tbody|tfoot|tr)$/i.test(parent.nodeName));
519 }
520 return endPointsInSameBlock;
521 },
522 /*
523 * Retrieve the HTML contents of the current selection
524 *
525 * @return string HTML text of the current selection
526 */
527 getHtml: function () {
528 var range = this.createRange(),
529 html = '';
530 if (HTMLArea.isIEBeforeIE9) {
531 if (this.getType() === 'Control') {
532 // We have a controlRange collection
533 var bodyRange = this.document.body.createTextRange();
534 bodyRange.moveToElementText(range(0));
535 html = bodyRange.htmlText;
536 } else {
537 html = range.htmlText;
538 }
539 } else if (!range.collapsed) {
540 var cloneContents = range.cloneContents();
541 if (!cloneContents) {
542 cloneContents = this.document.createDocumentFragment();
543 }
544 html = this.editor.iframe.htmlRenderer.render(cloneContents, false);
545 }
546 return html;
547 },
548 /*
549 * Insert a node at the current position
550 * Delete the current selection, if any.
551 * Split the text node, if needed.
552 *
553 * @param object toBeInserted: the node to be inserted
554 *
555 * @return object this
556 */
557 insertNode: function (toBeInserted) {
558 if (HTMLArea.isIEBeforeIE9) {
559 this.insertHtml(toBeInserted.outerHTML);
560 } else {
561 var range = this.createRange();
562 range.deleteContents();
563 toBeSelected = (toBeInserted.nodeType === HTMLArea.DOM.DOCUMENT_FRAGMENT_NODE) ? toBeInserted.lastChild : toBeInserted;
564 range.insertNode(toBeInserted);
565 this.selectNodeContents(toBeSelected, false);
566 }
567 return this;
568 },
569 /*
570 * Insert HTML source code at the current position
571 * Delete the current selection, if any.
572 *
573 * @param string html: the HTML source code
574 *
575 * @return object this
576 */
577 insertHtml: function (html) {
578 if (HTMLArea.isIEBeforeIE9) {
579 this.get();
580 if (this.getType() === 'Control') {
581 this.selection.clear();
582 this.get();
583 }
584 var range = this.createRange();
585 range.pasteHTML(html);
586 } else {
587 this.editor.focus();
588 var fragment = this.document.createDocumentFragment();
589 var div = this.document.createElement('div');
590 div.innerHTML = html;
591 while (div.firstChild) {
592 fragment.appendChild(div.firstChild);
593 }
594 this.insertNode(fragment);
595 }
596 return this;
597 },
598 /*
599 * Surround the selection with an element specified by its start and end tags
600 * Delete the selection, if any.
601 *
602 * @param string startTag: the start tag
603 * @param string endTag: the end tag
604 *
605 * @return void
606 */
607 surroundHtml: function (startTag, endTag) {
608 this.insertHtml(startTag + this.getHtml().replace(HTMLArea.DOM.RE_bodyTag, '') + endTag);
609 },
610 /*
611 * Execute some native execCommand command on the current selection
612 *
613 * @param string cmdID: the command name or id
614 * @param object UI:
615 * @param object param:
616 *
617 * @return boolean false
618 */
619 execCommand: function (cmdID, UI, param) {
620 var success = true;
621 this.editor.focus();
622 try {
623 this.document.execCommand(cmdID, UI, param);
624 } catch (e) {
625 success = false;
626 this.editor.appendToLog('HTMLArea.DOM.Selection', 'execCommand', e + ' by execCommand(' + cmdID + ')', 'error');
627 }
628 this.editor.updateToolbar();
629 return success;
630 },
631 /*
632 * Handle backspace event on the current selection
633 *
634 * @return boolean true to stop the event and cancel the default action
635 */
636 handleBackSpace: function () {
637 var range = this.createRange();
638 if (HTMLArea.isIEBeforeIE9) {
639 if (this.getType() === 'Control') {
640 // Deleting or backspacing on a control selection : delete the element
641 var element = this.getParentElement();
642 var parent = element.parentNode;
643 parent.removeChild(el);
644 return true;
645 } else if (this.isEmpty()) {
646 // Check if deleting an empty block with a table as next sibling
647 var element = this.getParentElement();
648 if (!element.innerHTML && HTMLArea.DOM.isBlockElement(element) && element.nextSibling && /^table$/i.test(element.nextSibling.nodeName)) {
649 var previous = element.previousSibling;
650 if (!previous) {
651 this.selectNodeContents(element.nextSibling.rows[0].cells[0], true);
652 } else if (/^table$/i.test(previous.nodeName)) {
653 this.selectNodeContents(previous.rows[previous.rows.length-1].cells[previous.rows[previous.rows.length-1].cells.length-1], false);
654 } else {
655 range.moveStart('character', -1);
656 range.collapse(true);
657 range.select();
658 }
659 el.parentNode.removeChild(element);
660 return true;
661 }
662 } else {
663 // Backspacing into a link
664 var range2 = range.duplicate();
665 range2.moveStart('character', -1);
666 var a = range2.parentElement();
667 if (a != range.parentElement() && /^a$/i.test(a.nodeName)) {
668 range2.collapse(true);
669 range2.moveEnd('character', 1);
670 range2.pasteHTML('');
671 range2.select();
672 return true;
673 }
674 return false;
675 }
676 } else {
677 var self = this;
678 window.setTimeout(function() {
679 var range = self.createRange();
680 var startContainer = range.startContainer;
681 var startOffset = range.startOffset;
682 // If the selection is collapsed...
683 if (self.isEmpty()) {
684 // ... and the cursor lies in a direct child of body...
685 if (/^(body)$/i.test(startContainer.nodeName)) {
686 var node = startContainer.childNodes[startOffset];
687 } else if (/^(body)$/i.test(startContainer.parentNode.nodeName)) {
688 var node = startContainer;
689 } else {
690 return false;
691 }
692 // ... which is a br or text node containing no non-whitespace character
693 if (/^(br|#text)$/i.test(node.nodeName) && !/\S/.test(node.textContent)) {
694 // Get a meaningful previous sibling in which to reposition de cursor
695 var previousSibling = node.previousSibling;
696 while (previousSibling && /^(br|#text)$/i.test(previousSibling.nodeName) && !/\S/.test(previousSibling.textContent)) {
697 previousSibling = previousSibling.previousSibling;
698 }
699 // If there is no meaningful previous sibling, the cursor is at the start of body
700 if (previousSibling) {
701 // Remove the node
702 HTMLArea.DOM.removeFromParent(node);
703 // Position the cursor
704 if (/^(ol|ul|dl)$/i.test(previousSibling.nodeName)) {
705 self.selectNodeContents(previousSibling.lastChild, false);
706 } else if (/^(table)$/i.test(previousSibling.nodeName)) {
707 self.selectNodeContents(previousSibling.rows[previousSibling.rows.length-1].cells[previousSibling.rows[previousSibling.rows.length-1].cells.length-1], false);
708 } else if (!/\S/.test(previousSibling.textContent) && previousSibling.firstChild) {
709 self.selectNode(previousSibling.firstChild, true);
710 } else {
711 self.selectNodeContents(previousSibling, false);
712 }
713 }
714 }
715 }
716 }, 10);
717 return false;
718 }
719 },
720 /*
721 * Detect emails and urls as they are typed in non-IE browsers
722 * Borrowed from Xinha (is not htmlArea) - http://xinha.gogo.co.nz/
723 *
724 * @param object event: the ExtJS key event
725 *
726 * @return void
727 */
728 detectURL: function (event) {
729 var ev = event.browserEvent;
730 var editor = this.editor;
731 var selection = this.get().selection;
732 if (!/^(a)$/i.test(this.getParentElement().nodeName)) {
733 var autoWrap = function (textNode, tag) {
734 var rightText = textNode.nextSibling;
735 if (typeof(tag) === 'string') {
736 tag = editor.document.createElement(tag);
737 }
738 var a = textNode.parentNode.insertBefore(tag, rightText);
739 HTMLArea.DOM.removeFromParent(textNode);
740 a.appendChild(textNode);
741 selection.collapse(rightText, 0);
742 rightText.parentNode.normalize();
743
744 editor.unLink = function() {
745 var t = a.firstChild;
746 a.removeChild(t);
747 a.parentNode.insertBefore(t, a);
748 HTMLArea.DOM.removeFromParent(a);
749 t.parentNode.normalize();
750 editor.unLink = null;
751 editor.unlinkOnUndo = false;
752 };
753
754 editor.unlinkOnUndo = true;
755 return a;
756 };
757 switch (ev.which) {
758 // Space or Enter or >, see if the text just typed looks like a URL, or email address and link it accordingly
759 case 13:
760 case 32:
761 if (selection && selection.isCollapsed && selection.anchorNode.nodeType === HTMLArea.DOM.TEXT_NODE && selection.anchorNode.data.length > 3 && selection.anchorNode.data.indexOf('.') >= 0) {
762 var midStart = selection.anchorNode.data.substring(0,selection.anchorOffset).search(/[a-zA-Z0-9]+\S{3,}$/);
763 if (midStart == -1) {
764 break;
765 }
766 if (this.getFirstAncestorOfType('a')) {
767 // already in an anchor
768 break;
769 }
770 var matchData = selection.anchorNode.data.substring(0,selection.anchorOffset).replace(/^.*?(\S*)$/, '$1');
771 if (matchData.indexOf('@') != -1) {
772 var m = matchData.match(HTMLArea.RE_email);
773 if (m) {
774 var leftText = selection.anchorNode;
775 var rightText = leftText.splitText(selection.anchorOffset);
776 var midText = leftText.splitText(midStart);
777 var midEnd = midText.data.search(/[^a-zA-Z0-9\.@_\-]/);
778 if (midEnd != -1) {
779 var endText = midText.splitText(midEnd);
780 }
781 autoWrap(midText, 'a').href = 'mailto:' + m[0];
782 break;
783 }
784 }
785 var m = matchData.match(HTMLArea.RE_url);
786 if (m) {
787 var leftText = selection.anchorNode;
788 var rightText = leftText.splitText(selection.anchorOffset);
789 var midText = leftText.splitText(midStart);
790 var midEnd = midText.data.search(/[^a-zA-Z0-9\._\-\/\&\?=:@]/);
791 if (midEnd != -1) {
792 var endText = midText.splitText(midEnd);
793 }
794 autoWrap(midText, 'a').href = (m[1] ? m[1] : 'http://') + m[3];
795 break;
796 }
797 }
798 break;
799 default:
800 if (ev.keyCode == 27 || (editor.unlinkOnUndo && ev.ctrlKey && ev.which == 122)) {
801 if (editor.unLink) {
802 editor.unLink();
803 event.stopEvent();
804 }
805 break;
806 } else if (ev.which || ev.keyCode == 8 || ev.keyCode == 46) {
807 editor.unlinkOnUndo = false;
808 if (selection.anchorNode && selection.anchorNode.nodeType === HTMLArea.DOM.TEXT_NODE) {
809 // See if we might be changing a link
810 var a = this.getFirstAncestorOfType('a');
811 if (!a) {
812 break;
813 }
814 if (!a.updateAnchorTimeout) {
815 if (selection.anchorNode.data.match(HTMLArea.RE_email) && (a.href.match('mailto:' + selection.anchorNode.data.trim()))) {
816 var textNode = selection.anchorNode;
817 var fn = function() {
818 a.href = 'mailto:' + textNode.data.trim();
819 a.updateAnchorTimeout = setTimeout(fn, 250);
820 };
821 a.updateAnchorTimeout = setTimeout(fn, 250);
822 break;
823 }
824 var m = selection.anchorNode.data.match(HTMLArea.RE_url);
825 if (m && a.href.match(selection.anchorNode.data.trim())) {
826 var textNode = selection.anchorNode;
827 var fn = function() {
828 var m = textNode.data.match(HTMLArea.RE_url);
829 a.href = (m[1] ? m[1] : 'http://') + m[3];
830 a.updateAnchorTimeout = setTimeout(fn, 250);
831 }
832 a.updateAnchorTimeout = setTimeout(fn, 250);
833 }
834 }
835 }
836 }
837 break;
838 }
839 }
840 },
841 /*
842 * Enter event handler
843 *
844 * @return boolean true to stop the event and cancel the default action
845 */
846 checkInsertParagraph: function() {
847 var editor = this.editor;
848 var left, right, rangeClone,
849 sel = this.get().selection,
850 range = this.createRange(),
851 p = this.getAllAncestors(),
852 block = null,
853 a = null,
854 doc = this.document;
855 for (var i = 0, n = p.length; i < n; ++i) {
856 if (HTMLArea.DOM.isBlockElement(p[i]) && !/^(html|body|table|tbody|thead|tfoot|tr|dl)$/i.test(p[i].nodeName)) {
857 block = p[i];
858 break;
859 }
860 }
861 if (block && /^(td|th|tr|tbody|thead|tfoot|table)$/i.test(block.nodeName) && this.editor.config.buttons.table && this.editor.config.buttons.table.disableEnterParagraphs) {
862 return false;
863 }
864 if (!range.collapsed) {
865 range.deleteContents();
866 }
867 this.empty();
868 if (!block || /^(td|div|article|aside|footer|header|nav|section)$/i.test(block.nodeName)) {
869 if (!block) {
870 block = doc.body;
871 }
872 if (block.hasChildNodes()) {
873 rangeClone = range.cloneRange();
874 if (range.startContainer == block) {
875 // Selection is directly under the block
876 var blockOnLeft = null;
877 var leftSibling = null;
878 // Looking for the farthest node on the left that is not a block
879 for (var i = range.startOffset; --i >= 0;) {
880 if (HTMLArea.DOM.isBlockElement(block.childNodes[i])) {
881 blockOnLeft = block.childNodes[i];
882 break;
883 } else {
884 rangeClone.setStartBefore(block.childNodes[i]);
885 }
886 }
887 } else {
888 // Looking for inline or text container immediate child of block
889 var inlineContainer = range.startContainer;
890 while (inlineContainer.parentNode != block) {
891 inlineContainer = inlineContainer.parentNode;
892 }
893 // Looking for the farthest node on the left that is not a block
894 var leftSibling = inlineContainer;
895 while (leftSibling.previousSibling && !HTMLArea.DOM.isBlockElement(leftSibling.previousSibling)) {
896 leftSibling = leftSibling.previousSibling;
897 }
898 rangeClone.setStartBefore(leftSibling);
899 var blockOnLeft = leftSibling.previousSibling;
900 }
901 // Avoiding surroundContents buggy in Opera and Safari
902 left = doc.createElement('p');
903 left.appendChild(rangeClone.extractContents());
904 if (!left.textContent && !left.getElementsByTagName('img').length && !left.getElementsByTagName('table').length) {
905 left.innerHTML = '<br />';
906 }
907 if (block.hasChildNodes()) {
908 if (blockOnLeft) {
909 left = block.insertBefore(left, blockOnLeft.nextSibling);
910 } else {
911 left = block.insertBefore(left, block.firstChild);
912 }
913 } else {
914 left = block.appendChild(left);
915 }
916 block.normalize();
917 // Looking for the farthest node on the right that is not a block
918 var rightSibling = left;
919 while (rightSibling.nextSibling && !HTMLArea.DOM.isBlockElement(rightSibling.nextSibling)) {
920 rightSibling = rightSibling.nextSibling;
921 }
922 var blockOnRight = rightSibling.nextSibling;
923 range.setEndAfter(rightSibling);
924 range.setStartAfter(left);
925 // Avoiding surroundContents buggy in Opera and Safari
926 right = doc.createElement('p');
927 right.appendChild(range.extractContents());
928 if (!right.textContent && !right.getElementsByTagName('img').length && !right.getElementsByTagName('table').length) {
929 right.innerHTML = '<br />';
930 }
931 if (!(left.childNodes.length == 1 && right.childNodes.length == 1 && left.firstChild.nodeName.toLowerCase() == 'br' && right.firstChild.nodeName.toLowerCase() == 'br')) {
932 if (blockOnRight) {
933 right = block.insertBefore(right, blockOnRight);
934 } else {
935 right = block.appendChild(right);
936 }
937 this.selectNodeContents(right, true);
938 } else {
939 this.selectNodeContents(left, true);
940 }
941 block.normalize();
942 } else {
943 var first = block.firstChild;
944 if (first) {
945 block.removeChild(first);
946 }
947 right = doc.createElement('p');
948 if (Ext.isWebKit || Ext.isOpera) {
949 right.innerHTML = '<br />';
950 }
951 right = block.appendChild(right);
952 this.selectNodeContents(right, true);
953 }
954 } else {
955 range.setEndAfter(block);
956 var df = range.extractContents(), left_empty = false;
957 if (!/\S/.test(block.innerHTML) || (!/\S/.test(block.textContent) && !/<(img|hr|table)/i.test(block.innerHTML))) {
958 if (!Ext.isOpera) {
959 block.innerHTML = '<br />';
960 }
961 left_empty = true;
962 }
963 p = df.firstChild;
964 if (p) {
965 if (!/\S/.test(p.innerHTML) || (!/\S/.test(p.textContent) && !/<(img|hr|table)/i.test(p.innerHTML))) {
966 if (/^h[1-6]$/i.test(p.nodeName)) {
967 p = HTMLArea.DOM.convertNode(p, 'p');
968 }
969 if (/^(dt|dd)$/i.test(p.nodeName)) {
970 p = HTMLArea.DOM.convertNode(p, /^(dt)$/i.test(p.nodeName) ? 'dd' : 'dt');
971 }
972 if (!Ext.isOpera) {
973 p.innerHTML = '<br />';
974 }
975 if (/^li$/i.test(p.nodeName) && left_empty && (!block.nextSibling || !/^li$/i.test(block.nextSibling.nodeName))) {
976 left = block.parentNode;
977 left.removeChild(block);
978 range.setEndAfter(left);
979 range.collapse(false);
980 p = HTMLArea.DOM.convertNode(p, /^(li|dd|td|th|p|h[1-6])$/i.test(left.parentNode.nodeName) ? 'br' : 'p');
981 }
982 }
983 range.insertNode(df);
984 // Remove any anchor created empty on both sides of the selection
985 if (p.previousSibling) {
986 var a = p.previousSibling.lastChild;
987 if (a && /^a$/i.test(a.nodeName) && !/\S/.test(a.innerHTML)) {
988 HTMLArea.DOM.convertNode(a, 'br');
989 }
990 }
991 var a = p.lastChild;
992 if (a && /^a$/i.test(a.nodeName) && !/\S/.test(a.innerHTML)) {
993 HTMLArea.DOM.convertNode(a, 'br');
994 }
995 // Walk inside the deepest child element (presumably inline element)
996 while (p.firstChild && p.firstChild.nodeType === HTMLArea.DOM.ELEMENT_NODE && !/^(br|img|hr|table)$/i.test(p.firstChild.nodeName)) {
997 p = p.firstChild;
998 }
999 if (/^br$/i.test(p.nodeName)) {
1000 p = p.parentNode.insertBefore(doc.createTextNode('\x20'), p);
1001 } else if (!/\S/.test(p.innerHTML)) {
1002 // Need some element inside the deepest element
1003 p.appendChild(doc.createElement('br'));
1004 }
1005 this.selectNodeContents(p, true);
1006 } else {
1007 if (/^(li|dt|dd)$/i.test(block.nodeName)) {
1008 p = doc.createElement(block.nodeName);
1009 } else {
1010 p = doc.createElement('p');
1011 }
1012 if (!Ext.isOpera) {
1013 p.innerHTML = '<br />';
1014 }
1015 if (block.nextSibling) {
1016 p = block.parentNode.insertBefore(p, block.nextSibling);
1017 } else {
1018 p = block.parentNode.appendChild(p);
1019 }
1020 this.selectNodeContents(p, true);
1021 }
1022 }
1023 this.editor.scrollToCaret();
1024 return true;
1025 }
1026 });