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