I want to get all of the elements contained in a user selection (as in DOM 2 ranges / MS TextRanges).
/** @return {Array.<Element>} */
function getSelectedElements() {
var elements = [];
// get elements in the user selection somehow
return elements;
}
I've tried to do this by following Tim Down's excellent solution to a similar question, and some Moz and MS docs, and some PPK stuff.
The approach is basically:
Define SelectionLikeObject as a DOM Selection or an IE Selection.
Define RangeLikeObject as a DOM Range or an IE TextRange.
Let
containerNodebe a Node.Let
containerElementbe an Element.Let
containedElementsbe a NodeList.Let
elementRangebe a RangeLikeObject.Let
selectedRangebe a RangeLikeObject.Let
selectedElementsbe an Array of Elements.Let
elementbe an Element.Let
selectionbe a SelectionLikeObject.Set
selectionfrom the user's selection.Set
selectedElementsto a new Array.For each
selectedRangeinselection:Set
containerNodeto the common ancestor container ofselectedRange.Set
containerElementto the closest Element ancestor tocontainerNode.Set
containedElementsto a list of descendants ofcontainerElement.For each
elementincontainedElements:Set
elementRangefromelement.If the boundaries of
elementRangefall within the boundaries ofselectedRange:- Push
elementontoselectedElements.
- Push
The DOM branch looks like this:
/**
@param {Document} doc
@return {Array.<Element>}
*/
getSelectedElements.fromDOM = function (doc) {
/** @type {Range} */
var selectedRange;
/** @type {Array.<Element>} */
var selectedElements = [];
/** @type {Node} */
var containerNode;
/** @type {Element} */
var containerElement;
/** @type {NodeList} */
var containedElements;
/** @type {Range} */
var elementRange;
/** @type {Element} */
var element;
/** @type {Selection} */
var selection = doc.defaultView.getSelection();
/** @type {number} */
var rangeCount = selection.rangeCount;
/** @type {number} */
var elementCount;
/** @type {number} */
var i;
// hack for browsers without getRangeAt
// see http://www.quirksmode.org/dom/range_intro.html
if (!selection.getRangeAt) {
selection.getRangeAt = function (i) {
/** @type {Range} */
var range = doc.createRange();
if (i || !selection.anchorNode) {
return range;
}
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
return range;
};
selection.rangeCount = 1;
}
elementRange = doc.createRange();
for (i = 0; i < rangeCount; ++i) {
selectedRange = selection.getRangeAt(i);
containerNode = selectedRange.commonAncestorContainer;
while (containerNode && containerNode.nodeType != 1) {
containerNode = containerNode.parentNode;
}
if (!containerNode) {
return selectedElements; // something went wrong...
}
containerElement = /** @type {Element} */ containerNode;
containedElements = containerElement.getElementsByTagName('*');
elementCount = containedElements.length;
for (var i = 0; i < elementCount; ++i) {
element = containedElements[i];
elementRange.selectNodeContents(element);
if (elementRange.compareBoundaryPoints(selectedRange.END_TO_START, selectedRange) < 1 &&
elementRange.compareBoundaryPoints(selectedRange.START_TO_END, selectedRange) > -1) {
selectedElements.push(element);
}
}
}
elementRange.detach();
return selectedElements;
};
The IE branch looks like this:
/**
@param {Document} doc
@return {Array.<Element>}
*/
getSelectedElements.fromIE = function (doc) {
// Selection - http://msdn.microsoft.com/en-us/library/ie/dd347133(v=vs.85).aspx
// TextRange - http://msdn.microsoft.com/en-us/library/dd347140(v=vs.85).aspx
// ControlRange - http://msdn.microsoft.com/en-us/library/ie/ms537447(v=vs.85).aspx
/** @type {TextRange|ControlRange} */
var ieRange = doc.selection && doc.selection.createRange();
/** @type {Array.<Element>} */
var selectedElements = [];
/** @type {TextRange} */
var selectedRange;
/** @type {Element} */
var containerElement;
/** @type {NodeList} */
var containedElements;
/** @type {TextRange} */
var elementRange;
/** @type {Element} */
var element;
/** @type {Selection} */
var selection;
/** @type {number} */
var i = -1;
if (ieRange.text === void 0) {
return []; // FIXME: It's a ControlRange, give up.
}
selectedRange = /** @type {TextRange} */ ieRange;
containerElement = selectedRange.parentElement();
containedElements = containerElement.getElementsByTagName('*');
elementRange = doc.body.createTextRange();
while ((element = containedElements[++i])) {
elementRange.moveToElementText(element);
if (elementRange.compareEndPoints("StartToEnd", selectedRange) > -1 &&
elementRange.compareEndPoints("EndToStart", selectedRange) < 1) {
selectedElements.push(element);
}
}
return /** @type {Array.<Element>} */ selectedElements;
};
Now, the issue I want to solve is this: if only part of the text in an element is selected, it appears in the returned array, even though it is only partly selected.
I'd like to add a parameter that changes the behavior to only include fully-selected elements. I have a feeling the answer lies with compareBoundaryPoints, I just don't understand it well enough to figure it out yet.
Also, the IE code is untested so far, but please let me know if anything looks wrong with it (or the DOM branch).