range.js | |
---|---|
(function ($) {
$.fn.range =
function () {
return $.Range(this[0])
}
var convertType = function (type) {
return type.replace(/([a-z])([a-z]+)/gi, function (all, first, next) {
return first + next.toLowerCase()
}).replace(/_/g, "");
}, | |
reverses things like STARTTOEND into ENDTOSTART | reverse = function (type) {
return type.replace(/^([a-z]+)_TO_([a-z]+)/i, function (all, first, last) {
return last + "_TO_" + first;
});
},
getWindow = function (element) {
return element ? element.ownerDocument.defaultView || element.ownerDocument.parentWindow : window
},
bisect = function (el, start, end) { |
split the start and end ... figure out who is touching ... | if (end - start == 1) {
return
}
},
support = {};
$.Range = function (range) { |
If it's called w/o new, call it with new! | if (this.constructor !== $.Range) {
return new $.Range(range);
} |
If we are passed a jQuery-wrapped element, get the raw element | if (range && range.jquery) {
range = range[0];
} |
If we have an element, or nothing | if (!range || range.nodeType) { |
create a range | this.win = getWindow(range)
if (this.win.document.createRange) {
this.range = this.win.document.createRange()
} else if (this.win && this.win.document.body && this.win.document.body.createTextRange) {
this.range = this.win.document.body.createTextRange()
} |
if we have an element, make the range select it | if (range) {
this.select(range)
}
} |
if we are given a point | else if (range.clientX != null || range.pageX != null || range.left != null) {
this.moveToPoint(range);
} |
if we are given a touch event | else if (range.originalEvent && range.originalEvent.touches && range.originalEvent.touches.length) {
this.moveToPoint(range.originalEvent.touches[0])
} |
if we are a normal event | else if (range.originalEvent && range.originalEvent.changedTouches && range.originalEvent.changedTouches.length) {
this.moveToPoint(range.originalEvent.changedTouches[0])
} |
given a TextRange or something else? | else {
this.range = range;
}
};
$.Range.
current = function (el) {
var win = getWindow(el),
selection;
if (win.getSelection) { |
If we can get the selection | selection = win.getSelection()
return new $.Range(selection.rangeCount ? selection.getRangeAt(0) : win.document.createRange())
} else { |
Otherwise use document.selection | return new $.Range(win.document.selection.createRange());
}
};
$.extend($.Range.prototype,
{
moveToPoint: function (point) {
var clientX = point.clientX,
clientY = point.clientY
if (!clientX) {
var off = scrollOffset();
clientX = (point.pageX || point.left || 0) - off.left;
clientY = (point.pageY || point.top || 0) - off.top;
}
if (support.moveToPoint) {
this.range = $.Range().range
this.range.moveToPoint(clientX, clientY);
return this;
} |
it's some text node in this range ... | var parent = document.elementFromPoint(clientX, clientY); |
typically it will be 'on' text | for (var n = 0; n < parent.childNodes.length; n++) {
var node = parent.childNodes[n];
if (node.nodeType === 3 || node.nodeType === 4) {
var range = $.Range(node),
length = range.toString().length; |
now lets start moving the end until the boundingRect is within our range | for (var i = 1; i < length + 1; i++) {
var rect = range.end(i).rect();
if (rect.left <= clientX && rect.left + rect.width >= clientX && rect.top <= clientY && rect.top + rect.height >= clientY) {
range.start(i - 1);
this.range = range.range;
return this;
}
}
}
} |
if not 'on' text, recursively go through and find out when we shift to next 'line' | var previous;
iterate(parent.childNodes, function (textNode) {
var range = $.Range(textNode);
if (range.rect().top > point.clientY) {
return false;
} else {
previous = range;
}
});
if (previous) {
previous.start(previous.toString().length);
this.range = previous.range;
} else {
this.range = $.Range(parent).range
}
},
window: function () {
return this.win || window;
},
overlaps: function (elRange) {
if (elRange.nodeType) {
elRange = $.Range(elRange).select(elRange);
} |
if the start is within the element ... | var startToStart = this.compare("START_TO_START", elRange),
endToEnd = this.compare("END_TO_END", elRange) |
if we wrap elRange | if (startToStart <= 0 && endToEnd >= 0) {
return true;
} |
if our start is inside of it | if (startToStart >= 0 && this.compare("START_TO_END", elRange) <= 0) {
return true;
} |
if our end is inside of elRange | if (this.compare("END_TO_START", elRange) >= 0 && endToEnd <= 0) {
return true;
}
return false;
},
collapse: function (toStart) {
this.range.collapse(toStart === undefined ? true : toStart);
return this;
},
toString: function () {
return typeof this.range.text == "string" ? this.range.text : this.range.toString();
},
start: function (set) { |
return start | if (set === undefined) {
if (this.range.startContainer) {
return {
container: this.range.startContainer,
offset: this.range.startOffset
}
} else { |
Get the start parent element | var start = this.clone().collapse().parent(); |
used to get the start element offset | var startRange = $.Range(start).select(start).collapse();
startRange.move("END_TO_START", this);
return {
container: start,
offset: startRange.toString().length
}
}
} else {
if (this.range.setStart) { |
supports setStart | if (typeof set == 'number') {
this.range.setStart(this.range.startContainer, set)
} else if (typeof set == 'string') {
var res = callMove(this.range.startContainer, this.range.startOffset, parseInt(set, 10))
this.range.setStart(res.node, res.offset);
} else {
this.range.setStart(set.container, set.offset)
}
} else {
if (typeof set == "string") {
this.range.moveStart('character', parseInt(set, 10))
} else { |
get the current end container | var container = this.start().container,
offset
if (typeof set == "number") {
offset = set
} else {
container = set.container
offset = set.offset
}
var newPoint = $.Range(container).collapse(); |
move it over offset characters | newPoint.range.move(offset);
this.move("START_TO_START", newPoint);
}
}
return this;
}
},
end: function (set) { |
read end | if (set === undefined) {
if (this.range.startContainer) {
return {
container: this.range.endContainer,
offset: this.range.endOffset
}
}
else {
var |
Get the end parent element | end = this.clone().collapse(false).parent(), |
used to get the end elements offset | endRange = $.Range(end).select(end).collapse();
endRange.move("END_TO_END", this);
return {
container: end,
offset: endRange.toString().length
}
}
} else {
if (this.range.setEnd) {
if (typeof set == 'number') {
this.range.setEnd(this.range.endContainer, set)
} else if (typeof set == 'string') {
var res = callMove(this.range.endContainer, this.range.endOffset, parseInt(set, 10))
this.range.setEnd(res.node, res.offset);
} else {
this.range.setEnd(set.container, set.offset)
}
} else {
if (typeof set == "string") {
this.range.moveEnd('character', parseInt(set, 10));
} else { |
get the current end container | var container = this.end().container,
offset
if (typeof set == "number") {
offset = set
} else {
container = set.container
offset = set.offset
}
var newPoint = $.Range(container).collapse(); |
move it over offset characters | newPoint.range.move(offset);
this.move("END_TO_START", newPoint);
}
}
return this;
}
},
parent: function () {
if (this.range.commonAncestorContainer) {
return this.range.commonAncestorContainer;
} else {
var parentElement = this.range.parentElement(),
range = this.range; |
IE's parentElement will always give an element, we want text ranges | iterate(parentElement.childNodes, function (txtNode) {
if ($.Range(txtNode).range.inRange(range)) { |
swap out the parentElement | parentElement = txtNode;
return false;
}
});
return parentElement;
}
},
rect: function (from) {
var rect = this.range.getBoundingClientRect(); |
for some reason in webkit this gets a better value | if (!rect.height && !rect.width) {
rect = this.range.getClientRects()[0]
}
if (from === 'page') { |
Add the scroll offset | var off = scrollOffset();
rect = $.extend({}, rect);
rect.top += off.top;
rect.left += off.left;
}
return rect;
},
rects: function (from) { |
order rects by size | var rects = $.map($.makeArray(this.range.getClientRects()).sort(function (rect1, rect2) {
return rect2.width * rect2.height - rect1.width * rect1.height;
}), function (rect) {
return $.extend({}, rect)
}),
i = 0,
j, len = rects.length; |
safari returns overlapping client rects - big rects can contain 2 smaller rects - some rects can contain 0 - width rects - we don't want these 0 width rects | while (i < rects.length) {
var cur = rects[i],
found = false;
j = i + 1;
while (j < rects.length) {
if (withinRect(cur, rects[j])) {
if (!rects[j].width) {
rects.splice(j, 1)
} else {
found = rects[j];
break;
}
} else {
j++;
}
}
if (found) {
rects.splice(i, 1)
} else {
i++;
}
} |
safari will be return overlapping ranges ... | if (from == 'page') {
var off = scrollOffset();
return $.each(rects, function (ith, item) {
item.top += off.top;
item.left += off.left;
})
}
return rects;
}
});
(function () { |
method branching .... | var fn = $.Range.prototype,
range = $.Range().range;
fn.compare = range.compareBoundaryPoints ?
function (type, range) {
return this.range.compareBoundaryPoints(this.window().Range[reverse(type)], range.range)
} : function (type, range) {
return this.range.compareEndPoints(convertType(type), range.range)
}
fn.move = range.setStart ?
function (type, range) {
var rangesRange = range.range;
switch (type) {
case "START_TO_END":
this.range.setStart(rangesRange.endContainer, rangesRange.endOffset)
break;
case "START_TO_START":
this.range.setStart(rangesRange.startContainer, rangesRange.startOffset)
break;
case "END_TO_END":
this.range.setEnd(rangesRange.endContainer, rangesRange.endOffset)
break;
case "END_TO_START":
this.range.setEnd(rangesRange.startContainer, rangesRange.startOffset)
break;
}
return this;
} : function (type, range) {
this.range.setEndPoint(convertType(type), range.range)
return this;
};
var cloneFunc = range.cloneRange ? "cloneRange" : "duplicate",
selectFunc = range.selectNodeContents ? "selectNodeContents" : "moveToElementText";
fn.
clone = function () {
return $.Range(this.range[cloneFunc]());
};
fn.
select = range.selectNodeContents ?
function (el) {
if (!el) {
var selection = this.window().getSelection();
selection.removeAllRanges();
selection.addRange(this.range);
} else {
this.range.selectNodeContents(el);
}
return this;
} : function (el) {
if (!el) {
this.range.select()
} else if (el.nodeType === 3) { |
select this node in the element ... | var parent = el.parentNode,
start = 0,
end;
iterate(parent.childNodes, function (txtNode) {
if (txtNode === el) {
end = start + txtNode.nodeValue.length;
return false;
} else {
start = start + txtNode.nodeValue.length
}
})
this.range.moveToElementText(parent);
this.range.moveEnd('character', end - this.range.text.length)
this.range.moveStart('character', start);
} else {
this.range.moveToElementText(el);
}
return this;
};
})(); |
helpers ----------------- iterates through a list of elements, calls cb on every text node if cb returns false, exits the iteration | var iterate = function (elems, cb) {
var elem, start;
for (var i = 0; elems[i]; i++) {
elem = elems[i]; |
Get the text from text nodes and CDATA nodes | if (elem.nodeType === 3 || elem.nodeType === 4) {
if (cb(elem) === false) {
return false;
} |
Traverse everything else, except comment nodes | }
else if (elem.nodeType !== 8) {
if (iterate(elem.childNodes, cb) === false) {
return false;
}
}
}
},
isText = function (node) {
return node.nodeType === 3 || node.nodeType === 4
},
iteratorMaker = function (toChildren, toNext) {
return function (node, mustMoveRight) { |
first try down | if (node[toChildren] && !mustMoveRight) {
return isText(node[toChildren]) ? node[toChildren] : arguments.callee(node[toChildren])
} else if (node[toNext]) {
return isText(node[toNext]) ? node[toNext] : arguments.callee(node[toNext])
} else if (node.parentNode) {
return arguments.callee(node.parentNode, true)
}
}
},
getNextTextNode = iteratorMaker("firstChild", "nextSibling"),
getPrevTextNode = iteratorMaker("lastChild", "previousSibling"),
callMove = function (container, offset, howMany) {
var mover = howMany < 0 ? getPrevTextNode : getNextTextNode; |
find the text element | if (!isText(container)) { |
sometimes offset isn't actually an element | container = container.childNodes[offset] ? container.childNodes[offset] : |
if this happens, use the last child | container.lastChild;
if (!isText(container)) {
container = mover(container)
}
return move(container, howMany)
} else {
if (offset + howMany < 0) {
return move(mover(container), offset + howMany)
} else {
return move(container, offset + howMany)
}
}
}, |
Moves howMany characters from the start of from | move = function (from, howMany) {
var mover = howMany < 0 ? getPrevTextNode : getNextTextNode;
howMany = Math.abs(howMany);
while (from && howMany >= from.nodeValue.length) {
howMany = howMany - from.nodeValue.length;
from = mover(from)
}
return {
node: from,
offset: mover === getNextTextNode ? howMany : from.nodeValue.length - howMany
}
},
supportWhitespace, isWhitespace = function (el) {
if (supportWhitespace == null) {
supportWhitespace = 'isElementContentWhitespace' in el;
}
return (supportWhitespace ? el.isElementContentWhitespace : (el.nodeType === 3 && '' == el.data.trim()));
}, |
if a point is within a rectangle | within = function (rect, point) {
return rect.left <= point.clientX && rect.left + rect.width >= point.clientX && rect.top <= point.clientY && rect.top + rect.height >= point.clientY
}, |
if a rectangle is within another rectangle | withinRect = function (outer, inner) {
return within(outer, {
clientX: inner.left,
clientY: inner.top
}) && //top left
within(outer, {
clientX: inner.left + inner.width,
clientY: inner.top
}) && //top right
within(outer, {
clientX: inner.left,
clientY: inner.top + inner.height
}) && //bottom left
within(outer, {
clientX: inner.left + inner.width,
clientY: inner.top + inner.height
}) //bottom right
}, |
gets the scroll offset from a window | scrollOffset = function (win) {
var win = win || window;
doc = win.document.documentElement, body = win.document.body;
return {
left: (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0),
top: (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0)
};
};
support.moveToPoint = !! $.Range().range.moveToPoint
return $;
})(jQuery);
|