(function () { var exports = this, Selection; Selection = function (env) { this._selection = null; this.env = env; this._initializeRangeLibrary(); this._getSelection(); }; Selection.prototype = { /** * Returns the selection object for the current browser. */ _getSelection: function () { if (this._selection) { this._selection.refresh(); } else if (this.env.frame) { this._selection = rangy.getIframeSelection(this.env.frame); } else { this._selection = rangy.getSelection(); } return this._selection; }, /** * Creates a range object. */ createRange: function () { return rangy.createRange(this.env.document); }, /** * Returns the range object at the specified position. The current range object * is at position 0. Note - currently only setting single range in `addRange` so * position 0 will be the only allocation filled. */ getRangeAt: function (pos) { this._selection.refresh(); try { return this._selection.getRangeAt(pos); } catch (e) { this._selection = null; return this._getSelection().getRangeAt(0); } }, /** * Adds the specified range to the current selection. Note - only supporting setting * a single range, so the previous range gets evicted. */ addRange: function (range) { this._selection || (this._selection = this._getSelection()); this._selection.setSingleRange(range); this._selection.ranges = [range]; return; }, /** * Initialize and extend the `rangy` library with some custom functionality. */ _initializeRangeLibrary: function () { var self = this; rangy.init(); rangy.config.checkSelectionRanges = false; var move = function (range, unitType, units, isStart) { if (units === 0) { throw Error('InvalidArgumentException: units cannot be 0'); } switch (unitType) { case ice.dom.CHARACTER_UNIT: if (units > 0) { range.moveCharRight(isStart, units); } else { range.moveCharLeft(isStart, units * -1); } break; case ice.dom.WORD_UNIT: default: // Removed. TODO: possibly refactor or re-implement. break; } }; /** * Moves the start of the range using the specified `unitType`, by the specified * number of `units`. Defaults to `CHARACTER_UNIT` and units of 1. */ rangy.rangePrototype.moveStart = function (unitType, units) { move(this, unitType, units, true); }; /** * Moves the end of the range using the specified `unitType`, by the specified * number of `units`. */ rangy.rangePrototype.moveEnd = function (unitType, units) { move(this, unitType, units, false); }; /** * Depending on the given `start` boolean, sets the start or end containers * to the given `container` with `offset` units. */ rangy.rangePrototype.setRange = function (start, container, offset) { if (start) { this.setStart(container, offset); } else { this.setEnd(container, offset); } }; /** * Depending on the given `moveStart` boolean, moves the start or end containers * to the left by the given number of character `units`. Use the following * example as a demonstration for where the range will fall as it moves in and * out of tag boundaries (where "|" is the marked range): * * test it o|ut * test it |out * test it| out * test i|t out * test |it out * test| it out * tes|t it out * * A range could be mapped in one of two ways: * * (1) If a startContainer is a Node of type Text, Comment, or CDATASection, then startOffset * is the number of characters from the start of startNode. For example, the following * are the range properties for `

te|st

` (where "|" is the collapsed range): * * startContainer: test * startOffset: 2 * endContainer: test * endOffset: 2 * * (2) For other Node types, startOffset is the number of child nodes between the start of * the startNode. Take the following html fragment: * * `

some test text

` * * If we were working with the following range properties: * * startContainer:

* startOffset: 2 * endContainer:

* endOffset: 2 * * Since

is an Element node, the offsets are based on the offset in child nodes of

and * the range is selecting the second child - the tag. * *

some test text

*/ rangy.rangePrototype.moveCharLeft = function (moveStart, units) { var container, offset; if (moveStart) { container = this.startContainer; offset = this.startOffset; } else { container = this.endContainer; offset = this.endOffset; } // Handle the case where the range conforms to (2) (noted in the comment above). if (container.nodeType === ice.dom.ELEMENT_NODE) { if (container.hasChildNodes()) { container = container.childNodes[offset]; container = this.getPreviousTextNode(container); // Get the previous text container that is not an empty text node. while (container && container.nodeType == ice.dom.TEXT_NODE && container.nodeValue === "") { container = this.getPreviousTextNode(container); } offset = container.data.length - units; } else { offset = units * -1; } } else { offset -= units; } if (offset < 0) { // We need to move to a previous selectable container. while (offset < 0) { var skippedBlockElem = []; container = this.getPreviousTextNode(container, skippedBlockElem); // We are at the beginning/out of the editable - break. if (!container) { return; } if (container.nodeType === ice.dom.ELEMENT_NODE) { continue; } offset += container.data.length; } } this.setRange(moveStart, container, offset); }; /** * Depending on the given `moveStart` boolean, moves the start or end containers * to the right by the given number of character `units`. Use the following * example as a demonstration for where the range will fall as it moves in and * out of tag boundaries (where "|" is the marked range): * * tes|t it out * test| it out * test |it out * test i|t out * test it| out * test it |out * * A range could be mapped in one of two ways: * * (1) If a startContainer is a Node of type Text, Comment, or CDATASection, then startOffset * is the number of characters from the start of startNode. For example, the following * are the range properties for `

te|st

` (where "|" is the collapsed range): * * startContainer: test * startOffset: 2 * endContainer: test * endOffset: 2 * * (2) For other Node types, startOffset is the number of child nodes between the start of * the startNode. Take the following html fragment: * * `

some test text

` * * If we were working with the following range properties: * * startContainer:

* startOffset: 2 * endContainer:

* endOffset: 2 * * Since

is an Element node, the offsets are based on the offset in child nodes of

and * the range is selecting the second child - the tag. * *

some test text

*/ rangy.rangePrototype.moveCharRight = function (moveStart, units) { var container, offset; if (moveStart) { container = this.startContainer; offset = this.startOffset; } else { container = this.endContainer; offset = this.endOffset; } if (container.nodeType === ice.dom.ELEMENT_NODE) { container = container.childNodes[offset]; if (container.nodeType !== ice.dom.TEXT_NODE) { container = this.getNextTextNode(container); } offset = units; } else { offset += units; } var diff = (offset - container.data.length); if (diff > 0) { var skippedBlockElem = []; // We need to move to the next selectable container. while (diff > 0) { container = this.getNextContainer(container, skippedBlockElem); if (container.nodeType === ice.dom.ELEMENT_NODE) { continue; } if (container.data.length >= diff) { // We found a container with enough content to select. break; } else if (container.data.length > 0) { // Container does not have enough content, // find the next one. diff -= container.data.length; } } offset = diff; } this.setRange(moveStart, container, offset); }; /** * Returns the deepest next container that the range can be extended to. * For example, if the next container is an element that contains text nodes, * the the container's firstChild is returned. */ rangy.rangePrototype.getNextContainer = function (container, skippedBlockElem) { if (!container) { return null; } while (container.nextSibling) { container = container.nextSibling; if (container.nodeType !== ice.dom.TEXT_NODE) { var child = this.getFirstSelectableChild(container); if (child !== null) { return child; } } else if (this.isSelectable(container) === true) { return container; } } // Look at parents next sibling. while (container && !container.nextSibling) { container = container.parentNode; } if (!container) { return null; } container = container.nextSibling; if (this.isSelectable(container) === true) { return container; } else if (skippedBlockElem && ice.dom.isBlockElement(container) === true) { skippedBlockElem.push(container); } var selChild = this.getFirstSelectableChild(container); if (selChild !== null) { return selChild; } return this.getNextContainer(container, skippedBlockElem); }; /** * Returns the deepest previous container that the range can be extended to. * For example, if the previous container is an element that contains text nodes, * then the container's lastChild is returned. */ rangy.rangePrototype.getPreviousContainer = function (container, skippedBlockElem) { if (!container) { return null; } while (container.previousSibling) { container = container.previousSibling; if (container.nodeType !== ice.dom.TEXT_NODE) { if (ice.dom.isStubElement(container) === true) { return container; } else { var child = this.getLastSelectableChild(container); if (child !== null) { return child; } } } else if (this.isSelectable(container) === true) { return container; } } // Look at parents next sibling. while (container && !container.previousSibling) { container = container.parentNode; } if (!container) { return null; } container = container.previousSibling; if (this.isSelectable(container) === true) { return container; } else if (skippedBlockElem && ice.dom.isBlockElement(container) === true) { skippedBlockElem.push(container); } var selChild = this.getLastSelectableChild(container); if (selChild !== null) { return selChild; } return this.getPreviousContainer(container, skippedBlockElem); }; rangy.rangePrototype.getNextTextNode = function (container) { if (container.nodeType === ice.dom.ELEMENT_NODE) { if (container.childNodes.length !== 0) { return this.getFirstSelectableChild(container); } } container = this.getNextContainer(container); if (container.nodeType === ice.dom.TEXT_NODE) { return container; } return this.getNextTextNode(container); }; rangy.rangePrototype.getPreviousTextNode = function (container, skippedBlockEl) { container = this.getPreviousContainer(container, skippedBlockEl); if (container.nodeType === ice.dom.TEXT_NODE) { return container; } return this.getPreviousTextNode(container, skippedBlockEl); }; rangy.rangePrototype.getFirstSelectableChild = function (element) { if (element) { if (element.nodeType !== ice.dom.TEXT_NODE) { var child = element.firstChild; while (child) { if (this.isSelectable(child) === true) { return child; } else if (child.firstChild) { // This node does have child nodes. var res = this.getFirstSelectableChild(child); if (res !== null) { return res; } else { child = child.nextSibling; } } else { child = child.nextSibling; } } } else { // Given element is a text node so return it. return element; } } return null; }; rangy.rangePrototype.getLastSelectableChild = function (element) { if (element) { if (element.nodeType !== ice.dom.TEXT_NODE) { var child = element.lastChild; while (child) { if (this.isSelectable(child) === true) { return child; } else if (child.lastChild) { // This node does have child nodes. var res = this.getLastSelectableChild(child); if (res !== null) { return res; } else { child = child.previousSibling; } } else { child = child.previousSibling; } } } else { // Given element is a text node so return it. return element; } } return null; }; rangy.rangePrototype.isSelectable = function (container) { if (container && container.nodeType === ice.dom.TEXT_NODE && container.data.length !== 0) { return true; } return false; }; rangy.rangePrototype.getHTMLContents = function (clonedSelection) { if (!clonedSelection) { clonedSelection = this.cloneContents(); } var div = self.env.document.createElement('div'); div.appendChild(clonedSelection.cloneNode(true)); return div.innerHTML; }; rangy.rangePrototype.getHTMLContentsObj = function () { return this.cloneContents(); }; } }; exports.Selection = Selection; }).call(this.ice);