UNPKG

@funktechno/texthighlighter

Version:

a no dependency typescript supported tool for highlighting user selected content

438 lines (437 loc) 14.3 kB
"use strict"; // highlight extensions // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase // type H_HTMLElement = HTMLElement; // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase // interface ie_HTMLElement extends HTMLElement { // createTextRange(): TextRange; // } Object.defineProperty(exports, "__esModule", { value: true }); exports.activator = exports.groupHighlights = exports.defaults = exports.haveSameColor = exports.unique = exports.sortByDepth = exports.refineRangeBoundaries = exports.dom = exports.IGNORE_TAGS = exports.NODE_TYPE = exports.TIMESTAMP_ATTR = exports.DATA_ATTR = exports.unbindEvents = exports.bindEvents = void 0; // eslint-disable-next-line @typescript-eslint/class-name-casing // interface H_Node extends Node { // splitText(endOffset: number): any; // } // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase // interface H_Window extends Window { // find(text: any, caseSens: any): boolean; // } var /** * Attribute added by default to every highlight. * @type {string} */ DATA_ATTR = "data-highlighted", /** * Attribute used to group highlight wrappers. * @type {string} */ TIMESTAMP_ATTR = "data-timestamp", NODE_TYPE = { ELEMENT_NODE: 1, TEXT_NODE: 3 }, /** * Don't highlight content of these tags. * @type {string[]} */ IGNORE_TAGS = [ "SCRIPT", "STYLE", "SELECT", "OPTION", "BUTTON", "OBJECT", "APPLET", "VIDEO", "AUDIO", "CANVAS", "EMBED", "PARAM", "METER", "PROGRESS" ]; exports.DATA_ATTR = DATA_ATTR; exports.TIMESTAMP_ATTR = TIMESTAMP_ATTR; exports.NODE_TYPE = NODE_TYPE; exports.IGNORE_TAGS = IGNORE_TAGS; function activator(type) { return new type(); } exports.activator = activator; /** * Groups given highlights by timestamp. * @param {Array} highlights * @returns {Array} Grouped highlights. */ function groupHighlights(highlights) { var order = [], chunks = {}, grouped = []; highlights.forEach(function (hl) { var timestamp = hl.getAttribute(TIMESTAMP_ATTR); if (typeof chunks[timestamp] === "undefined") { chunks[timestamp] = []; order.push(timestamp); } chunks[timestamp].push(hl); }); order.forEach(function (timestamp) { var group = chunks[timestamp]; grouped.push({ chunks: group, timestamp: timestamp, toString: function () { return group .map(function (h) { return h.textContent; }) .join(""); } }); }); return grouped; } exports.groupHighlights = groupHighlights; /** * Fills undefined values in obj with default properties with the same name from source object. * @param {object} obj - target object, can't be null, must be initialized first * @param {object} source - source object with default values * @returns {object} */ function defaults(obj, source) { if (obj == null) obj = {}; for (var prop in source) { if (Object.prototype.hasOwnProperty.call(source, prop) && obj[prop] === void 0) { obj[prop] = source[prop]; } } return obj; } exports.defaults = defaults; /** * Returns array without duplicated values. * @param {Array} arr * @returns {Array} */ function unique(arr) { return arr.filter(function (value, idx, self) { return self.indexOf(value) === idx; }); } exports.unique = unique; /** * Takes range object as parameter and refines it boundaries * @param range * @returns {object} refined boundaries and initial state of highlighting algorithm. */ function refineRangeBoundaries(range) { var startContainer = range.startContainer, endContainer = range.endContainer, goDeeper = true; var ancestor = range.commonAncestorContainer; if (range.endOffset === 0) { while (endContainer && !endContainer.previousSibling && endContainer.parentNode !== ancestor) { endContainer = endContainer.parentNode; } if (endContainer) endContainer = endContainer.previousSibling; } else if (endContainer.nodeType === NODE_TYPE.TEXT_NODE) { if (endContainer && endContainer.nodeValue && range.endOffset < endContainer.nodeValue.length) { var t = endContainer; t.splitText(range.endOffset); } } else if (range.endOffset > 0) { endContainer = endContainer.childNodes.item(range.endOffset - 1); } if (startContainer.nodeType === NODE_TYPE.TEXT_NODE) { if (startContainer && startContainer.nodeValue && range.startOffset === startContainer.nodeValue.length) { goDeeper = false; } else if (startContainer instanceof Node && range.startOffset > 0) { var t = startContainer; startContainer = t.splitText(range.startOffset); if (startContainer && endContainer === startContainer.previousSibling) { endContainer = startContainer; } } } else if (range.startOffset < startContainer.childNodes.length) { startContainer = startContainer.childNodes.item(range.startOffset); } else { startContainer = startContainer.nextSibling; } return { startContainer: startContainer, endContainer: endContainer, goDeeper: goDeeper }; } exports.refineRangeBoundaries = refineRangeBoundaries; function bindEvents(el, scope) { el.addEventListener("mouseup", scope.highlightHandler.bind(scope)); el.addEventListener("touchend", scope.highlightHandler.bind(scope)); } exports.bindEvents = bindEvents; function unbindEvents(el, scope) { el.removeEventListener("mouseup", scope.highlightHandler.bind(scope)); el.removeEventListener("touchend", scope.highlightHandler.bind(scope)); } exports.unbindEvents = unbindEvents; /** * Utility functions to make DOM manipulation easier. * @param {Node|HTMLElement} [el] - base DOM element to manipulate * @returns {object} */ var dom = function (el) { return /** @lends dom **/ { /** * Adds class to element. * @param {string} className */ addClass: function (className) { if (el instanceof HTMLElement) if (el.classList) { el.classList.add(className); } else { el.className += " " + className; } }, /** * Removes class from element. * @param {string} className */ removeClass: function (className) { if (el instanceof HTMLElement) { if (el.classList) { el.classList.remove(className); } else { el.className = el.className.replace(new RegExp("(^|\\b)" + className + "(\\b|$)", "gi"), " "); } } }, /** * Prepends child nodes to base element. * @param {Node[]} nodesToPrepend */ prepend: function (nodesToPrepend) { var nodes = Array.prototype.slice.call(nodesToPrepend); var i = nodes.length; if (el) while (i--) { el.insertBefore(nodes[i], el.firstChild); } }, /** * Appends child nodes to base element. * @param {Node[]} nodesToAppend */ append: function (nodesToAppend) { if (el) { var nodes = Array.prototype.slice.call(nodesToAppend); for (var i = 0, len = nodes.length; i < len; ++i) { el.appendChild(nodes[i]); } } }, /** * Inserts base element after refEl. * @param {Node} refEl - node after which base element will be inserted * @returns {Node} - inserted element */ insertAfter: function (refEl) { return refEl.parentNode.insertBefore(el, refEl.nextSibling); }, /** * Inserts base element before refEl. * @param {Node} refEl - node before which base element will be inserted * @returns {Node} - inserted element */ insertBefore: function (refEl) { return refEl.parentNode ? refEl.parentNode.insertBefore(el, refEl) : refEl; }, /** * Removes base element from DOM. */ remove: function () { if (el && el.parentNode) { el.parentNode.removeChild(el); el = null; } }, /** * Returns true if base element contains given child. * @param {Node|HTMLElement} child * @returns {boolean} */ contains: function (child) { return el && el !== child && el.contains(child); }, /** * Wraps base element in wrapper element. * @param {HTMLElement} wrapper * @returns {HTMLElement} wrapper element */ wrap: function (wrapper) { if (el) { if (el.parentNode) { el.parentNode.insertBefore(wrapper, el); } wrapper.appendChild(el); } return wrapper; }, /** * Unwraps base element. * @returns {Node[]} - child nodes of unwrapped element. */ unwrap: function () { if (el) { var nodes = Array.prototype.slice.call(el.childNodes); var wrapper_1; // debugger; nodes.forEach(function (node) { wrapper_1 = node.parentNode; var d = dom(node); if (d && node.parentNode) d.insertBefore(node.parentNode); dom(wrapper_1).remove(); }); return nodes; } }, /** * Returns array of base element parents. * @returns {HTMLElement[]} */ parents: function () { var parent; var path = []; if (el) { while ((parent = el.parentNode)) { path.push(parent); el = parent; } } return path; }, /** * Normalizes text nodes within base element, ie. merges sibling text nodes and assures that every * element node has only one text node. * It should does the same as standard element.normalize, but IE implements it incorrectly. */ normalizeTextNodes: function () { if (!el) { return; } if (el.nodeType === NODE_TYPE.TEXT_NODE && el.nodeValue && el.parentNode) { while (el.nextSibling && el.nextSibling.nodeType === NODE_TYPE.TEXT_NODE) { el.nodeValue += el.nextSibling.nodeValue; el.parentNode.removeChild(el.nextSibling); } } else { dom(el.firstChild).normalizeTextNodes(); } dom(el.nextSibling).normalizeTextNodes(); }, /** * Returns element background color. * @returns {CSSStyleDeclaration.backgroundColor} */ color: function () { return el instanceof HTMLElement && el.style ? el.style.backgroundColor : null; }, /** * Creates dom element from given html string. * @param {string} html * @returns {NodeList} */ fromHTML: function (html) { var div = document.createElement("div"); div.innerHTML = html; return div.childNodes; }, /** * Returns first range of the window of base element. * @returns {Range} */ getRange: function () { var selection = dom(el).getSelection(); var range; if (selection && selection.rangeCount > 0) { range = selection.getRangeAt(0); } return range; }, /** * Removes all ranges of the window of base element. */ removeAllRanges: function () { var selection = dom(el).getSelection(); if (selection) selection.removeAllRanges(); }, /** * Returns selection object of the window of base element. * @returns {Selection} */ getSelection: function () { var win = dom(el).getWindow(); return win ? win.getSelection() : null; }, /** * Returns window of the base element. * @returns {Window} */ getWindow: function () { var doc = dom(el).getDocument(); return doc instanceof Document ? doc.defaultView : null; }, /** * Returns document of the base element. * @returns {HTMLDocument} */ getDocument: function () { // if ownerDocument is null then el is the document itself. if (el) return el.ownerDocument || el; } }; }; exports.dom = dom; /** * Returns true if elements a i b have the same color. * @param {Node} a * @param {Node} b * @returns {boolean} */ function haveSameColor(a, b) { return dom(a).color() === dom(b).color(); } exports.haveSameColor = haveSameColor; /** * Sorts array of DOM elements by its depth in DOM tree. * @param {HTMLElement[]} arr - array to sort. * @param {boolean} descending - order of sort. */ function sortByDepth(arr, descending) { arr.sort(function (a, b) { return (dom(descending ? b : a).parents().length - dom(descending ? a : b).parents().length); }); } exports.sortByDepth = sortByDepth;