wrap-range-text
Version:
Safely wrap all selected text contained within a DOM Range
158 lines (129 loc) • 3.9 kB
JavaScript
// return all text nodes that are contained within `el`
function getTextNodes(el) {
el = el || document.body
var doc = el.ownerDocument || document
, walker = doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false)
, textNodes = []
, node
while (node = walker.nextNode()) {
textNodes.push(node)
}
return textNodes
}
// return true if `rangeA` intersects `rangeB`
function rangesIntersect(rangeA, rangeB) {
return rangeA.compareBoundaryPoints(Range.END_TO_START, rangeB) === -1 &&
rangeA.compareBoundaryPoints(Range.START_TO_END, rangeB) === 1
}
// create and return a range that selects `node`
function createRangeFromNode(node) {
var range = node.ownerDocument.createRange()
try {
range.selectNode(node)
} catch (e) {
range.selectNodeContents(node)
}
return range
}
// return true if `node` is fully or partially selected by `range`
function rangeIntersectsNode(range, node) {
if (range.intersectsNode) {
return range.intersectsNode(node)
} else {
return rangesIntersect(range, createRangeFromNode(node))
}
}
// return all non-empty text nodes fully or partially selected by `range`
function getRangeTextNodes(range) {
var container = range.commonAncestorContainer
, nodes = getTextNodes(container.parentNode || container)
return nodes.filter(function (node) {
return rangeIntersectsNode(range, node) && isNonEmptyTextNode(node)
})
}
// returns true if `node` has text content
function isNonEmptyTextNode(node) {
return node.textContent.length > 0
}
// remove `el` from the DOM
function remove(el) {
if (el.parentNode) {
el.parentNode.removeChild(el)
}
}
// replace `node` with `replacementNode`
function replaceNode(replacementNode, node) {
remove(replacementNode)
node.parentNode.insertBefore(replacementNode, node)
remove(node)
}
// unwrap `el` by replacing itself with its contents
function unwrap(el) {
var range = document.createRange()
range.selectNodeContents(el)
replaceNode(range.extractContents(), el)
}
// undo the effect of `wrapRangeText`, given a resulting array of wrapper `nodes`
function undo(nodes) {
nodes.forEach(function (node) {
var parent = node.parentNode
unwrap(node)
parent.normalize()
})
}
// create a node wrapper function
function createWrapperFunction(wrapperEl, range) {
var startNode = range.startContainer
, endNode = range.endContainer
, startOffset = range.startOffset
, endOffset = range.endOffset
return function wrapNode(node) {
var currentRange = document.createRange()
, currentWrapper = wrapperEl.cloneNode()
currentRange.selectNodeContents(node)
if (node === startNode && startNode.nodeType === 3) {
currentRange.setStart(node, startOffset)
startNode = currentWrapper
startOffset = 0
}
if (node === endNode && endNode.nodeType === 3) {
currentRange.setEnd(node, endOffset)
endNode = currentWrapper
endOffset = 1
}
currentRange.surroundContents(currentWrapper)
return currentWrapper
}
}
function wrapRangeText(wrapperEl, range) {
var nodes
, wrapNode
, wrapperObj = {}
if (typeof range === 'undefined') {
// get the current selection if no range is specified
range = window.getSelection().getRangeAt(0)
}
if (range.isCollapsed) {
// nothing to wrap
return []
}
if (typeof wrapperEl === 'undefined') {
wrapperEl = 'span'
}
if (typeof wrapperEl === 'string') {
// assume it's a tagname
wrapperEl = document.createElement(wrapperEl)
}
wrapNode = createWrapperFunction(wrapperEl, range)
nodes = getRangeTextNodes(range)
nodes = nodes.map(wrapNode)
wrapperObj.nodes = nodes
wrapperObj.unwrap = function () {
if (this.nodes.length) {
undo(this.nodes)
this.nodes = []
}
}
return wrapperObj
}
module.exports = wrapRangeText