| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- /**
- * TridentSelection.js
- *
- * Copyright, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
- /**
- * Selection class for old explorer versions. This one fakes the
- * native selection object available on modern browsers.
- *
- * @class tinymce.dom.TridentSelection
- */
- define("tinymce/dom/TridentSelection", [], function() {
- function Selection(selection) {
- var self = this, dom = selection.dom, FALSE = false;
- function getPosition(rng, start) {
- var checkRng, startIndex = 0, endIndex, inside,
- children, child, offset, index, position = -1, parent;
- // Setup test range, collapse it and get the parent
- checkRng = rng.duplicate();
- checkRng.collapse(start);
- parent = checkRng.parentElement();
- // Check if the selection is within the right document
- if (parent.ownerDocument !== selection.dom.doc) {
- return;
- }
- // IE will report non editable elements as it's parent so look for an editable one
- while (parent.contentEditable === "false") {
- parent = parent.parentNode;
- }
- // If parent doesn't have any children then return that we are inside the element
- if (!parent.hasChildNodes()) {
- return {node: parent, inside: 1};
- }
- // Setup node list and endIndex
- children = parent.children;
- endIndex = children.length - 1;
- // Perform a binary search for the position
- while (startIndex <= endIndex) {
- index = Math.floor((startIndex + endIndex) / 2);
- // Move selection to node and compare the ranges
- child = children[index];
- checkRng.moveToElementText(child);
- position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
- // Before/after or an exact match
- if (position > 0) {
- endIndex = index - 1;
- } else if (position < 0) {
- startIndex = index + 1;
- } else {
- return {node: child};
- }
- }
- // Check if child position is before or we didn't find a position
- if (position < 0) {
- // No element child was found use the parent element and the offset inside that
- if (!child) {
- checkRng.moveToElementText(parent);
- checkRng.collapse(true);
- child = parent;
- inside = true;
- } else {
- checkRng.collapse(false);
- }
- // Walk character by character in text node until we hit the selected range endpoint,
- // hit the end of document or parent isn't the right one
- // We need to walk char by char since rng.text or rng.htmlText will trim line endings
- offset = 0;
- while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
- if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
- break;
- }
- offset++;
- }
- } else {
- // Child position is after the selection endpoint
- checkRng.collapse(true);
- // Walk character by character in text node until we hit the selected range endpoint, hit
- // the end of document or parent isn't the right one
- offset = 0;
- while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
- if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
- break;
- }
- offset++;
- }
- }
- return {node: child, position: position, offset: offset, inside: inside};
- }
- // Returns a W3C DOM compatible range object by using the IE Range API
- function getRange() {
- var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
- // If selection is outside the current document just return an empty range
- element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
- if (element.ownerDocument != dom.doc) {
- return domRange;
- }
- collapsed = selection.isCollapsed();
- // Handle control selection
- if (ieRange.item) {
- domRange.setStart(element.parentNode, dom.nodeIndex(element));
- domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
- return domRange;
- }
- function findEndPoint(start) {
- var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
- container = endPoint.node;
- offset = endPoint.offset;
- if (endPoint.inside && !container.hasChildNodes()) {
- domRange[start ? 'setStart' : 'setEnd'](container, 0);
- return;
- }
- if (offset === undef) {
- domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
- return;
- }
- if (endPoint.position < 0) {
- sibling = endPoint.inside ? container.firstChild : container.nextSibling;
- if (!sibling) {
- domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
- return;
- }
- if (!offset) {
- if (sibling.nodeType == 3) {
- domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
- } else {
- domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
- }
- return;
- }
- // Find the text node and offset
- while (sibling) {
- nodeValue = sibling.nodeValue;
- textNodeOffset += nodeValue.length;
- // We are at or passed the position we where looking for
- if (textNodeOffset >= offset) {
- container = sibling;
- textNodeOffset -= offset;
- textNodeOffset = nodeValue.length - textNodeOffset;
- break;
- }
- sibling = sibling.nextSibling;
- }
- } else {
- // Find the text node and offset
- sibling = container.previousSibling;
- if (!sibling) {
- return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
- }
- // If there isn't any text to loop then use the first position
- if (!offset) {
- if (container.nodeType == 3) {
- domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
- } else {
- domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
- }
- return;
- }
- while (sibling) {
- textNodeOffset += sibling.nodeValue.length;
- // We are at or passed the position we where looking for
- if (textNodeOffset >= offset) {
- container = sibling;
- textNodeOffset -= offset;
- break;
- }
- sibling = sibling.previousSibling;
- }
- }
- domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
- }
- try {
- // Find start point
- findEndPoint(true);
- // Find end point if needed
- if (!collapsed) {
- findEndPoint();
- }
- } catch (ex) {
- // IE has a nasty bug where text nodes might throw "invalid argument" when you
- // access the nodeValue or other properties of text nodes. This seems to happend when
- // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
- if (ex.number == -2147024809) {
- // Get the current selection
- bookmark = self.getBookmark(2);
- // Get start element
- tmpRange = ieRange.duplicate();
- tmpRange.collapse(true);
- element = tmpRange.parentElement();
- // Get end element
- if (!collapsed) {
- tmpRange = ieRange.duplicate();
- tmpRange.collapse(false);
- element2 = tmpRange.parentElement();
- element2.innerHTML = element2.innerHTML;
- }
- // Remove the broken elements
- element.innerHTML = element.innerHTML;
- // Restore the selection
- self.moveToBookmark(bookmark);
- // Since the range has moved we need to re-get it
- ieRange = selection.getRng();
- // Find start point
- findEndPoint(true);
- // Find end point if needed
- if (!collapsed) {
- findEndPoint();
- }
- } else {
- throw ex; // Throw other errors
- }
- }
- return domRange;
- }
- this.getBookmark = function(type) {
- var rng = selection.getRng(), bookmark = {};
- function getIndexes(node) {
- var parent, root, children, i, indexes = [];
- parent = node.parentNode;
- root = dom.getRoot().parentNode;
- while (parent != root && parent.nodeType !== 9) {
- children = parent.children;
- i = children.length;
- while (i--) {
- if (node === children[i]) {
- indexes.push(i);
- break;
- }
- }
- node = parent;
- parent = parent.parentNode;
- }
- return indexes;
- }
- function getBookmarkEndPoint(start) {
- var position;
- position = getPosition(rng, start);
- if (position) {
- return {
- position: position.position,
- offset: position.offset,
- indexes: getIndexes(position.node),
- inside: position.inside
- };
- }
- }
- // Non ubstructive bookmark
- if (type === 2) {
- // Handle text selection
- if (!rng.item) {
- bookmark.start = getBookmarkEndPoint(true);
- if (!selection.isCollapsed()) {
- bookmark.end = getBookmarkEndPoint();
- }
- } else {
- bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
- }
- }
- return bookmark;
- };
- this.moveToBookmark = function(bookmark) {
- var rng, body = dom.doc.body;
- function resolveIndexes(indexes) {
- var node, i, idx, children;
- node = dom.getRoot();
- for (i = indexes.length - 1; i >= 0; i--) {
- children = node.children;
- idx = indexes[i];
- if (idx <= children.length - 1) {
- node = children[idx];
- }
- }
- return node;
- }
- function setBookmarkEndPoint(start) {
- var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
- if (endPoint) {
- moveLeft = endPoint.position > 0;
- moveRng = body.createTextRange();
- moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
- offset = endPoint.offset;
- if (offset !== undef) {
- moveRng.collapse(endPoint.inside || moveLeft);
- moveRng.moveStart('character', moveLeft ? -offset : offset);
- } else {
- moveRng.collapse(start);
- }
- rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
- if (start) {
- rng.collapse(true);
- }
- }
- }
- if (bookmark.start) {
- if (bookmark.start.ctrl) {
- rng = body.createControlRange();
- rng.addElement(resolveIndexes(bookmark.start.indexes));
- rng.select();
- } else {
- rng = body.createTextRange();
- setBookmarkEndPoint(true);
- setBookmarkEndPoint();
- rng.select();
- }
- }
- };
- this.addRange = function(rng) {
- var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
- doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
- function setEndPoint(start) {
- var container, offset, marker, tmpRng, nodes;
- marker = dom.create('a');
- container = start ? startContainer : endContainer;
- offset = start ? startOffset : endOffset;
- tmpRng = ieRng.duplicate();
- if (container == doc || container == doc.documentElement) {
- container = body;
- offset = 0;
- }
- if (container.nodeType == 3) {
- container.parentNode.insertBefore(marker, container);
- tmpRng.moveToElementText(marker);
- tmpRng.moveStart('character', offset);
- dom.remove(marker);
- ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
- } else {
- nodes = container.childNodes;
- if (nodes.length) {
- if (offset >= nodes.length) {
- dom.insertAfter(marker, nodes[nodes.length - 1]);
- } else {
- container.insertBefore(marker, nodes[offset]);
- }
- tmpRng.moveToElementText(marker);
- } else if (container.canHaveHTML) {
- // Empty node selection for example <div>|</div>
- // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
- container.innerHTML = '<span></span>';
- marker = container.firstChild;
- tmpRng.moveToElementText(marker);
- tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
- }
- ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
- dom.remove(marker);
- }
- }
- // Setup some shorter versions
- startContainer = rng.startContainer;
- startOffset = rng.startOffset;
- endContainer = rng.endContainer;
- endOffset = rng.endOffset;
- ieRng = body.createTextRange();
- // If single element selection then try making a control selection out of it
- if (startContainer == endContainer && startContainer.nodeType == 1) {
- // Trick to place the caret inside an empty block element like <p></p>
- if (startOffset == endOffset && !startContainer.hasChildNodes()) {
- if (startContainer.canHaveHTML) {
- // Check if previous sibling is an empty block if it is then we need to render it
- // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
- // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
- sibling = startContainer.previousSibling;
- if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
- sibling.innerHTML = '';
- } else {
- sibling = null;
- }
- startContainer.innerHTML = '<span></span><span></span>';
- ieRng.moveToElementText(startContainer.lastChild);
- ieRng.select();
- dom.doc.selection.clear();
- startContainer.innerHTML = '';
- if (sibling) {
- sibling.innerHTML = '';
- }
- return;
- } else {
- startOffset = dom.nodeIndex(startContainer);
- startContainer = startContainer.parentNode;
- }
- }
- if (startOffset == endOffset - 1) {
- try {
- ctrlElm = startContainer.childNodes[startOffset];
- ctrlRng = body.createControlRange();
- ctrlRng.addElement(ctrlElm);
- ctrlRng.select();
- // Check if the range produced is on the correct element and is a control range
- // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
- nativeRng = selection.getRng();
- if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
- return;
- }
- } catch (ex) {
- // Ignore
- }
- }
- }
- // Set start/end point of selection
- setEndPoint(true);
- setEndPoint();
- // Select the new range and scroll it into view
- ieRng.select();
- };
- // Expose range method
- this.getRangeAt = getRange;
- }
- return Selection;
- });
|