selection-ranges
Version:
Selection range manipulation for contenteditable elements
802 lines (648 loc) • 14.9 kB
JavaScript
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
/**
* Expose `xor`
*/
var componentXor;
var hasRequiredComponentXor;
function requireComponentXor () {
if (hasRequiredComponentXor) return componentXor;
hasRequiredComponentXor = 1;
componentXor = xor;
/**
* XOR utility
*
* T T F
* T F T
* F T T
* F F F
*
* @param {Boolean} a
* @param {Boolean} b
* @return {Boolean}
*/
function xor(a, b) {
return a ^ b;
}
return componentXor;
}
/**
* Global Names
*/
var componentProps;
var hasRequiredComponentProps;
function requireComponentProps () {
if (hasRequiredComponentProps) return componentProps;
hasRequiredComponentProps = 1;
var globals = /\b(Array|Date|Object|Math|JSON)\b/g;
/**
* Return immediate identifiers parsed from `str`.
*
* @param {String} str
* @param {String|Function} map function or prefix
* @return {Array}
* @api public
*/
componentProps = function(str, fn){
var p = unique(props(str));
if (fn && 'string' == typeof fn) fn = prefixed(fn);
if (fn) return map(str, p, fn);
return p;
};
/**
* Return immediate identifiers in `str`.
*
* @param {String} str
* @return {Array}
* @api private
*/
function props(str) {
return str
.replace(/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\//g, '')
.replace(globals, '')
.match(/[a-zA-Z_]\w*/g)
|| [];
}
/**
* Return `str` with `props` mapped with `fn`.
*
* @param {String} str
* @param {Array} props
* @param {Function} fn
* @return {String}
* @api private
*/
function map(str, props, fn) {
var re = /\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\/|[a-zA-Z_]\w*/g;
return str.replace(re, function(_){
if ('(' == _[_.length - 1]) return fn(_);
if (!~props.indexOf(_)) return _;
return fn(_);
});
}
/**
* Return unique array.
*
* @param {Array} arr
* @return {Array}
* @api private
*/
function unique(arr) {
var ret = [];
for (var i = 0; i < arr.length; i++) {
if (~ret.indexOf(arr[i])) continue;
ret.push(arr[i]);
}
return ret;
}
/**
* Map with prefix `str`.
*/
function prefixed(str) {
return function(_){
return str + _;
};
}
return componentProps;
}
/**
* Module Dependencies
*/
var domIterator;
var hasRequiredDomIterator;
function requireDomIterator () {
if (hasRequiredDomIterator) return domIterator;
hasRequiredDomIterator = 1;
var xor = requireComponentXor();
var props = requireComponentProps();
/**
* Export `Iterator`
*/
domIterator = Iterator;
/**
* Initialize `Iterator`
*
* @param {Node} node
* @param {Node} root
* @return {Iterator} self
* @api public
*/
function Iterator(node, root) {
if (!(this instanceof Iterator)) return new Iterator(node, root);
this.node = this.start = this.peeked = node;
this.root = root;
this.closingTag = false;
this._revisit = true;
this._selects = [];
this._rejects = [];
if (node && this.higher(node)) {
throw new Error('root must be a parent or ancestor to node');
}
}
/**
* Reset the Iterator
*
* @param {Node} node (optional)
* @return {Iterator} self
* @api public
*/
Iterator.prototype.reset = function(node) {
this.node = node || this.start;
return this;
};
/**
* Revisit element nodes. Defaults to `true`
*/
Iterator.prototype.revisit = function(revisit) {
this._revisit = undefined == revisit ? true : revisit;
return this;
};
/**
* Jump to the opening tag
*/
Iterator.prototype.opening = function() {
if (1 == this.node.nodeType) this.closingTag = false;
return this;
};
/**
* Jump to the closing tag
*/
Iterator.prototype.atOpening = function() {
return !this.closingTag;
};
/**
* Jump to the closing tag
*/
Iterator.prototype.closing = function() {
if (1 == this.node.nodeType) this.closingTag = true;
return this;
};
/**
* Jump to the closing tag
*/
Iterator.prototype.atClosing = function() {
return this.closingTag;
};
/**
* Next node
*
* @param {Number} type
* @return {Node|null}
* @api public
*/
Iterator.prototype.next = traverse('nextSibling', 'firstChild');
/**
* Previous node
*
* @param {Number} type
* @return {Node|null}
* @api public
*/
Iterator.prototype.previous =
Iterator.prototype.prev = traverse('previousSibling', 'lastChild');
/**
* Make traverse function
*
* @param {String} dir
* @param {String} child
* @return {Function}
* @api private
*/
function traverse(dir, child) {
var next = dir == 'nextSibling';
return function walk(expr, n, peek) {
expr = this.compile(expr);
n = n && n > 0 ? n : 1;
var node = this.node;
var closing = this.closingTag;
var revisit = this._revisit;
while (node) {
if (xor(next, closing) && node[child]) {
// element with children: <em>...</em>
node = node[child];
closing = !next;
} else if (1 == node.nodeType && !node[child] && xor(next, closing)) {
// empty element tag: <em></em>
closing = next;
if (!revisit) continue;
} else if (node[dir]) {
// element has a neighbor: ...<em></em>...
node = node[dir];
closing = !next;
} else {
// done with current layer, move up.
node = node.parentNode;
closing = next;
if (!revisit) continue;
}
if (!node || this.higher(node, this.root)) break;
if (expr(node) && this.selects(node, peek) && this.rejects(node, peek)) {
if (--n) continue;
if (!peek) this.node = node;
this.closingTag = closing;
return node;
}
}
return null;
};
}
/**
* Select nodes that cause `expr(node)`
* to be truthy
*
* @param {Number|String|Function} expr
* @return {Iterator} self
* @api public
*/
Iterator.prototype.select = function(expr) {
expr = this.compile(expr);
this._selects.push(expr);
return this;
};
/**
* Run through the selects ORing each
*
* @param {Node} node
* @param {Boolean} peek
* @return {Boolean}
* @api private
*/
Iterator.prototype.selects = function(node, peek) {
var exprs = this._selects;
var len = exprs.length;
if (!len) return true;
for (var i = 0; i < len; i++) {
if (exprs[i].call(this, node, peek)) return true;
}
return false;
};
/**
* Select nodes that cause `expr(node)`
* to be falsy
*
* @param {Number|String|Function} expr
* @return {Iterator} self
* @api public
*/
Iterator.prototype.reject = function(expr) {
expr = this.compile(expr);
this._rejects.push(expr);
return this;
};
/**
* Run through the reject expressions ANDing each
*
* @param {Node} node
* @param {Boolean} peek
* @return {Boolean}
* @api private
*/
Iterator.prototype.rejects = function(node, peek) {
var exprs = this._rejects;
var len = exprs.length;
if (!len) return true;
for (var i = 0; i < len; i++) {
if (exprs[i].call(this, node, peek)) return false;
}
return true;
};
/**
* Check if node is higher
* than root.
*
* @param {Node} node
* @param {Node} root
* @return {Boolean}
* @api private
*/
Iterator.prototype.higher = function(node) {
var root = this.root;
if (!root) return false;
node = node.parentNode;
while (node && node != root) node = node.parentNode;
return node != root;
};
/**
* Compile an expression
*
* @param {String|Function|Number} expr
* @return {Function}
*/
Iterator.prototype.compile = function(expr) {
switch (typeof expr) {
case 'number':
return function(node) { return expr == node.nodeType; };
case 'string':
return new Function('node', 'Object.freeze(node); return ' + props(expr, 'node.'));
case 'function':
return expr;
default:
return function() { return true; };
}
};
/**
* Peek in either direction
* `n` nodes. Peek backwards
* using negative numbers.
*
* @param {Number} n (optional)
* @return {Node|null}
* @api public
*/
Iterator.prototype.peak =
Iterator.prototype.peek = function(expr, n) {
if (arguments.length == 1) n = expr, expr = true;
n = undefined == n ? 1 : n;
if (!n) return this.node;
else if (n > 0) return this.next(expr, n, true);
else return this.prev(expr, Math.abs(n), true);
};
/**
* Add a plugin
*
* @param {Function} fn
* @return {Iterator}
* @api public
*/
Iterator.prototype.use = function(fn) {
fn(this);
return this;
};
return domIterator;
}
var domIteratorExports = requireDomIterator();
var iterator = /*@__PURE__*/getDefaultExportFromCjs(domIteratorExports);
/**
* Module Dependencies
*/
var selection = window.getSelection();
/**
* @typedef { { start: number, end: number } } SelectionRange
*/
/**
* Add selection / insert cursor.
*
* @param {Range} range
*/
function applyRange(range) {
selection?.removeAllRanges();
selection?.addRange(range);
}
/**
* Get current document selection.
*
* @return {Selection}
*/
function getWindowSelection() {
return selection;
}
/**
* Return true if element is part of window selection.
*
* @param {Element} el
* @return {boolean}
*/
function isSelected(el) {
if (!selection?.rangeCount) {
return null;
}
var focusNode = selection.focusNode;
// IE supports Node#contains for elements only
// thus we ensure we check against an actual Element node
if (isText(focusNode)) {
focusNode = focusNode.parentNode;
}
return el == focusNode || el.contains(focusNode);
}
/**
* Set cursor or selection position.
*
* @param {Element} el
* @param {SelectionRange} selectionRange
*/
function setRange(el, selectionRange) {
var range = createRange(el, selectionRange);
applyRange(range);
}
/**
* Get cursor or selection position.
*
* Returns `null` if the element is not currently selected
*
* @param {Element} el
*
* @return {SelectionRange|null}
*/
function getRange(el) {
if (!isSelected(el)) {
return null;
}
var range = selection.getRangeAt(0);
var startContainer = range.startContainer;
var endContainer = range.endContainer;
var startOffset = range.startOffset;
var endOffset = range.endOffset;
var i = iterator(el.firstChild, el);
var next = i.node;
var last;
var isClosing = false;
var selectionStart;
var count = 0;
function isBeforeEnd(node, referenceNode) {
if (arguments.length === 1) {
referenceNode = node;
}
return (
node.parentNode === endContainer &&
referenceNode == endContainer.childNodes[endOffset]
);
}
function isBeforeStart(node, referenceNode) {
if (arguments.length === 1) {
referenceNode = node;
}
return (
node.parentNode === startContainer &&
referenceNode == startContainer.childNodes[startOffset]
);
}
while (next) {
// start before node
if (isBeforeStart(next) && !isClosing) {
selectionStart = count;
}
// end before node
if (isBeforeEnd(next)) {
break;
}
if (isBr(next) && endContainer === next) {
break;
}
if (!isClosing) {
if (
isBr(next) || (
last && (last.nextSibling == next) && (
isDiv(next) ||
isParagraph(next)
)
)
) {
count++;
}
}
if (isText(next)) {
// #text node
if (startContainer === next) {
selectionStart = count + startOffset;
}
if (endContainer === next) {
count += endOffset;
break;
}
count += next.textContent.length;
}
if (isText(next) || isClosing) {
// start before node
if (isBeforeStart(next, next.nextSibling)) {
selectionStart = count;
}
// end before node
if (isBeforeEnd(next, next.nextSibling)) {
break;
}
}
last = next;
next = i.next();
isClosing = i.closingTag;
}
// selection until end of text
return {
start: typeof selectionStart === 'undefined' ? count : selectionStart,
end: count
};
}
/**
* Annotate the given text with markers based on the
* given range.
*
* @param {string} text
* @param {SelectionRange} range
*
* @return {string} annotated text
*/
function annotateRange(text, range) {
var str;
if (range.start === range.end) {
str = (
text.substring(0, range.start) +
'|' +
text.substring(range.start)
);
} else {
str = (
text.substring(0, range.start) +
'<' +
text.substring(range.start, range.end) +
'>' +
text.substring(range.end)
);
}
return str;
}
// helpers ///////////////////////////
/**
* @param {Element} el
* @param {SelectionRange} selection
*
* @return {Range}
*/
function createRange(el, selection) {
var start = selection.start;
var end = selection.end;
var range = document.createRange();
var i = iterator(el.firstChild, el);
var next = i.node;
var isClosing = false;
var count = 0;
var length;
while (next) {
if (count === start) {
if (isClosing) {
range.setStartAfter(next);
} else {
range.setStartBefore(next);
}
}
if (count === end) {
if (isClosing) {
range.setEndAfter(next);
} else {
range.setEndBefore(next);
}
return range;
}
if (!isClosing) {
if (
isBr(next) || (
next.previousSibling && (
isDiv(next) ||
isParagraph(next)
)
)
) {
count++;
}
}
if (isText(next)) {
length = next.textContent.length;
if (count <= start && count + length > start) {
range.setStart(next, start - count);
}
if (count + length > end) {
range.setEnd(next, end - count);
return range;
}
count += length;
}
next = i.next();
isClosing = i.closingTag;
}
// out of range
if (count <= start) {
if (el.lastChild) {
range.setStartAfter(el.lastChild);
} else {
range.setStart(el, 0);
}
}
if (el.lastChild) {
range.setEndAfter(el.lastChild);
} else {
range.setEnd(el, 0);
}
return range;
}
function isText(node) {
return node.nodeType === 3;
}
function isBr(node) {
return (
node.nodeType === 1 &&
node.nodeName === 'BR'
);
}
function isDiv(node) {
return (
node.nodeType === 1 &&
node.nodeName === 'DIV'
);
}
function isParagraph(node) {
return (
node.nodeType === 1 &&
node.nodeName === 'P'
);
}
export { annotateRange, applyRange, getRange, getWindowSelection, isSelected, setRange };
//# sourceMappingURL=index.js.map