├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_template.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── pipeline.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── archived ├── TextHighligher.ts └── TextHighlighter.js ├── build └── replace.js ├── client └── index.ts ├── demos ├── assets │ ├── ColorPicker.css │ ├── ColorPicker.js │ ├── github.css │ ├── img.jpg │ ├── rainbow-custom.min.js │ ├── reset.css │ ├── setYear.js │ └── styles.css ├── callbacks.html ├── iframe-content.html ├── iframe.html ├── serialization.html └── simple.html ├── dist └── TextHighlighter.js ├── jest.config.js ├── lib ├── Library.d.ts ├── Library.js ├── TextHighlighter.d.ts ├── TextHighlighter.js ├── Utils.d.ts ├── Utils.js ├── index.d.ts ├── index.js └── types │ ├── index.d.ts │ └── index.js ├── package-lock.json ├── package.json ├── src ├── Library.ts ├── TextHighlighter.ts ├── Utils.ts ├── index.ts └── types │ └── index.ts ├── tests ├── data │ └── results │ │ └── doHighlight.json ├── index.spec.ts └── utils │ ├── helper.ts │ └── types.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "rules": { 9 | "semi": ["error", "always"], 10 | "quotes": ["error", "double"], 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "@typescript-eslint/camelcase": "off", 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "no-multiple-empty-lines": "error", 15 | "@typescript-eslint/no-inferrable-types": [ 16 | "warn", { 17 | "ignoreParameters": true 18 | } 19 | ], 20 | "@typescript-eslint/no-unused-vars": "warn", 21 | "@typescript-eslint/class-name-casing": "off" 22 | }, 23 | "ignorePatterns": ["src/TextHighlighter.js","lib/*","dist/*","archived"], 24 | "overrides": [ 25 | { 26 | "files": [ 27 | "**/__tests__/*.{j,t}s?(x)", 28 | "**/tests/**/*.spec.{j,t}s?(x)" 29 | ], 30 | "env": { 31 | "jest": true 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | This issue is: 2 | 3 | - [ ] Bug report 4 | - [ ] Feature request 5 | - [ ] Improvement 6 | 7 | ### Actual behaviour 8 | 9 | 10 | ### Expected behaviour 11 | 12 | 13 | ### Steps to reproduce 14 | 15 | 16 | ### Screenshots 17 | 18 | 19 | ### Logs 20 | * attach export from options screen. Can replace urls and repo names used. 21 | 22 | ### Configuration 23 | 24 | - OS: 25 | - Browser: 26 | - Browser version: 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | Changes proposed in this pull request: 4 | - 5 | - 6 | - -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "@types/node" 10 | versions: 11 | - 14.14.22 12 | - 14.14.24 13 | - 14.14.25 14 | - 14.14.26 15 | - 14.14.28 16 | - 14.14.30 17 | - 14.14.31 18 | - 14.14.32 19 | - 14.14.33 20 | - 14.14.34 21 | - 14.14.35 22 | - 14.14.36 23 | - 14.14.37 24 | - 14.14.39 25 | - 14.14.41 26 | - 15.0.0 27 | - dependency-name: "@types/eslint" 28 | versions: 29 | - 7.2.6 30 | - 7.2.7 31 | - 7.2.8 32 | - 7.2.9 33 | - dependency-name: y18n 34 | versions: 35 | - 4.0.1 36 | - 4.0.2 37 | - dependency-name: "@types/jest" 38 | versions: 39 | - 26.0.20 40 | - 26.0.21 41 | - 26.0.22 42 | - dependency-name: ts-jest 43 | versions: 44 | - 26.4.4 45 | - 26.5.0 46 | - 26.5.1 47 | - 26.5.2 48 | - 26.5.3 49 | - 26.5.4 50 | - dependency-name: typescript 51 | versions: 52 | - 4.1.3 53 | - 4.1.4 54 | - 4.1.5 55 | - 4.2.2 56 | - 4.2.3 57 | - dependency-name: node-notifier 58 | versions: 59 | - 8.0.1 60 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ dev, master* ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ dev ] 20 | # schedule: 21 | # - cron: '30 5,17 * * *' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Build Pipeline 2 | 3 | on: 4 | # [pull_request, push] 5 | pull_request: 6 | push: 7 | branches: 8 | - master # Push events on master branch 9 | - dev 10 | 11 | # use https://marketplace.visualstudio.com/items?itemName=me-dutour-mathieu.vscode-github-actions to validate yml in vscode 12 | # env: 13 | 14 | jobs: 15 | 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Cancel Previous Runs 20 | uses: styfle/cancel-workflow-action@0.6.0 21 | with: 22 | access_token: ${{ github.token }} 23 | - uses: actions/checkout@v1 24 | - name: Read .nvmrc 25 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 26 | id: nvm 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 30 | - run: yarn install 31 | - run: npm run test 32 | - name: upload junit 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: junit 36 | path: junit.xml 37 | - uses: ashley-taylor/junit-report-annotations-action@1.3 38 | if: always() 39 | with: 40 | access-token: ${{ secrets.GITHUB_TOKEN }} 41 | path: junit.xml 42 | - run: npm run test:coverage 43 | - name: upload code coverage 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: Report-CodeCoverage 47 | path: coverage 48 | - run: npm run build 49 | - name: build lib,dist 50 | run: | 51 | npm run build 52 | npm run build:client 53 | - name: upload lib 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: lib 57 | path: lib 58 | - name: upload dist 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: dist 62 | path: dist -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Push events on master branch 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Read .nvmrc 14 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 15 | id: nvm 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 19 | - run: yarn install 20 | - run: npm run test 21 | - name: upload junit 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: junit 25 | path: junit.xml 26 | - uses: ashley-taylor/junit-report-annotations-action@1.3 27 | if: always() 28 | with: 29 | access-token: ${{ secrets.GITHUB_TOKEN }} 30 | path: junit.xml 31 | - uses: JS-DevTools/npm-publish@v1 32 | with: 33 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Coverage reports 4 | coverage 5 | 6 | junit.xml 7 | yarn.lock 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | tslint.json 4 | .prettierrc -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @funktechno:registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.15.0 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest single run", 11 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 12 | "args": [ 13 | "-c", 14 | "./jest.config.js", 15 | "--verbose", 16 | "-i", 17 | "--no-cache" 18 | ], 19 | "console": "integratedTerminal", 20 | "internalConsoleOptions": "neverOpen" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Jest watch run", 26 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 27 | "args": [ 28 | "-c", 29 | "./jest.config.js", 30 | "--verbose", 31 | "-i", 32 | "--no-cache", 33 | "--watchAll" 34 | ], 35 | "console": "integratedTerminal", 36 | "internalConsoleOptions": "neverOpen" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## 2.1.2 (2021-08-02) 6 | 7 | ### Bug Fixes 8 | 9 | * fix relative paths 10 | 11 | ## 2.1.1 (2021-07-30) 12 | 13 | ### Bug Fixes 14 | 15 | * crossing paragraph bug fix 16 | * update versions -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 - 2014 mirz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # texthighlighter 2 | * a **no dependency** typescript supported tool for highlighting user selected content, removing highlights, serializing existing highlights, and applying serialized highlights. Works on mobile device. 3 | * a no dependency typescript port of https://github.com/mir3z/texthighlighter because typescript is amazing. Some bugs were even fixed by just converting to typescript. Functions were converted to not need class instantiation. Followed article https://itnext.io/step-by-step-building-and-publishing-an-npm-typescript-package-44fe7164964c on setting up a typescript package after testing my port on my own project. Pull requests are welcome! 4 | 5 | ## Usage 6 | * `npm install @funktechno/texthighlighter` 7 | * in code 8 | 9 | ```js 10 | import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, optionsImpl } from "@/../node_modules/@funktechno/texthighlighter/lib/index"; 11 | const domEle = document.getElementById("sandbox"); 12 | const options: optionsImpl = {}; 13 | if (this.color) options.color = this.color; 14 | if (domEle) doHighlight(domEle, true, options); 15 | ``` 16 | 17 | In project need to set up own mouseup and touchend events. touchend is for mobile 18 | 19 | vue example 20 | ```html 21 |
27 | ``` 28 | 29 | vue script 30 | ```js 31 | var methods = { 32 | runMobileHighlight(:any){ 33 | const selection = document.getSelection(); 34 | if (selection) { 35 | const domEle = document.getElementById("sandbox"); 36 | const options: optionsImpl = {}; 37 | if (domEle) { 38 | const highlightMade = doHighlight(domEle, true, options); 39 | } 40 | } 41 | 42 | }, 43 | runHighlight(:any){ 44 | // run mobile a bit different 45 | if (this.isMobile()) return; 46 | const domEle = document.getElementById("sandbox"); 47 | const options: optionsImpl = {}; 48 | if (domEle) { 49 | const highlightMade = doHighlight(domEle, true, options); 50 | } 51 | } 52 | } 53 | 54 | ``` 55 | 56 | ## Demos 57 | 58 | * [Simple demo](http://funktechno.github.io/texthighlighter/demos/simple.html) 59 | * [Callbacks](http://funktechno.github.io/texthighlighter/demos/callbacks.html) 60 | * [Serialization](http://funktechno.github.io/texthighlighter/demos/serialization.html) 61 | 62 | ## development 63 | * `npm install` or `yarn import` 64 | * `npm run build` 65 | * `npm run build:client` 66 | * `npm run lint:fix` 67 | * `yarn run live-server` 68 | 69 | ## deploy 70 | * npm publish --access public 71 | 72 | ## todo 73 | * [x] convert library to typescript 74 | * works where it's being used (in a vue ts project) 75 | * [x] bring over demos and examples 76 | * [ ] improve unit tests 77 | * [x] pipeline 78 | * [ ] bring over previous unit tests 79 | * [ ] extend highlights for more options 80 | * [ ] whole element selection for comment support, or keeprange disabled 81 | -------------------------------------------------------------------------------- /archived/TextHighligher.ts: -------------------------------------------------------------------------------- 1 | // (function (global) { 2 | // TextHighlighter (ARCHIVED, not working) 3 | // original file first converted to typescript that doesn't throw any lint errors. 4 | // Had issues doing a new class of it so extracted methods and clean functions into utils file 5 | "use strict"; 6 | import { 7 | TIMESTAMP_ATTR, 8 | IGNORE_TAGS, 9 | NODE_TYPE, 10 | DATA_ATTR, 11 | dom, 12 | refineRangeBoundaries, 13 | sortByDepth, 14 | unique, 15 | haveSameColor, 16 | optionsI, 17 | paramsImp, 18 | highlightI, 19 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 20 | H_Window, 21 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 22 | ie_HTMLElement, 23 | TextRange, 24 | // optionsImpl, 25 | // activator, 26 | defaults, 27 | groupHighlights 28 | } from "./TextHighlighterUtils"; 29 | 30 | // { highlightHandler: { bind: (arg0: any) => any }} 31 | function bindEvents( 32 | el: { addEventListener: (arg0: string, arg1: any) => void }, 33 | scope: highlightI 34 | ) { 35 | el.addEventListener("mouseup", scope.highlightHandler.bind(scope)); 36 | el.addEventListener("touchend", scope.highlightHandler.bind(scope)); 37 | } 38 | 39 | function unbindEvents( 40 | el: { removeEventListener: (arg0: string, arg1: any) => void }, 41 | scope: { highlightHandler: { bind: (arg0: any) => any } } 42 | ) { 43 | el.removeEventListener("mouseup", scope.highlightHandler.bind(scope)); 44 | el.removeEventListener("touchend", scope.highlightHandler.bind(scope)); 45 | } 46 | 47 | /** 48 | * Creates TextHighlighter instance and binds to given DOM elements. 49 | * @param {HTMLElement} element - DOM element to which highlighted will be applied. 50 | * @param {object} [options] - additional options. 51 | * @param {string} options.color - highlight color. 52 | * @param {string} options.highlightedClass - class added to highlight, 'highlighted' by default. 53 | * @param {string} options.contextClass - class added to element to which highlighter is applied, 54 | * 'highlighter-context' by default. 55 | * @param {function} options.onRemoveHighlight - function called before highlight is removed. Highlight is 56 | * passed as param. Function should return true if highlight should be removed, or false - to prevent removal. 57 | * @param {function} options.onBeforeHighlight - function called before highlight is created. Range object is 58 | * passed as param. Function should return true to continue processing, or false - to prevent highlighting. 59 | * @param {function} options.onAfterHighlight - function called after highlight is created. Array of created 60 | * wrappers is passed as param. 61 | * @class TextHighlighter 62 | */ 63 | function TextHighlighter( 64 | this: highlightI, 65 | element: HTMLElement, 66 | options?: optionsI 67 | ) { 68 | if (!element) { 69 | throw "Missing anchor element"; 70 | } 71 | 72 | this.el = element; 73 | this.options = defaults(options, { 74 | color: "#ffff7b", 75 | highlightedClass: "highlighted", 76 | contextClass: "highlighter-context", 77 | onRemoveHighlight: function() { 78 | return true; 79 | }, 80 | onBeforeHighlight: function() { 81 | return true; 82 | }, 83 | onAfterHighlight: function() { 84 | return true; 85 | } 86 | }); 87 | if (this.options?.contextClass) 88 | dom(this.el).addClass(this.options.contextClass); 89 | bindEvents(this.el, this); 90 | return this; 91 | } 92 | 93 | /** 94 | * Permanently disables highlighting. 95 | * Unbinds events and remove context element class. 96 | * @memberof TextHighlighter 97 | */ 98 | TextHighlighter.prototype.destroy = function() { 99 | unbindEvents(this.el, this); 100 | dom(this.el).removeClass(this.options.contextClass); 101 | }; 102 | 103 | TextHighlighter.prototype.highlightHandler = function() { 104 | this.doHighlight(); 105 | }; 106 | 107 | /** 108 | * Highlights current range. 109 | * @param {boolean} keepRange - Don't remove range after highlighting. Default: false. 110 | * @memberof TextHighlighter 111 | */ 112 | TextHighlighter.prototype.doHighlight = function(keepRange: any) { 113 | const range = dom(this.el).getRange(); 114 | let wrapper, createdHighlights, normalizedHighlights, timestamp: string; 115 | 116 | if (!range || range.collapsed) { 117 | return; 118 | } 119 | 120 | if (this.options.onBeforeHighlight(range) === true) { 121 | timestamp = (+new Date()).toString(); 122 | wrapper = TextHighlighter.createWrapper(this.options); 123 | wrapper.setAttribute(TIMESTAMP_ATTR, timestamp); 124 | 125 | createdHighlights = this.highlightRange(range, wrapper); 126 | normalizedHighlights = this.normalizeHighlights(createdHighlights); 127 | 128 | this.options.onAfterHighlight(range, normalizedHighlights, timestamp); 129 | } 130 | 131 | if (!keepRange) { 132 | dom(this.el).removeAllRanges(); 133 | } 134 | }; 135 | 136 | /** 137 | * Highlights range. 138 | * Wraps text of given range object in wrapper element. 139 | * @param {Range} range 140 | * @param {HTMLElement} wrapper 141 | * @returns {Array} - array of created highlights. 142 | * @memberof TextHighlighter 143 | */ 144 | TextHighlighter.prototype.highlightRange = function( 145 | range: Range, 146 | wrapper: { cloneNode: (arg0: boolean) => any } 147 | ) { 148 | if (!range || range.collapsed) { 149 | return []; 150 | } 151 | 152 | const result = refineRangeBoundaries(range); 153 | const startContainer = result.startContainer, 154 | endContainer = result.endContainer, 155 | highlights = []; 156 | 157 | let goDeeper = result.goDeeper, 158 | done = false, 159 | node = startContainer, 160 | highlight, 161 | wrapperClone, 162 | nodeParent; 163 | 164 | do { 165 | if (node && goDeeper && node.nodeType === NODE_TYPE.TEXT_NODE) { 166 | if ( 167 | node.parentNode instanceof HTMLElement && 168 | node.parentNode.tagName && 169 | node.nodeValue && 170 | IGNORE_TAGS.indexOf(node.parentNode.tagName) === -1 && 171 | node.nodeValue.trim() !== "" 172 | ) { 173 | wrapperClone = wrapper.cloneNode(true); 174 | wrapperClone.setAttribute(DATA_ATTR, true); 175 | nodeParent = node.parentNode; 176 | 177 | // highlight if a node is inside the el 178 | if (dom(this.el).contains(nodeParent) || nodeParent === this.el) { 179 | highlight = dom(node).wrap(wrapperClone); 180 | highlights.push(highlight); 181 | } 182 | } 183 | 184 | goDeeper = false; 185 | } 186 | if ( 187 | node === endContainer && 188 | endContainer instanceof HTMLElement && 189 | !(endContainer.hasChildNodes() && goDeeper) 190 | ) { 191 | done = true; 192 | } 193 | 194 | if ( 195 | node instanceof HTMLElement && 196 | node.tagName && 197 | IGNORE_TAGS.indexOf(node.tagName) > -1 198 | ) { 199 | if ( 200 | endContainer instanceof HTMLElement && 201 | endContainer.parentNode === node 202 | ) { 203 | done = true; 204 | } 205 | goDeeper = false; 206 | } 207 | if (goDeeper && node instanceof HTMLElement && node.hasChildNodes()) { 208 | node = node.firstChild; 209 | } else if (node instanceof HTMLElement && node.nextSibling) { 210 | node = node.nextSibling; 211 | goDeeper = true; 212 | } else if (node instanceof HTMLElement) { 213 | node = node.parentNode; 214 | goDeeper = false; 215 | } 216 | } while (!done); 217 | 218 | return highlights; 219 | }; 220 | 221 | /** 222 | * Normalizes highlights. Ensures that highlighting is done with use of the smallest possible number of 223 | * wrapping HTML elements. 224 | * Flattens highlights structure and merges sibling highlights. Normalizes text nodes within highlights. 225 | * @param {Array} highlights - highlights to normalize. 226 | * @returns {Array} - array of normalized highlights. Order and number of returned highlights may be different than 227 | * input highlights. 228 | * @memberof TextHighlighter 229 | */ 230 | TextHighlighter.prototype.normalizeHighlights = function(highlights: any[]) { 231 | let normalizedHighlights; 232 | 233 | this.flattenNestedHighlights(highlights); 234 | this.mergeSiblingHighlights(highlights); 235 | 236 | // omit removed nodes 237 | normalizedHighlights = highlights.filter(function(hl: { 238 | parentElement: any; 239 | }) { 240 | return hl.parentElement ? hl : null; 241 | }); 242 | 243 | normalizedHighlights = unique(normalizedHighlights); 244 | normalizedHighlights.sort(function( 245 | a: { offsetTop: number; offsetLeft: number }, 246 | b: { offsetTop: number; offsetLeft: number } 247 | ) { 248 | return a.offsetTop - b.offsetTop || a.offsetLeft - b.offsetLeft; 249 | }); 250 | 251 | return normalizedHighlights; 252 | }; 253 | 254 | /** 255 | * Flattens highlights structure. 256 | * Note: this method changes input highlights - their order and number after calling this method may change. 257 | * @param {Array} highlights - highlights to flatten. 258 | * @memberof TextHighlighter 259 | */ 260 | TextHighlighter.prototype.flattenNestedHighlights = function( 261 | highlights: any[] 262 | ) { 263 | let again; 264 | // self = this; 265 | 266 | sortByDepth(highlights, true); 267 | 268 | const flattenOnce = () => { 269 | let again = false; 270 | 271 | highlights.forEach((hl: Node, i: number | number) => { 272 | const parent = hl.parentElement; 273 | if (parent) { 274 | const parentPrev = parent.previousSibling, 275 | parentNext = parent.nextSibling; 276 | 277 | if (this.isHighlight(parent)) { 278 | if (!haveSameColor(parent, hl)) { 279 | if (!hl.nextSibling && parentNext) { 280 | const newLocal: any = parentNext || parent; 281 | if (newLocal) { 282 | dom(hl).insertBefore(newLocal); 283 | again = true; 284 | } 285 | } 286 | 287 | if (!hl.previousSibling && parentPrev) { 288 | const newLocal: any = parentPrev || parent; 289 | if (newLocal) { 290 | dom(hl).insertAfter(newLocal); 291 | again = true; 292 | } 293 | } 294 | 295 | if (!parent.hasChildNodes()) { 296 | dom(parent).remove(); 297 | } 298 | } else { 299 | if (hl && hl.firstChild) parent.replaceChild(hl.firstChild, hl); 300 | highlights[i] = parent; 301 | again = true; 302 | } 303 | } 304 | } 305 | }); 306 | 307 | return again; 308 | }; 309 | 310 | do { 311 | again = flattenOnce(); 312 | } while (again); 313 | }; 314 | 315 | /** 316 | * Merges sibling highlights and normalizes descendant text nodes. 317 | * Note: this method changes input highlights - their order and number after calling this method may change. 318 | * @param highlights 319 | * @memberof TextHighlighter 320 | */ 321 | TextHighlighter.prototype.mergeSiblingHighlights = function(highlights: any[]) { 322 | // const self = this; 323 | 324 | const shouldMerge = (current: Node, node: Node) => { 325 | return ( 326 | node && 327 | node.nodeType === NODE_TYPE.ELEMENT_NODE && 328 | haveSameColor(current, node) && 329 | this.isHighlight(node) 330 | ); 331 | }; 332 | // : { 333 | // previousSibling: any; 334 | // nextSibling: any; 335 | // } 336 | highlights.forEach(function(highlight: any) { 337 | const prev = highlight.previousSibling, 338 | next = highlight.nextSibling; 339 | 340 | if (shouldMerge(highlight, prev)) { 341 | dom(highlight).prepend(prev.childNodes); 342 | dom(prev).remove(); 343 | } 344 | if (shouldMerge(highlight, next)) { 345 | dom(highlight).append(next.childNodes); 346 | dom(next).remove(); 347 | } 348 | 349 | dom(highlight).normalizeTextNodes(); 350 | }); 351 | }; 352 | 353 | /** 354 | * Sets highlighting color. 355 | * @param {string} color - valid CSS color. 356 | * @memberof TextHighlighter 357 | */ 358 | TextHighlighter.prototype.setColor = function(color: any) { 359 | this.options.color = color; 360 | }; 361 | 362 | /** 363 | * Returns highlighting color. 364 | * @returns {string} 365 | * @memberof TextHighlighter 366 | */ 367 | TextHighlighter.prototype.getColor = function() { 368 | return this.options.color; 369 | }; 370 | 371 | /** 372 | * Removes highlights from element. If element is a highlight itself, it is removed as well. 373 | * If no element is given, all highlights all removed. 374 | * @param {HTMLElement} [element] - element to remove highlights from 375 | * @memberof TextHighlighter 376 | */ 377 | TextHighlighter.prototype.removeHighlights = function(element: any) { 378 | const container = element || this.el, 379 | highlights = this.getHighlights({ container: container }); 380 | // self = this; 381 | 382 | function mergeSiblingTextNodes(textNode: { 383 | previousSibling: any; 384 | nextSibling: any; 385 | nodeValue: any; 386 | }) { 387 | const prev = textNode.previousSibling, 388 | next = textNode.nextSibling; 389 | 390 | if (prev && prev.nodeType === NODE_TYPE.TEXT_NODE) { 391 | textNode.nodeValue = prev.nodeValue + textNode.nodeValue; 392 | dom(prev).remove(); 393 | } 394 | if (next && next.nodeType === NODE_TYPE.TEXT_NODE) { 395 | textNode.nodeValue = textNode.nodeValue + next.nodeValue; 396 | dom(next).remove(); 397 | } 398 | } 399 | 400 | function removeHighlight(highlight: any) { 401 | const textNodes = dom(highlight).unwrap(); 402 | if (textNodes) 403 | textNodes.forEach(function(node) { 404 | mergeSiblingTextNodes(node); 405 | }); 406 | } 407 | 408 | sortByDepth(highlights, true); 409 | 410 | highlights.forEach((hl: any) => { 411 | if (this.options.onRemoveHighlight(hl) === true) { 412 | removeHighlight(hl); 413 | } 414 | }); 415 | }; 416 | 417 | /** 418 | * Returns highlights from given container. 419 | * @param params 420 | * @param {HTMLElement} [params.container] - return highlights from this element. Default: the element the 421 | * highlighter is applied to. 422 | * @param {boolean} [params.andSelf] - if set to true and container is a highlight itself, add container to 423 | * returned results. Default: true. 424 | * @param {boolean} [params.grouped] - if set to true, highlights are grouped in logical groups of highlights added 425 | * in the same moment. Each group is an object which has got array of highlights, 'toString' method and 'timestamp' 426 | * property. Default: false. 427 | * @returns {Array} - array of highlights. 428 | * @memberof TextHighlighter 429 | */ 430 | TextHighlighter.prototype.getHighlights = function(params: paramsImp) { 431 | params = defaults(params, { 432 | container: this.el, 433 | andSelf: true, 434 | grouped: false 435 | }); 436 | if (params.container) { 437 | const nodeList = params.container.querySelectorAll("[" + DATA_ATTR + "]"); 438 | let highlights = Array.prototype.slice.call(nodeList); 439 | 440 | if (params.andSelf === true && params.container.hasAttribute(DATA_ATTR)) { 441 | highlights.push(params.container); 442 | } 443 | 444 | if (params.grouped) { 445 | highlights = groupHighlights(highlights); 446 | } 447 | return highlights; 448 | } 449 | }; 450 | 451 | /** 452 | * Returns true if element is a highlight. 453 | * All highlights have 'data-highlighted' attribute. 454 | * @param el - element to check. 455 | * @returns {boolean} 456 | * @memberof TextHighlighter 457 | */ 458 | TextHighlighter.prototype.isHighlight = function(el: { 459 | nodeType: number; 460 | hasAttribute: (arg0: string) => any; 461 | }) { 462 | return ( 463 | el && el.nodeType === NODE_TYPE.ELEMENT_NODE && el.hasAttribute(DATA_ATTR) 464 | ); 465 | }; 466 | 467 | /** 468 | * Serializes all highlights in the element the highlighter is applied to. 469 | * @returns {string} - stringified JSON with highlights definition 470 | * @memberof TextHighlighter 471 | */ 472 | TextHighlighter.prototype.serializeHighlights = function() { 473 | const highlights = this.getHighlights(), 474 | refEl = this.el, 475 | hlDescriptors: any[][] = []; 476 | 477 | function getElementPath( 478 | el: HTMLElement | ParentNode | ChildNode, 479 | refElement: any 480 | ) { 481 | const path = []; 482 | let childNodes; 483 | 484 | if (el) 485 | do { 486 | if (el instanceof HTMLElement && el.parentNode) { 487 | childNodes = Array.prototype.slice.call(el.parentNode.childNodes); 488 | path.unshift(childNodes.indexOf(el)); 489 | el = el.parentNode; 490 | } 491 | } while (el !== refElement || !el); 492 | 493 | return path; 494 | } 495 | 496 | sortByDepth(highlights, false); 497 | 498 | // { 499 | // textContent: string | any[]; 500 | // cloneNode: (arg0: boolean) => any; 501 | // previousSibling: { nodeType: number; length: number }; 502 | // } 503 | highlights.forEach(function(highlight: HTMLElement) { 504 | if (highlight && highlight.textContent) { 505 | let offset = 0, // Hl offset from previous sibling within parent node. 506 | wrapper = highlight.cloneNode(true) as HTMLElement | string; 507 | const length = highlight.textContent.length, 508 | hlPath = getElementPath(highlight, refEl); 509 | if (wrapper instanceof HTMLElement) { 510 | wrapper.innerHTML = ""; 511 | wrapper = wrapper.outerHTML; 512 | } 513 | 514 | if ( 515 | highlight.previousSibling && 516 | highlight.previousSibling.nodeType === NODE_TYPE.TEXT_NODE 517 | ) { 518 | offset = highlight.previousSibling.childNodes.length; 519 | } 520 | 521 | hlDescriptors.push([ 522 | wrapper, 523 | highlight.textContent, 524 | hlPath.join(":"), 525 | offset, 526 | length 527 | ]); 528 | } 529 | }); 530 | 531 | return JSON.stringify(hlDescriptors); 532 | }; 533 | 534 | /** 535 | * Deserializes highlights. 536 | * @throws exception when can't parse JSON or JSON has invalid structure. 537 | * @param {object} json - JSON object with highlights definition. 538 | * @returns {Array} - array of deserialized highlights. 539 | * @memberof TextHighlighter 540 | */ 541 | TextHighlighter.prototype.deserializeHighlights = function(json: string) { 542 | let hlDescriptors; 543 | const highlights: { appendChild: (arg0: any) => void }[] = []; 544 | // const self = this; 545 | 546 | if (!json) { 547 | return highlights; 548 | } 549 | 550 | try { 551 | hlDescriptors = JSON.parse(json); 552 | } catch (e) { 553 | throw "Can't parse JSON: " + e; 554 | } 555 | 556 | function deserializationFn(this: any, hlDescriptor: any[]) { 557 | const hl = { 558 | wrapper: hlDescriptor[0], 559 | text: hlDescriptor[1], 560 | path: hlDescriptor[2].split(":"), 561 | offset: hlDescriptor[3], 562 | length: hlDescriptor[4] 563 | }; 564 | let elIndex = hl.path.pop(), 565 | node = this.el, 566 | highlight, 567 | idx; 568 | 569 | while ((idx = hl.path.shift())) { 570 | node = node.childNodes[idx]; 571 | } 572 | 573 | if ( 574 | node.childNodes[elIndex - 1] && 575 | node.childNodes[elIndex - 1].nodeType === NODE_TYPE.TEXT_NODE 576 | ) { 577 | elIndex -= 1; 578 | } 579 | 580 | node = node.childNodes[elIndex]; 581 | const hlNode = node.splitText(hl.offset); 582 | hlNode.splitText(hl.length); 583 | 584 | if (hlNode.nextSibling && !hlNode.nextSibling.nodeValue) { 585 | dom(hlNode.nextSibling).remove(); 586 | } 587 | 588 | if (hlNode.previousSibling && !hlNode.previousSibling.nodeValue) { 589 | dom(hlNode.previousSibling).remove(); 590 | } 591 | if (hl && hl.wrapper) { 592 | const tmpHtml = dom(hlNode).fromHTML(hl.wrapper)[0] as HTMLElement; 593 | if (tmpHtml) { 594 | highlight = dom(hlNode).wrap(tmpHtml); 595 | highlights.push(highlight); 596 | } 597 | } 598 | } 599 | 600 | hlDescriptors.forEach(function(hlDescriptor: any) { 601 | try { 602 | deserializationFn(hlDescriptor); 603 | } catch (e) { 604 | if (console && console.warn) { 605 | console.warn("Can't deserialize highlight descriptor. Cause: " + e); 606 | } 607 | } 608 | }); 609 | 610 | return highlights; 611 | }; 612 | 613 | /** 614 | * Finds and highlights given text. 615 | * @param {string} text - text to search for 616 | * @param {boolean} [caseSensitive] - if set to true, performs case sensitive search (default: true) 617 | * @memberof TextHighlighter 618 | */ 619 | TextHighlighter.prototype.find = function(text: any, caseSensitive: any) { 620 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 621 | const wnd = (dom(this.el).getWindow() as Window) as H_Window, 622 | scrollX = wnd.scrollX, 623 | scrollY = wnd.scrollY, 624 | caseSens = typeof caseSensitive === "undefined" ? true : caseSensitive; 625 | 626 | dom(this.el).removeAllRanges(); 627 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 628 | const body = wnd.document.body as ie_HTMLElement; 629 | 630 | if (wnd && wnd.find) { 631 | while (wnd.find(text, caseSens)) { 632 | this.doHighlight(true); 633 | } 634 | } else if (wnd && body && body.createTextRange) { 635 | const textRange: TextRange = body.createTextRange(); 636 | textRange.moveToElementText(this.el); 637 | while (textRange.findText(text, 1, caseSens ? 4 : 0)) { 638 | if ( 639 | !dom(this.el).contains(textRange.parentElement()) && 640 | textRange.parentElement() !== this.el 641 | ) { 642 | break; 643 | } 644 | 645 | textRange.select(); 646 | this.doHighlight(true); 647 | textRange.collapse(false); 648 | } 649 | } 650 | 651 | dom(this.el).removeAllRanges(); 652 | if (wnd) wnd.scrollTo(scrollX, scrollY); 653 | }; 654 | 655 | /** 656 | * Creates wrapper for highlights. 657 | * TextHighlighter instance calls this method each time it needs to create highlights and pass options retrieved 658 | * in constructor. 659 | * @param {object} options - the same object as in TextHighlighter constructor. 660 | * @returns {HTMLElement} 661 | * @memberof TextHighlighter 662 | * @static 663 | */ 664 | TextHighlighter.createWrapper = function(options: { 665 | color: string; 666 | highlightedClass: string; 667 | }) { 668 | const span = document.createElement("span"); 669 | span.style.backgroundColor = options.color; 670 | span.className = options.highlightedClass; 671 | return span; 672 | }; 673 | 674 | export { TextHighlighter }; 675 | // global.TextHighlighter = TextHighlighter; 676 | // })(window); 677 | -------------------------------------------------------------------------------- /build/replace.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | dist/TextHighlighter.js 4 | lib/index.d.ts 5 | 6 | replace ../src/Library - ./Library 7 | */ 8 | 9 | const replace = require('replace-in-file'); 10 | const options = { 11 | files: ['./dist/TextHighlighter.js', 12 | './lib/*'], 13 | from: /\.\.\/src\/Library/g, 14 | to: './Library', 15 | }; 16 | 17 | try { 18 | const results = replace.sync(options); 19 | console.log('Replacement results:', results); 20 | } 21 | catch (error) { 22 | console.error('Error occurred:', error); 23 | } -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | // TextHighLighterv2 client 2 | import { TextHighlighterType } from "../src/types"; 3 | import { TextHighlighter } from "../src"; 4 | 5 | interface Window { 6 | TextHighlighter: TextHighlighterType; 7 | } 8 | 9 | declare let window: Window; 10 | window.TextHighlighter = TextHighlighter; 11 | -------------------------------------------------------------------------------- /demos/assets/ColorPicker.css: -------------------------------------------------------------------------------- 1 | .color-picker { 2 | display: block; 3 | margin: 10px 0 5px; 4 | text-align: center; 5 | } 6 | 7 | .color-picker div { 8 | width: 23px; 9 | height: 23px; 10 | border: 1px solid #111; 11 | border-radius: 12px; 12 | margin: 5px 8px; 13 | display: inline-block; 14 | cursor: pointer; 15 | } 16 | 17 | .color-picker div:hover { 18 | border: 1px solid #444; 19 | } 20 | 21 | .color-picker div.selected { 22 | box-shadow: #666 0 0 4px 2px; 23 | } 24 | -------------------------------------------------------------------------------- /demos/assets/ColorPicker.js: -------------------------------------------------------------------------------- 1 | var ColorPicker = (function () { 2 | 'use strict'; 3 | 4 | var COLORS = [ 5 | '#FFFF7B', 6 | '#F44336', 7 | '#8BC34A', 8 | '#29B6F6' 9 | ]; 10 | var CLASS_SELECTED = 'selected'; 11 | 12 | function ColorPicker(el) { 13 | var self = this; 14 | this.color = COLORS[0]; 15 | this.selected = null; 16 | 17 | COLORS.forEach(function (color) { 18 | var div = document.createElement('div'); 19 | div.style.backgroundColor = color; 20 | 21 | if (self.color === color) { 22 | div.className = CLASS_SELECTED; 23 | self.selected = div; 24 | } 25 | 26 | div.addEventListener('click', function () { 27 | if (color !== self.color) { 28 | self.color = color; 29 | 30 | if (self.selected) { 31 | self.selected.className = ''; 32 | } 33 | self.selected = div; 34 | self.selected.className = CLASS_SELECTED; 35 | 36 | if (typeof self.callback === 'function') { 37 | self.callback.call(self, color); 38 | } 39 | } 40 | }, false); 41 | 42 | el.appendChild(div); 43 | }); 44 | 45 | } 46 | 47 | 48 | ColorPicker.prototype.onColorChange = function (callback) { 49 | this.callback = callback; 50 | }; 51 | 52 | return ColorPicker; 53 | })(); -------------------------------------------------------------------------------- /demos/assets/github.css: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub theme 3 | * 4 | * @author Craig Campbell 5 | * @version 1.0.4 6 | */ 7 | pre { 8 | border: 1px solid #ccc; 9 | word-wrap: break-word; 10 | padding: 6px 10px; 11 | line-height: 19px; 12 | margin-bottom: 20px; 13 | } 14 | 15 | code { 16 | border: 1px solid #eaeaea; 17 | margin: 0px 2px; 18 | padding: 0px 5px; 19 | font-size: 12px; 20 | } 21 | 22 | pre code { 23 | border: 0px; 24 | padding: 0px; 25 | margin: 0px; 26 | -moz-border-radius: 0px; 27 | -webkit-border-radius: 0px; 28 | border-radius: 0px; 29 | } 30 | 31 | pre, code { 32 | font-family: Consolas, 'Liberation Mono', Courier, monospace; 33 | color: #333; 34 | background: #f8f8f8; 35 | -moz-border-radius: 3px; 36 | -webkit-border-radius: 3px; 37 | border-radius: 3px; 38 | } 39 | 40 | pre, pre code { 41 | font-size: 13px; 42 | } 43 | 44 | pre .comment { 45 | color: #998; 46 | } 47 | 48 | pre .support { 49 | color: #0086B3; 50 | } 51 | 52 | pre .tag, pre .tag-name { 53 | color: navy; 54 | } 55 | 56 | pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function { 57 | font-weight: bold; 58 | } 59 | 60 | pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace { 61 | color: #333; 62 | } 63 | 64 | pre .constant.numeric, pre .keyword.unit, pre .hex-color { 65 | font-weight: normal; 66 | color: #099; 67 | } 68 | 69 | pre .entity.class { 70 | color: #458; 71 | } 72 | 73 | pre .entity.id, pre .entity.function { 74 | color: #900; 75 | } 76 | 77 | pre .attribute, pre .variable { 78 | color: teal; 79 | } 80 | 81 | pre .string, pre .support.value { 82 | font-weight: normal; 83 | color: #d14; 84 | } 85 | 86 | pre .regexp { 87 | color: #009926; 88 | } 89 | -------------------------------------------------------------------------------- /demos/assets/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funktechno/texthighlighter/7dae66399da0e76f5a21f4a0e9afd6fedf292a92/demos/assets/img.jpg -------------------------------------------------------------------------------- /demos/assets/rainbow-custom.min.js: -------------------------------------------------------------------------------- 1 | /* Rainbow v1.2 rainbowco.de | included languages: generic, javascript */ 2 | window.Rainbow=function(){function A(a){var b=a.getAttribute("data-language")||a.parentNode.getAttribute("data-language");if(!b){var c=/\blang(?:uage)?-(\w+)/;(a=a.className.match(c)||a.parentNode.className.match(c))&&(b=a[1])}return b}function B(a,b){for(var c in f[d]){c=parseInt(c,10);if(a==c&&b==f[d][c]?0:a<=c&&b>=f[d][c])delete f[d][c],delete j[d][c];if(a>=c&&ac&&b'+b+""}function r(a, 3 | b,c,i){if("undefined"===typeof a||null===a)i();else{var e=a.exec(c);if(e){++s;!b.name&&"string"==typeof b.matches[0]&&(b.name=b.matches[0],delete b.matches[0]);var k=e[0],g=e.index,t=e[0].length+g,h=function(){function e(){r(a,b,c,i)}s%100>0?e():setTimeout(e,0)};if(B(g,t))h();else{var m=u(b.matches),l=function(a,c,i){if(a>=c.length)i(k);else{var d=e[c[a]];if(d){var g=b.matches[c[a]],f=g.language,h=g.name&&g.matches?g.matches:g,j=function(b,d,g){var f;f=0;var h;for(h=1;h/g,">").replace(/&(?![\w\#]+;)/g,"&"),b,c)}function o(a,b,c){if(b 2 | 3 | 4 | 5 | TextHighlighter Demo | Callbacks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |

TextHighlighter Callbacks

23 |
    24 |
  • 25 | Select any text in the sandbox to highlight it. You should see messages before and after 26 | highlighting. 27 |
  • 28 |
  • Change highlighting color:
  • 29 |
  • Remove all highlights: . You should see message before removing.
  • 30 |
31 |
32 | 33 |
34 |

 35 | new TextHighlighter(sandbox, {
 36 |     onBeforeHighlight: function (range) {
 37 |         // ...
 38 |     },
 39 |     onAfterHighlight: function (range, hlts) {
 40 |         // ...
 41 |     },
 42 |     onRemoveHighlight: function (hlt) {
 43 |         // ...
 44 |     }
 45 | });
 46 | 
47 |
48 | 49 |
50 |
SANDBOX
51 |
52 |

Lorem ipsum

53 |

54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus malesuada sagittis. Morbi 55 | purus odio, blandit ac urna sed, interdum pharetra leo. Cras congue id est sit amet mattis. 56 | Sed in metus et orci eleifend commodo. Phasellus at odio imperdiet, efficitur augue in, pulvinar 57 | sapien. Pellentesque leo nulla, porta non lectus eu, ullamcorper semper est. Nunc convallis 58 | risus vel mauris accumsan, in rutrum odio sodales. Vestibulum ante ipsum primis in faucibus 59 | orci luctus et ultrices posuere cubilia Curae; Sed at tempus mauris. Fusce blandit felis sit amet 60 | magna lacinia blandit. 61 |

62 | dummy image 63 |

64 | Maecenas faucibus hendrerit lectus, in auctor felis tristique at. Pellentesque a felis ut nibh 65 | malesuada auctor. Ut egestas elit ac ultrices ullamcorper. Pellentesque enim est, varius 66 | ultrices velit eget, consectetur aliquam tortor. Aliquam sit amet nibh id tellus sollicitudin 67 | faucibus. Nunc euismod augue tempus, ornare justo interdum, consectetur lacus. Pellentesque a 68 | molestie tellus, eget convallis lectus. 69 |

70 |

71 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae nunc sed risus blandit convallis 72 | id id risus. Morbi tortor metus, imperdiet sed ipsum quis, condimentum mattis tellus. Fusce orci nisi, 73 | ultricies vel hendrerit id, egestas id turpis. Proin cursus diam tortor, sed ullamcorper eros commodo 74 | vitae. Aenean et maximus sapien. Nam felis velit, ullamcorper eu turpis ut, hendrerit accumsan augue. 75 | Nulla et purus sem. Ut at hendrerit purus. Phasellus mollis commodo ante eu mollis. In nec 76 | dui vel mauris lacinia vulputate id nec turpis. Aliquam vestibulum, elit sit amet fringilla 77 | malesuada, quam nunc eleifend nunc, id iaculis est neque pretium libero. 78 |

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
First NameLast NameEmail
JohnLuciusj.lu@mail.com
NikolaiBodrovniko.b@mail.com
SteveSuzys.suzy@mail.com
AlexStirlingalexs@mail.com
107 | 108 |

109 | Donec sollicitudin commodo risus in consequat. Curabitur ultricies sagittis mi, a dapibus nunc mollis 110 | sed. Sed ut pretium leo, quis vehicula diam. Proin nisi metus, elementum ut mi porttitor, cursus 111 | egestas sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus mattis ipsum 112 | ut enim efficitur mollis. Vivamus id mollis lectus. 113 |

114 | 115 |
116 |
117 | 118 | 119 |
120 | 121 |
122 | 123 | 124 |
125 | 126 |
127 | 128 | 132 |
133 | 134 | 135 |
136 | 137 |

138 | In est tortor, tincidunt vitae elit at, ultricies tincidunt magna. Aenean suscipit ante sapien, 139 | quis sagittis felis efficitur feugiat. In arcu elit, hendrerit vel varius eget, elementum vitae 140 | lectus. Phasellus ut purus commodo ante iaculis molestie. Integer turpis felis, pellentesque eu 141 | dignissim vel, sodales vel metus. Aliquam tempus lorem odio. Sed purus arcu, auctor eget sodales 142 | ac, venenatis ac velit. Praesent a quam at purus varius accumsan sit amet quis magna. Praesent 143 | efficitur velit quis mi posuere, ut egestas elit egestas. Etiam vulputate lacus in posuere 144 | suscipit. Fusce molestie sem ipsum. Phasellus consectetur, purus quis auctor laoreet, elit 145 | nisi aliquet metus, et placerat nunc tellus ac massa. Praesent cursus ornare nulla eu ultrices. 146 |

147 |
148 |
149 |
150 | 153 | 154 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /demos/iframe-content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Iframe content 6 | 7 | 8 | 59 | 60 | 61 |

Lorem ipsum

62 |

63 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus malesuada sagittis. Morbi 64 | purus odio, blandit ac urna sed, interdum pharetra leo. Cras congue id est sit amet mattis. 65 | Sed in metus et orci eleifend commodo. Phasellus at odio imperdiet, efficitur augue in, pulvinar 66 | sapien. Pellentesque leo nulla, porta non lectus eu, ullamcorper semper est. Nunc convallis 67 | risus vel mauris accumsan, in rutrum odio sodales. Vestibulum ante ipsum primis in faucibus 68 | orci luctus et ultrices posuere cubilia Curae; Sed at tempus mauris. Fusce blandit felis sit amet 69 | magna lacinia blandit. 70 |

71 | dummy image 72 |

73 | Maecenas faucibus hendrerit lectus, in auctor felis tristique at. Pellentesque a felis ut nibh 74 | malesuada auctor. Ut egestas elit ac ultrices ullamcorper. Pellentesque enim est, varius 75 | ultrices velit eget, consectetur aliquam tortor. Aliquam sit amet nibh id tellus sollicitudin 76 | faucibus. Nunc euismod augue tempus, ornare justo interdum, consectetur lacus. Pellentesque a 77 | molestie tellus, eget convallis lectus. 78 |

79 |

80 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae nunc sed risus blandit convallis 81 | id id risus. Morbi tortor metus, imperdiet sed ipsum quis, condimentum mattis tellus. Fusce orci nisi, 82 | ultricies vel hendrerit id, egestas id turpis. Proin cursus diam tortor, sed ullamcorper eros commodo 83 | vitae. Aenean et maximus sapien. Nam felis velit, ullamcorper eu turpis ut, hendrerit accumsan augue. 84 | Nulla et purus sem. Ut at hendrerit purus. Phasellus mollis commodo ante eu mollis. In nec 85 | dui vel mauris lacinia vulputate id nec turpis. Aliquam vestibulum, elit sit amet fringilla 86 | malesuada, quam nunc eleifend nunc, id iaculis est neque pretium libero. 87 |

88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
First NameLast NameEmail
JohnLuciusj.lu@mail.com
NikolaiBodrovniko.b@mail.com
SteveSuzys.suzy@mail.com
AlexStirlingalexs@mail.com
116 | 117 |

118 | Donec sollicitudin commodo risus in consequat. Curabitur ultricies sagittis mi, a dapibus nunc mollis 119 | sed. Sed ut pretium leo, quis vehicula diam. Proin nisi metus, elementum ut mi porttitor, cursus 120 | egestas sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus mattis ipsum 121 | ut enim efficitur mollis. Vivamus id mollis lectus. 122 |

123 | 124 |
125 |
126 | 127 | 128 |
129 | 130 |
131 | 132 | 133 |
134 | 135 |
136 | 137 | 141 |
142 | 143 | 144 |
145 | 146 |

147 | In est tortor, tincidunt vitae elit at, ultricies tincidunt magna. Aenean suscipit ante sapien, 148 | quis sagittis felis efficitur feugiat. In arcu elit, hendrerit vel varius eget, elementum vitae 149 | lectus. Phasellus ut purus commodo ante iaculis molestie. Integer turpis felis, pellentesque eu 150 | dignissim vel, sodales vel metus. Aliquam tempus lorem odio. Sed purus arcu, auctor eget sodales 151 | ac, venenatis ac velit. Praesent a quam at purus varius accumsan sit amet quis magna. Praesent 152 | efficitur velit quis mi posuere, ut egestas elit egestas. Etiam vulputate lacus in posuere 153 | suscipit. Fusce molestie sem ipsum. Phasellus consectetur, purus quis auctor laoreet, elit 154 | nisi aliquet metus, et placerat nunc tellus ac massa. Praesent cursus ornare nulla eu ultrices. 155 |

156 | 157 | -------------------------------------------------------------------------------- /demos/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TextHighlighter Demo | Iframe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |

TextHighlighter Demo

23 |
    24 |
  • Select any text in the sandbox to highlight it.
  • 25 |
  • Change highlighting color:
  • 26 |
  • Remove all highlights:
  • 27 |
28 |
29 | 30 |
31 |

32 | var iframe = document.querySelector('iframe');
33 | 
34 | iframe.onload = function () {
35 |     var hltr = new TextHighlighter(
36 |         iframe.contentDocument.body
37 |     );
38 | };
39 | 
40 |
41 | 42 |
43 |
SANDBOX
44 |
45 | 46 |
47 |
48 |
49 | 52 | 53 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /demos/serialization.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TextHighlighter Demo | Serialization 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |

TextHighlighter Serialization

23 |
    24 |
  • 25 | Select any text in the sandbox to highlight it. 26 |
  • 27 |
  • Change highlighting color:
  • 28 |
  • Remove all highlights:
  • 29 |
  • Serialize all highlights:
  • 30 |
  • Deserialize highlights:
  • 31 |
32 |
33 | 34 |
35 |

 36 | var hltr = new TextHighlighter(sandbox),
 37 | serialized;
 38 | 
 39 | serializeBtn.onclick = function () {
 40 |     serialized = hltr.serializeHighlights();
 41 |     console.log(serialized);
 42 |     hltr.removeHighlights();
 43 | });
 44 | 
 45 | deserializeBtn.onclick = function () {
 46 |     hltr.removeHighlights();
 47 |     hltr.deserializeHighlights(serialized);
 48 | });
 49 | 
50 |
51 | 52 |
53 |
SANDBOX
54 |
55 |

Lorem ipsum

56 |

57 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus malesuada sagittis. Morbi 58 | purus odio, blandit ac urna sed, interdum pharetra leo. Cras congue id est sit amet mattis. 59 | Sed in metus et orci eleifend commodo. Phasellus at odio imperdiet, efficitur augue in, pulvinar 60 | sapien. Pellentesque leo nulla, porta non lectus eu, ullamcorper semper est. Nunc convallis 61 | risus vel mauris accumsan, in rutrum odio sodales. Vestibulum ante ipsum primis in faucibus 62 | orci luctus et ultrices posuere cubilia Curae; Sed at tempus mauris. Fusce blandit felis sit amet 63 | magna lacinia blandit. 64 |

65 | dummy image 66 |

67 | Maecenas faucibus hendrerit lectus, in auctor felis tristique at. Pellentesque a felis ut nibh 68 | malesuada auctor. Ut egestas elit ac ultrices ullamcorper. Pellentesque enim est, varius 69 | ultrices velit eget, consectetur aliquam tortor. Aliquam sit amet nibh id tellus sollicitudin 70 | faucibus. Nunc euismod augue tempus, ornare justo interdum, consectetur lacus. Pellentesque a 71 | molestie tellus, eget convallis lectus. 72 |

73 |

74 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae nunc sed risus blandit convallis 75 | id id risus. Morbi tortor metus, imperdiet sed ipsum quis, condimentum mattis tellus. Fusce orci nisi, 76 | ultricies vel hendrerit id, egestas id turpis. Proin cursus diam tortor, sed ullamcorper eros commodo 77 | vitae. Aenean et maximus sapien. Nam felis velit, ullamcorper eu turpis ut, hendrerit accumsan augue. 78 | Nulla et purus sem. Ut at hendrerit purus. Phasellus mollis commodo ante eu mollis. In nec 79 | dui vel mauris lacinia vulputate id nec turpis. Aliquam vestibulum, elit sit amet fringilla 80 | malesuada, quam nunc eleifend nunc, id iaculis est neque pretium libero. 81 |

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
First NameLast NameEmail
JohnLuciusj.lu@mail.com
NikolaiBodrovniko.b@mail.com
SteveSuzys.suzy@mail.com
AlexStirlingalexs@mail.com
110 | 111 |

112 | Donec sollicitudin commodo risus in consequat. Curabitur ultricies sagittis mi, a dapibus nunc mollis 113 | sed. Sed ut pretium leo, quis vehicula diam. Proin nisi metus, elementum ut mi porttitor, cursus 114 | egestas sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus mattis ipsum 115 | ut enim efficitur mollis. Vivamus id mollis lectus. 116 |

117 | 118 |
119 |
120 | 121 | 122 |
123 | 124 |
125 | 126 | 127 |
128 | 129 |
130 | 131 | 135 |
136 | 137 | 138 |
139 | 140 |

141 | In est tortor, tincidunt vitae elit at, ultricies tincidunt magna. Aenean suscipit ante sapien, 142 | quis sagittis felis efficitur feugiat. In arcu elit, hendrerit vel varius eget, elementum vitae 143 | lectus. Phasellus ut purus commodo ante iaculis molestie. Integer turpis felis, pellentesque eu 144 | dignissim vel, sodales vel metus. Aliquam tempus lorem odio. Sed purus arcu, auctor eget sodales 145 | ac, venenatis ac velit. Praesent a quam at purus varius accumsan sit amet quis magna. Praesent 146 | efficitur velit quis mi posuere, ut egestas elit egestas. Etiam vulputate lacus in posuere 147 | suscipit. Fusce molestie sem ipsum. Phasellus consectetur, purus quis auctor laoreet, elit 148 | nisi aliquet metus, et placerat nunc tellus ac massa. Praesent cursus ornare nulla eu ultrices. 149 |

150 |
151 |
152 |
153 | 156 | 157 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /demos/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TextHighlighter Demo | Simple Usage 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |

TextHighlighter Demo

23 |
    24 |
  • Select any text in the sandbox to highlight it.
  • 25 |
  • Change highlighting color:
  • 26 |
  • Remove all highlights:
  • 27 |
28 |
29 | 30 |
31 |

 32 | var hltr = new TextHighlighter(sandbox);
 33 | 
 34 | colors.onColorChange(function (color) {
 35 |     hltr.setColor(color);
 36 | });
 37 | 
 38 | buttonRemove.onclick = function () {
 39 |     hltr.removeHighlights();
 40 | };
 41 | 
42 |
43 | 44 |
45 |
SANDBOX
46 |
47 |

Lorem ipsum

48 |

49 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus malesuada sagittis. Morbi 50 | purus odio, blandit ac urna sed, interdum pharetra leo. Cras congue id est sit amet mattis. 51 | Sed in metus et orci eleifend commodo. Phasellus at odio imperdiet, efficitur augue in, pulvinar 52 | sapien. Pellentesque leo nulla, porta non lectus eu, ullamcorper semper est. Nunc convallis 53 | risus vel mauris accumsan, in rutrum odio sodales. Vestibulum ante ipsum primis in faucibus 54 | orci luctus et ultrices posuere cubilia Curae; Sed at tempus mauris. Fusce blandit felis sit amet 55 | magna lacinia blandit. 56 |

57 | dummy image 58 |

59 | Maecenas faucibus hendrerit lectus, in auctor felis tristique at. Pellentesque a felis ut nibh 60 | malesuada auctor. Ut egestas elit ac ultrices ullamcorper. Pellentesque enim est, varius 61 | ultrices velit eget, consectetur aliquam tortor. Aliquam sit amet nibh id tellus sollicitudin 62 | faucibus. Nunc euismod augue tempus, ornare justo interdum, consectetur lacus. Pellentesque a 63 | molestie tellus, eget convallis lectus. 64 |

65 |

66 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae nunc sed risus blandit convallis 67 | id id risus. Morbi tortor metus, imperdiet sed ipsum quis, condimentum mattis tellus. Fusce orci nisi, 68 | ultricies vel hendrerit id, egestas id turpis. Proin cursus diam tortor, sed ullamcorper eros commodo 69 | vitae. Aenean et maximus sapien. Nam felis velit, ullamcorper eu turpis ut, hendrerit accumsan augue. 70 | Nulla et purus sem. Ut at hendrerit purus. Phasellus mollis commodo ante eu mollis. In nec 71 | dui vel mauris lacinia vulputate id nec turpis. Aliquam vestibulum, elit sit amet fringilla 72 | malesuada, quam nunc eleifend nunc, id iaculis est neque pretium libero. 73 |

74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
First NameLast NameEmail
JohnLuciusj.lu@mail.com
NikolaiBodrovniko.b@mail.com
SteveSuzys.suzy@mail.com
AlexStirlingalexs@mail.com
102 | 103 |

104 | Donec sollicitudin commodo risus in consequat. Curabitur ultricies sagittis mi, a dapibus nunc mollis 105 | sed. Sed ut pretium leo, quis vehicula diam. Proin nisi metus, elementum ut mi porttitor, cursus 106 | egestas sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus mattis ipsum 107 | ut enim efficitur mollis. Vivamus id mollis lectus. 108 |

109 | 110 |
111 |
112 | 113 | 114 |
115 | 116 |
117 | 118 | 119 |
120 | 121 |
122 | 123 | 127 |
128 | 129 | 130 |
131 | 132 |

133 | In est tortor, tincidunt vitae elit at, ultricies tincidunt magna. Aenean suscipit ante sapien, 134 | quis sagittis felis efficitur feugiat. In arcu elit, hendrerit vel varius eget, elementum vitae 135 | lectus. Phasellus ut purus commodo ante iaculis molestie. Integer turpis felis, pellentesque eu 136 | dignissim vel, sodales vel metus. Aliquam tempus lorem odio. Sed purus arcu, auctor eget sodales 137 | ac, venenatis ac velit. Praesent a quam at purus varius accumsan sit amet quis magna. Praesent 138 | efficitur velit quis mi posuere, ut egestas elit egestas. Etiam vulputate lacus in posuere 139 | suscipit. Fusce molestie sem ipsum. Phasellus consectetur, purus quis auctor laoreet, elit 140 | nisi aliquet metus, et placerat nunc tellus ac massa. Praesent cursus ornare nulla eu ultrices. 141 |

142 |
143 |
144 |
145 | 148 | 149 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | "ts-jest": { 4 | tsConfig: "tsconfig.json" 5 | } 6 | }, 7 | moduleFileExtensions: [ 8 | "ts", 9 | "js" 10 | ], 11 | transform: { 12 | "^.+\\.(ts|tsx)$": "ts-jest" 13 | }, 14 | testMatch: [ 15 | "**/tests/**/*.spec.(ts|js)" 16 | ], 17 | runner: "jest-serial-runner", 18 | testEnvironment: "node", 19 | reporters: ["default", "jest-junit"], 20 | // collectCoverage: true, 21 | collectCoverageFrom: ["src/**/*.{ts,js}", "!**/node_modules/**"] 22 | }; -------------------------------------------------------------------------------- /lib/Library.d.ts: -------------------------------------------------------------------------------- 1 | import { optionsImpl, paramsImp } from "./types"; 2 | /** 3 | * Creates wrapper for highlights. 4 | * TextHighlighter instance calls this method each time it needs to create highlights and pass options retrieved 5 | * in constructor. 6 | * @param {object} options - the same object as in TextHighlighter constructor. 7 | * @returns {HTMLElement} 8 | * @memberof TextHighlighter 9 | * @static 10 | */ 11 | declare function createWrapper(options: optionsImpl): HTMLSpanElement; 12 | /** 13 | * Highlights range. 14 | * Wraps text of given range object in wrapper element. 15 | * @param {Range} range 16 | * @param {HTMLElement} wrapper 17 | * @returns {Array} - array of created highlights. 18 | * @memberof TextHighlighter 19 | */ 20 | declare const highlightRange: (el: HTMLElement, range: Range, wrapper: { 21 | cloneNode: (arg0: boolean) => any; 22 | }) => HTMLElement[]; 23 | /** 24 | * Flattens highlights structure. 25 | * Note: this method changes input highlights - their order and number after calling this method may change. 26 | * @param {Array} highlights - highlights to flatten. 27 | * @memberof TextHighlighter 28 | */ 29 | export declare const flattenNestedHighlights: (highlights: any[]) => void; 30 | /** 31 | * Merges sibling highlights and normalizes descendant text nodes. 32 | * Note: this method changes input highlights - their order and number after calling this method may change. 33 | * @param highlights 34 | * @memberof TextHighlighter 35 | */ 36 | export declare const mergeSiblingHighlights: (highlights: any[]) => void; 37 | /** 38 | * Normalizes highlights. Ensures that highlighting is done with use of the smallest possible number of 39 | * wrapping HTML elements. 40 | * Flattens highlights structure and merges sibling highlights. Normalizes text nodes within highlights. 41 | * @param {Array} highlights - highlights to normalize. 42 | * @returns {Array} - array of normalized highlights. Order and number of returned highlights may be different than 43 | * input highlights. 44 | * @memberof TextHighlighter 45 | */ 46 | export declare const normalizeHighlights: (highlights: any[]) => any; 47 | /** 48 | * highlight selected element 49 | * @param el 50 | * @param options 51 | * @param keepRange 52 | */ 53 | declare const doHighlight: (el: HTMLElement, keepRange: boolean, options?: optionsImpl | undefined) => boolean; 54 | /** 55 | * Deserializes highlights. 56 | * @throws exception when can't parse JSON or JSON has invalid structure. 57 | * @param {object} json - JSON object with highlights definition. 58 | * @returns {Array} - array of deserialized highlights. 59 | * @memberof TextHighlighter 60 | */ 61 | declare const deserializeHighlights: (el: HTMLElement, json: string) => { 62 | appendChild: (arg0: any) => void; 63 | }[]; 64 | export declare const find: (el: HTMLElement, text: string, caseSensitive: boolean, options?: optionsImpl | undefined) => void; 65 | /** 66 | * Returns highlights from given container. 67 | * @param params 68 | * @param {HTMLElement} [params.container] - return highlights from this element. Default: the element the 69 | * highlighter is applied to. 70 | * @param {boolean} [params.andSelf] - if set to true and container is a highlight itself, add container to 71 | * returned results. Default: true. 72 | * @param {boolean} [params.grouped] - if set to true, highlights are grouped in logical groups of highlights added 73 | * in the same moment. Each group is an object which has got array of highlights, 'toString' method and 'timestamp' 74 | * property. Default: false. 75 | * @returns {Array} - array of highlights. 76 | * @memberof TextHighlighter 77 | */ 78 | export declare const getHighlights: (el: HTMLElement, params?: paramsImp | undefined) => any[] | undefined; 79 | /** 80 | * Serializes all highlights in the element the highlighter is applied to. 81 | * @returns {string} - stringified JSON with highlights definition 82 | * @memberof TextHighlighter 83 | */ 84 | declare const serializeHighlights: (el: HTMLElement | null) => string | undefined; 85 | declare const removeHighlights: (element: HTMLElement, options?: optionsImpl | undefined) => void; 86 | export { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, createWrapper, highlightRange }; 87 | -------------------------------------------------------------------------------- /lib/Library.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.highlightRange = exports.createWrapper = exports.removeHighlights = exports.serializeHighlights = exports.deserializeHighlights = exports.doHighlight = exports.getHighlights = exports.find = exports.normalizeHighlights = exports.mergeSiblingHighlights = exports.flattenNestedHighlights = void 0; 4 | // TextHighLighterv2 library 5 | // Port by: lastlink 6 | var types_1 = require("./types"); 7 | var Utils_1 = require("./Utils"); 8 | /** 9 | * Creates wrapper for highlights. 10 | * TextHighlighter instance calls this method each time it needs to create highlights and pass options retrieved 11 | * in constructor. 12 | * @param {object} options - the same object as in TextHighlighter constructor. 13 | * @returns {HTMLElement} 14 | * @memberof TextHighlighter 15 | * @static 16 | */ 17 | function createWrapper(options) { 18 | var span = document.createElement("span"); 19 | if (options.color) { 20 | span.style.backgroundColor = options.color; 21 | span.setAttribute("data-backgroundcolor", options.color); 22 | } 23 | if (options.highlightedClass) 24 | span.className = options.highlightedClass; 25 | return span; 26 | } 27 | exports.createWrapper = createWrapper; 28 | /** 29 | * Highlights range. 30 | * Wraps text of given range object in wrapper element. 31 | * @param {Range} range 32 | * @param {HTMLElement} wrapper 33 | * @returns {Array} - array of created highlights. 34 | * @memberof TextHighlighter 35 | */ 36 | var highlightRange = function (el, range, wrapper) { 37 | if (!range || range.collapsed) { 38 | return []; 39 | } 40 | var result = Utils_1.refineRangeBoundaries(range); 41 | var startContainer = result.startContainer, endContainer = result.endContainer, highlights = []; 42 | var goDeeper = result.goDeeper, done = false, node = startContainer, highlight, wrapperClone, nodeParent; 43 | do { 44 | if (node && goDeeper && node.nodeType === Utils_1.NODE_TYPE.TEXT_NODE) { 45 | if (node.parentNode instanceof HTMLElement && 46 | node.parentNode.tagName && 47 | node.nodeValue && 48 | Utils_1.IGNORE_TAGS.indexOf(node.parentNode.tagName) === -1 && 49 | node.nodeValue.trim() !== "") { 50 | wrapperClone = wrapper.cloneNode(true); 51 | wrapperClone.setAttribute(Utils_1.DATA_ATTR, true); 52 | nodeParent = node.parentNode; 53 | // highlight if a node is inside the el 54 | if (Utils_1.dom(el).contains(nodeParent) || nodeParent === el) { 55 | highlight = Utils_1.dom(node).wrap(wrapperClone); 56 | highlights.push(highlight); 57 | } 58 | } 59 | goDeeper = false; 60 | } 61 | if (node === endContainer && 62 | endContainer && 63 | !(endContainer.hasChildNodes() && goDeeper)) { 64 | done = true; 65 | } 66 | if (node instanceof HTMLElement && 67 | node.tagName && 68 | Utils_1.IGNORE_TAGS.indexOf(node.tagName) > -1) { 69 | if (endContainer instanceof HTMLElement && 70 | endContainer.parentNode === node) { 71 | done = true; 72 | } 73 | goDeeper = false; 74 | } 75 | if (goDeeper && 76 | (node instanceof Text || node instanceof HTMLElement) && 77 | node.hasChildNodes()) { 78 | node = node.firstChild; 79 | } 80 | else if (node && node.nextSibling) { 81 | node = node.nextSibling; 82 | goDeeper = true; 83 | } 84 | else if (node) { 85 | node = node.parentNode; 86 | goDeeper = false; 87 | } 88 | } while (!done); 89 | return highlights; 90 | }; 91 | exports.highlightRange = highlightRange; 92 | // : { 93 | // nodeType: number; 94 | // hasAttribute: (arg0: string) => any; 95 | // } 96 | /** 97 | * Returns true if element is a highlight. 98 | * All highlights have 'data-highlighted' attribute. 99 | * @param el - element to check. 100 | * @returns {boolean} 101 | * @memberof TextHighlighter 102 | */ 103 | var isHighlight = function (el) { 104 | return (el && el.nodeType === Utils_1.NODE_TYPE.ELEMENT_NODE && el.hasAttribute(Utils_1.DATA_ATTR)); 105 | }; 106 | /** 107 | * Flattens highlights structure. 108 | * Note: this method changes input highlights - their order and number after calling this method may change. 109 | * @param {Array} highlights - highlights to flatten. 110 | * @memberof TextHighlighter 111 | */ 112 | var flattenNestedHighlights = function (highlights) { 113 | var again; 114 | // self = this; 115 | Utils_1.sortByDepth(highlights, true); 116 | var flattenOnce = function () { 117 | var again = false; 118 | highlights.forEach(function (hl, i) { 119 | var parent = hl.parentElement; 120 | if (parent) { 121 | var parentPrev = parent.previousSibling, parentNext = parent.nextSibling; 122 | if (isHighlight(parent)) { 123 | if (!Utils_1.haveSameColor(parent, hl)) { 124 | if (!hl.nextSibling && parentNext) { 125 | var newLocal = parentNext || parent; 126 | if (newLocal) { 127 | Utils_1.dom(hl).insertBefore(newLocal); 128 | again = true; 129 | } 130 | } 131 | if (!hl.previousSibling && parentPrev) { 132 | var newLocal = parentPrev || parent; 133 | if (newLocal) { 134 | Utils_1.dom(hl).insertAfter(newLocal); 135 | again = true; 136 | } 137 | } 138 | if (!parent.hasChildNodes()) { 139 | Utils_1.dom(parent).remove(); 140 | } 141 | } 142 | else { 143 | if (hl && hl.firstChild) 144 | parent.replaceChild(hl.firstChild, hl); 145 | highlights[i] = parent; 146 | again = true; 147 | } 148 | } 149 | } 150 | }); 151 | return again; 152 | }; 153 | do { 154 | again = flattenOnce(); 155 | } while (again); 156 | }; 157 | exports.flattenNestedHighlights = flattenNestedHighlights; 158 | /** 159 | * Merges sibling highlights and normalizes descendant text nodes. 160 | * Note: this method changes input highlights - their order and number after calling this method may change. 161 | * @param highlights 162 | * @memberof TextHighlighter 163 | */ 164 | var mergeSiblingHighlights = function (highlights) { 165 | // const self = this; 166 | var shouldMerge = function (current, node) { 167 | return (node && 168 | node.nodeType === Utils_1.NODE_TYPE.ELEMENT_NODE && 169 | Utils_1.haveSameColor(current, node) && 170 | isHighlight(node)); 171 | }; 172 | // : { 173 | // previousSibling: any; 174 | // nextSibling: any; 175 | // } 176 | highlights.forEach(function (highlight) { 177 | var prev = highlight.previousSibling, next = highlight.nextSibling; 178 | if (shouldMerge(highlight, prev)) { 179 | Utils_1.dom(highlight).prepend(prev.childNodes); 180 | Utils_1.dom(prev).remove(); 181 | } 182 | if (shouldMerge(highlight, next)) { 183 | Utils_1.dom(highlight).append(next.childNodes); 184 | Utils_1.dom(next).remove(); 185 | } 186 | Utils_1.dom(highlight).normalizeTextNodes(); 187 | }); 188 | }; 189 | exports.mergeSiblingHighlights = mergeSiblingHighlights; 190 | /** 191 | * Normalizes highlights. Ensures that highlighting is done with use of the smallest possible number of 192 | * wrapping HTML elements. 193 | * Flattens highlights structure and merges sibling highlights. Normalizes text nodes within highlights. 194 | * @param {Array} highlights - highlights to normalize. 195 | * @returns {Array} - array of normalized highlights. Order and number of returned highlights may be different than 196 | * input highlights. 197 | * @memberof TextHighlighter 198 | */ 199 | var normalizeHighlights = function (highlights) { 200 | var normalizedHighlights; 201 | exports.flattenNestedHighlights(highlights); 202 | exports.mergeSiblingHighlights(highlights); 203 | // omit removed nodes 204 | normalizedHighlights = highlights.filter(function (hl) { 205 | return hl.parentElement ? hl : null; 206 | }); 207 | normalizedHighlights = Utils_1.unique(normalizedHighlights); 208 | normalizedHighlights.sort(function (a, b) { 209 | return a.offsetTop - b.offsetTop || a.offsetLeft - b.offsetLeft; 210 | }); 211 | return normalizedHighlights; 212 | }; 213 | exports.normalizeHighlights = normalizeHighlights; 214 | /** 215 | * highlight selected element 216 | * @param el 217 | * @param options 218 | * @param keepRange 219 | */ 220 | var doHighlight = function (el, keepRange, options) { 221 | var range = Utils_1.dom(el).getRange(); 222 | var wrapper, createdHighlights, normalizedHighlights, timestamp; 223 | if (!options) 224 | options = new types_1.optionsImpl(); 225 | options = Utils_1.defaults(options, { 226 | color: "#ffff7b", 227 | highlightedClass: "highlighted", 228 | contextClass: "highlighter-context", 229 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 230 | onRemoveHighlight: function () { 231 | var e = []; 232 | for (var _i = 0; _i < arguments.length; _i++) { 233 | e[_i] = arguments[_i]; 234 | } 235 | return true; 236 | }, 237 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 238 | onBeforeHighlight: function () { 239 | var e = []; 240 | for (var _i = 0; _i < arguments.length; _i++) { 241 | e[_i] = arguments[_i]; 242 | } 243 | return true; 244 | }, 245 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 246 | onAfterHighlight: function () { 247 | var e = []; 248 | for (var _i = 0; _i < arguments.length; _i++) { 249 | e[_i] = arguments[_i]; 250 | } 251 | return true; 252 | } 253 | }); 254 | if (!range || range.collapsed) { 255 | return false; 256 | } 257 | var highlightMade = false; 258 | if (options.onBeforeHighlight && options.onBeforeHighlight(range) === true) { 259 | timestamp = (+new Date()).toString(); 260 | wrapper = createWrapper(options); 261 | wrapper.setAttribute(Utils_1.TIMESTAMP_ATTR, timestamp); 262 | createdHighlights = highlightRange(el, range, wrapper); 263 | if (createdHighlights.length > 0) 264 | highlightMade = true; 265 | normalizedHighlights = exports.normalizeHighlights(createdHighlights); 266 | if (options.onAfterHighlight) 267 | options.onAfterHighlight(range, normalizedHighlights, timestamp); 268 | } 269 | if (!keepRange) { 270 | Utils_1.dom(el).removeAllRanges(); 271 | } 272 | return highlightMade; 273 | }; 274 | exports.doHighlight = doHighlight; 275 | /** 276 | * Deserializes highlights. 277 | * @throws exception when can't parse JSON or JSON has invalid structure. 278 | * @param {object} json - JSON object with highlights definition. 279 | * @returns {Array} - array of deserialized highlights. 280 | * @memberof TextHighlighter 281 | */ 282 | var deserializeHighlights = function (el, json) { 283 | var hlDescriptors; 284 | var highlights = []; 285 | // const self = this; 286 | if (!json) { 287 | return highlights; 288 | } 289 | try { 290 | // const r = String.raw`${json}`; 291 | hlDescriptors = JSON.parse(json); 292 | } 293 | catch (e) { 294 | throw "Can't parse JSON: " + e; 295 | } 296 | function deserializationFn(hlDescriptor) { 297 | var hl = hlDescriptor; 298 | hl.hlpaths = hl.path.split(":").map(Number); 299 | if (!hl.hlpaths || hl.hlpaths.length == 0) 300 | return; 301 | // { 302 | // wrapper: hlDescriptor[0], 303 | // text: hlDescriptor[1], 304 | // path: hlDescriptor[2].split(":"), 305 | // offset: hlDescriptor[3], 306 | // length: hlDescriptor[4] 307 | // }; 308 | var elIndex = hl.hlpaths.pop(), node = el, highlight, idx; 309 | // should have a value and not be 0 (false is = to 0) 310 | if (elIndex != 0 && !elIndex) 311 | return; 312 | while (hl.hlpaths.length > 0) { 313 | idx = hl.hlpaths.shift(); 314 | if (idx || idx == 0) 315 | node = node.childNodes[idx]; 316 | } 317 | if (node.childNodes[elIndex - 1] && 318 | node.childNodes[elIndex - 1].nodeType === Utils_1.NODE_TYPE.TEXT_NODE) { 319 | elIndex -= 1; 320 | } 321 | node = node.childNodes[elIndex]; 322 | if (node instanceof Text) { 323 | var hlNode = node.splitText(hl.offset); 324 | hlNode.splitText(hl.length); 325 | if (hlNode.nextSibling && !hlNode.nextSibling.nodeValue) { 326 | Utils_1.dom(hlNode.nextSibling).remove(); 327 | } 328 | if (hlNode.previousSibling && !hlNode.previousSibling.nodeValue) { 329 | Utils_1.dom(hlNode.previousSibling).remove(); 330 | } 331 | if (hl && hl.wrapper) { 332 | var tmpHtml = Utils_1.dom(hlNode).fromHTML(hl.wrapper)[0]; 333 | if (tmpHtml) { 334 | highlight = Utils_1.dom(hlNode).wrap(tmpHtml); 335 | highlights.push(highlight); 336 | } 337 | } 338 | } 339 | } 340 | hlDescriptors.forEach(function (hlDescriptor) { 341 | try { 342 | deserializationFn(hlDescriptor); 343 | } 344 | catch (e) { 345 | if (console && console.warn) { 346 | console.warn("Can't deserialize highlight descriptor. Cause: " + e); 347 | } 348 | } 349 | }); 350 | return highlights; 351 | }; 352 | exports.deserializeHighlights = deserializeHighlights; 353 | var find = function (el, text, caseSensitive, options) { 354 | var wnd = Utils_1.dom(el).getWindow(); 355 | if (wnd) { 356 | var scrollX_1 = wnd.scrollX, scrollY_1 = wnd.scrollY, caseSens = (typeof caseSensitive === "undefined" ? true : caseSensitive); 357 | // dom(el).removeAllRanges(); 358 | // const test = wnd.innerh 359 | if ("find" in wnd) { 360 | while (wnd.find(text, caseSens)) { 361 | doHighlight(el, true, options); 362 | } 363 | } 364 | else if (wnd.document.body.createTextRange) { 365 | var textRange = wnd.document.body.createTextRange(); 366 | textRange.moveToElementText(el); 367 | while (textRange.findText(text, 1, caseSens ? 4 : 0)) { 368 | if (!Utils_1.dom(el).contains(textRange.parentElement()) && textRange.parentElement() !== el) { 369 | break; 370 | } 371 | textRange.select(); 372 | doHighlight(el, true, options); 373 | textRange.collapse(false); 374 | } 375 | } 376 | Utils_1.dom(el).removeAllRanges(); 377 | wnd.scrollTo(scrollX_1, scrollY_1); 378 | } 379 | }; 380 | exports.find = find; 381 | /** 382 | * Returns highlights from given container. 383 | * @param params 384 | * @param {HTMLElement} [params.container] - return highlights from this element. Default: the element the 385 | * highlighter is applied to. 386 | * @param {boolean} [params.andSelf] - if set to true and container is a highlight itself, add container to 387 | * returned results. Default: true. 388 | * @param {boolean} [params.grouped] - if set to true, highlights are grouped in logical groups of highlights added 389 | * in the same moment. Each group is an object which has got array of highlights, 'toString' method and 'timestamp' 390 | * property. Default: false. 391 | * @returns {Array} - array of highlights. 392 | * @memberof TextHighlighter 393 | */ 394 | var getHighlights = function (el, params) { 395 | if (!params) 396 | params = new types_1.paramsImp(); 397 | params = Utils_1.defaults(params, { 398 | container: el, 399 | andSelf: true, 400 | grouped: false 401 | }); 402 | if (params.container) { 403 | var nodeList = params.container.querySelectorAll("[" + Utils_1.DATA_ATTR + "]"); 404 | var highlights = Array.prototype.slice.call(nodeList); 405 | if (params.andSelf === true && params.container.hasAttribute(Utils_1.DATA_ATTR)) { 406 | highlights.push(params.container); 407 | } 408 | if (params.grouped) { 409 | highlights = Utils_1.groupHighlights(highlights); 410 | } 411 | return highlights; 412 | } 413 | }; 414 | exports.getHighlights = getHighlights; 415 | /** 416 | * Serializes all highlights in the element the highlighter is applied to. 417 | * @returns {string} - stringified JSON with highlights definition 418 | * @memberof TextHighlighter 419 | */ 420 | var serializeHighlights = function (el) { 421 | if (!el) 422 | return; 423 | var highlights = exports.getHighlights(el), refEl = el, hlDescriptors = []; 424 | if (!highlights) 425 | return; 426 | function getElementPath(el, refElement) { 427 | var path = []; 428 | var childNodes; 429 | if (el) 430 | do { 431 | if (el instanceof HTMLElement && el.parentNode) { 432 | childNodes = Array.prototype.slice.call(el.parentNode.childNodes); 433 | path.unshift(childNodes.indexOf(el)); 434 | el = el.parentNode; 435 | } 436 | } while (el !== refElement || !el); 437 | return path; 438 | } 439 | Utils_1.sortByDepth(highlights, false); 440 | // { 441 | // textContent: string | any[]; 442 | // cloneNode: (arg0: boolean) => any; 443 | // previousSibling: { nodeType: number; length: number }; 444 | // } 445 | highlights.forEach(function (highlight) { 446 | if (highlight && highlight.textContent) { 447 | var offset = 0, // Hl offset from previous sibling within parent node. 448 | wrapper = highlight.cloneNode(true); 449 | var length_1 = highlight.textContent.length, hlPath = getElementPath(highlight, refEl); 450 | var color = ""; 451 | if (wrapper instanceof HTMLElement) { 452 | var c = wrapper.getAttribute("data-backgroundcolor"); 453 | if (c) 454 | color = c.trim(); 455 | wrapper.innerHTML = ""; 456 | wrapper = wrapper.outerHTML; 457 | } 458 | if (highlight.previousSibling && 459 | highlight.previousSibling.nodeType === Utils_1.NODE_TYPE.TEXT_NODE && 460 | highlight.previousSibling instanceof Text) { 461 | offset = highlight.previousSibling.length; 462 | } 463 | var hl = { 464 | wrapper: wrapper, 465 | textContent: highlight.textContent, 466 | path: hlPath.join(":"), 467 | color: color, 468 | offset: offset, 469 | length: length_1 470 | }; 471 | hlDescriptors.push(hl); 472 | } 473 | }); 474 | return JSON.stringify(hlDescriptors); 475 | }; 476 | exports.serializeHighlights = serializeHighlights; 477 | var removeHighlights = function (element, options) { 478 | var container = element, highlights = exports.getHighlights(element, { container: container }); 479 | // self = this; 480 | if (!highlights) 481 | return; 482 | if (!options) 483 | options = new types_1.optionsImpl(); 484 | options = Utils_1.defaults(options, { 485 | color: "#ffff7b", 486 | highlightedClass: "highlighted", 487 | contextClass: "highlighter-context", 488 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 489 | onRemoveHighlight: function () { 490 | var e = []; 491 | for (var _i = 0; _i < arguments.length; _i++) { 492 | e[_i] = arguments[_i]; 493 | } 494 | return true; 495 | }, 496 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 497 | onBeforeHighlight: function () { 498 | var e = []; 499 | for (var _i = 0; _i < arguments.length; _i++) { 500 | e[_i] = arguments[_i]; 501 | } 502 | return true; 503 | }, 504 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 505 | onAfterHighlight: function () { 506 | var e = []; 507 | for (var _i = 0; _i < arguments.length; _i++) { 508 | e[_i] = arguments[_i]; 509 | } 510 | return true; 511 | } 512 | }); 513 | function mergeSiblingTextNodes(textNode) { 514 | var prev = textNode.previousSibling, next = textNode.nextSibling; 515 | if (prev && prev.nodeType === Utils_1.NODE_TYPE.TEXT_NODE) { 516 | textNode.nodeValue = prev.nodeValue + textNode.nodeValue; 517 | Utils_1.dom(prev).remove(); 518 | } 519 | if (next && next.nodeType === Utils_1.NODE_TYPE.TEXT_NODE) { 520 | textNode.nodeValue = textNode.nodeValue + next.nodeValue; 521 | Utils_1.dom(next).remove(); 522 | } 523 | } 524 | function removeHighlight(highlight) { 525 | if (!highlight) 526 | return; 527 | var textNodes = Utils_1.dom(highlight).unwrap(); 528 | if (textNodes) 529 | textNodes.forEach(function (node) { 530 | mergeSiblingTextNodes(node); 531 | }); 532 | } 533 | Utils_1.sortByDepth(highlights, true); 534 | highlights.forEach(function (hl) { 535 | if (options && 536 | options.onRemoveHighlight && 537 | options.onRemoveHighlight(hl) === true) { 538 | removeHighlight(hl); 539 | } 540 | }); 541 | }; 542 | exports.removeHighlights = removeHighlights; 543 | -------------------------------------------------------------------------------- /lib/TextHighlighter.d.ts: -------------------------------------------------------------------------------- 1 | import { TextHighlighterType } from "./types"; 2 | declare const TextHighlighter: TextHighlighterType; 3 | export { TextHighlighter }; 4 | -------------------------------------------------------------------------------- /lib/TextHighlighter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.TextHighlighter = void 0; 4 | var Library_1 = require("./Library"); 5 | var Utils_1 = require("./Utils"); 6 | var TextHighlighter = function (element, options) { 7 | if (!element) { 8 | throw "Missing anchor element"; 9 | } 10 | this.el = element; 11 | this.options = Utils_1.defaults(options, { 12 | color: "#ffff7b", 13 | highlightedClass: "highlighted", 14 | contextClass: "highlighter-context", 15 | onRemoveHighlight: function () { return true; }, 16 | onBeforeHighlight: function () { return true; }, 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | onAfterHighlight: function () { 19 | var e = []; 20 | for (var _i = 0; _i < arguments.length; _i++) { 21 | e[_i] = arguments[_i]; 22 | } 23 | return true; 24 | } 25 | }); 26 | if (this.options && this.options.contextClass) 27 | Utils_1.dom(this.el).addClass(this.options.contextClass); 28 | Utils_1.bindEvents(this.el, this); 29 | return this; 30 | }; 31 | exports.TextHighlighter = TextHighlighter; 32 | /** 33 | * Permanently disables highlighting. 34 | * Unbinds events and remove context element class. 35 | * @memberof TextHighlighter 36 | */ 37 | TextHighlighter.prototype.destroy = function () { 38 | Utils_1.unbindEvents(this.el, this); 39 | Utils_1.dom(this.el).removeClass(this.options.contextClass); 40 | }; 41 | TextHighlighter.prototype.highlightHandler = function () { 42 | this.doHighlight(); 43 | }; 44 | TextHighlighter.prototype.doHighlight = function (keepRange) { 45 | Library_1.doHighlight(this.el, keepRange, this.options); 46 | }; 47 | TextHighlighter.prototype.highlightRange = function (range, wrapper) { 48 | Library_1.highlightRange(this.el, range, wrapper); 49 | }; 50 | TextHighlighter.prototype.normalizeHighlights = function (highlights) { 51 | Library_1.normalizeHighlights(highlights); 52 | }; 53 | TextHighlighter.prototype.flattenNestedHighlights = function (highlights) { 54 | Library_1.flattenNestedHighlights(highlights); 55 | }; 56 | TextHighlighter.prototype.mergeSiblingHighlights = function (highlights) { 57 | Library_1.mergeSiblingHighlights(highlights); 58 | }; 59 | TextHighlighter.prototype.setColor = function (color) { 60 | this.options.color = color; 61 | }; 62 | TextHighlighter.prototype.getColor = function () { 63 | return this.options.color; 64 | }; 65 | TextHighlighter.prototype.removeHighlights = function (element) { 66 | var container = element || this.el; 67 | Library_1.removeHighlights(container, this.options); 68 | }; 69 | TextHighlighter.prototype.getHighlights = function (params) { 70 | Library_1.getHighlights(this.el, params); 71 | }; 72 | TextHighlighter.prototype.isHighlight = function (el) { 73 | return el && el.nodeType === Utils_1.NODE_TYPE.ELEMENT_NODE && el.hasAttribute(Utils_1.DATA_ATTR); 74 | }; 75 | TextHighlighter.prototype.serializeHighlights = function () { 76 | return Library_1.serializeHighlights(this.el); 77 | }; 78 | TextHighlighter.prototype.deserializeHighlights = function (json) { 79 | Library_1.deserializeHighlights(this.el, json); 80 | }; 81 | TextHighlighter.prototype.find = function (text, caseSensitive) { 82 | Library_1.find(this.el, text, caseSensitive); 83 | }; 84 | TextHighlighter.createWrapper = function (options) { 85 | Library_1.createWrapper(options); 86 | }; 87 | -------------------------------------------------------------------------------- /lib/Utils.d.ts: -------------------------------------------------------------------------------- 1 | declare const /** 2 | * Attribute added by default to every highlight. 3 | * @type {string} 4 | */ DATA_ATTR = "data-highlighted", 5 | /** 6 | * Attribute used to group highlight wrappers. 7 | * @type {string} 8 | */ 9 | TIMESTAMP_ATTR = "data-timestamp", NODE_TYPE: { 10 | ELEMENT_NODE: number; 11 | TEXT_NODE: number; 12 | }, 13 | /** 14 | * Don't highlight content of these tags. 15 | * @type {string[]} 16 | */ 17 | IGNORE_TAGS: string[]; 18 | declare function activator(type: { 19 | new (): T; 20 | }): T; 21 | /** 22 | * Groups given highlights by timestamp. 23 | * @param {Array} highlights 24 | * @returns {Array} Grouped highlights. 25 | */ 26 | declare function groupHighlights(highlights: any): any; 27 | /** 28 | * Fills undefined values in obj with default properties with the same name from source object. 29 | * @param {object} obj - target object, can't be null, must be initialized first 30 | * @param {object} source - source object with default values 31 | * @returns {object} 32 | */ 33 | declare function defaults(obj: T, source: T): T; 34 | /** 35 | * Returns array without duplicated values. 36 | * @param {Array} arr 37 | * @returns {Array} 38 | */ 39 | declare function unique(arr: any): any; 40 | /** 41 | * Takes range object as parameter and refines it boundaries 42 | * @param range 43 | * @returns {object} refined boundaries and initial state of highlighting algorithm. 44 | */ 45 | declare function refineRangeBoundaries(range: Range): { 46 | startContainer: Node | (Node & ParentNode) | null; 47 | endContainer: Node | null; 48 | goDeeper: boolean; 49 | }; 50 | export declare function bindEvents(el: HTMLElement, scope: any): void; 51 | export declare function unbindEvents(el: HTMLElement, scope: any): void; 52 | /** 53 | * Utility functions to make DOM manipulation easier. 54 | * @param {Node|HTMLElement} [el] - base DOM element to manipulate 55 | * @returns {object} 56 | */ 57 | declare const dom: (el: Node | HTMLElement | null | undefined) => { 58 | /** 59 | * Adds class to element. 60 | * @param {string} className 61 | */ 62 | addClass: (className: string) => void; 63 | /** 64 | * Removes class from element. 65 | * @param {string} className 66 | */ 67 | removeClass: (className: string) => void; 68 | /** 69 | * Prepends child nodes to base element. 70 | * @param {Node[]} nodesToPrepend 71 | */ 72 | prepend: (nodesToPrepend: any) => void; 73 | /** 74 | * Appends child nodes to base element. 75 | * @param {Node[]} nodesToAppend 76 | */ 77 | append: (nodesToAppend: any) => void; 78 | /** 79 | * Inserts base element after refEl. 80 | * @param {Node} refEl - node after which base element will be inserted 81 | * @returns {Node} - inserted element 82 | */ 83 | insertAfter: (refEl: { 84 | parentNode: { 85 | insertBefore: (arg0: any, arg1: any) => any; 86 | }; 87 | nextSibling: any; 88 | }) => any; 89 | /** 90 | * Inserts base element before refEl. 91 | * @param {Node} refEl - node before which base element will be inserted 92 | * @returns {Node} - inserted element 93 | */ 94 | insertBefore: (refEl: { 95 | parentNode: { 96 | insertBefore: (arg0: any, arg1: any) => any; 97 | }; 98 | }) => any; 99 | /** 100 | * Removes base element from DOM. 101 | */ 102 | remove: () => void; 103 | /** 104 | * Returns true if base element contains given child. 105 | * @param {Node|HTMLElement} child 106 | * @returns {boolean} 107 | */ 108 | contains: (child: any) => boolean | null | undefined; 109 | /** 110 | * Wraps base element in wrapper element. 111 | * @param {HTMLElement} wrapper 112 | * @returns {HTMLElement} wrapper element 113 | */ 114 | wrap: (wrapper: HTMLElement) => HTMLElement; 115 | /** 116 | * Unwraps base element. 117 | * @returns {Node[]} - child nodes of unwrapped element. 118 | */ 119 | unwrap: () => any[] | undefined; 120 | /** 121 | * Returns array of base element parents. 122 | * @returns {HTMLElement[]} 123 | */ 124 | parents: () => (Node & ParentNode)[]; 125 | /** 126 | * Normalizes text nodes within base element, ie. merges sibling text nodes and assures that every 127 | * element node has only one text node. 128 | * It should does the same as standard element.normalize, but IE implements it incorrectly. 129 | */ 130 | normalizeTextNodes: () => void; 131 | /** 132 | * Returns element background color. 133 | * @returns {CSSStyleDeclaration.backgroundColor} 134 | */ 135 | color: () => string | null; 136 | /** 137 | * Creates dom element from given html string. 138 | * @param {string} html 139 | * @returns {NodeList} 140 | */ 141 | fromHTML: (html: string) => NodeListOf; 142 | /** 143 | * Returns first range of the window of base element. 144 | * @returns {Range} 145 | */ 146 | getRange: () => Range | undefined; 147 | /** 148 | * Removes all ranges of the window of base element. 149 | */ 150 | removeAllRanges: () => void; 151 | /** 152 | * Returns selection object of the window of base element. 153 | * @returns {Selection} 154 | */ 155 | getSelection: () => Selection | null; 156 | /** 157 | * Returns window of the base element. 158 | * @returns {Window} 159 | */ 160 | getWindow: () => (Window & typeof globalThis) | null; 161 | /** 162 | * Returns document of the base element. 163 | * @returns {HTMLDocument} 164 | */ 165 | getDocument: () => Node | undefined; 166 | }; 167 | /** 168 | * Returns true if elements a i b have the same color. 169 | * @param {Node} a 170 | * @param {Node} b 171 | * @returns {boolean} 172 | */ 173 | declare function haveSameColor(a: Node, b: Node): boolean; 174 | /** 175 | * Sorts array of DOM elements by its depth in DOM tree. 176 | * @param {HTMLElement[]} arr - array to sort. 177 | * @param {boolean} descending - order of sort. 178 | */ 179 | declare function sortByDepth(arr: any, descending: any): void; 180 | export { DATA_ATTR, TIMESTAMP_ATTR, NODE_TYPE, IGNORE_TAGS, dom, refineRangeBoundaries, sortByDepth, unique, haveSameColor, defaults, groupHighlights, activator }; 181 | -------------------------------------------------------------------------------- /lib/Utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // highlight extensions 3 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 4 | // type H_HTMLElement = HTMLElement; 5 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 6 | // interface ie_HTMLElement extends HTMLElement { 7 | // createTextRange(): TextRange; 8 | // } 9 | Object.defineProperty(exports, "__esModule", { value: true }); 10 | 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; 11 | // eslint-disable-next-line @typescript-eslint/class-name-casing 12 | // interface H_Node extends Node { 13 | // splitText(endOffset: number): any; 14 | // } 15 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 16 | // interface H_Window extends Window { 17 | // find(text: any, caseSens: any): boolean; 18 | // } 19 | var /** 20 | * Attribute added by default to every highlight. 21 | * @type {string} 22 | */ DATA_ATTR = "data-highlighted", 23 | /** 24 | * Attribute used to group highlight wrappers. 25 | * @type {string} 26 | */ 27 | TIMESTAMP_ATTR = "data-timestamp", NODE_TYPE = { 28 | ELEMENT_NODE: 1, 29 | TEXT_NODE: 3 30 | }, 31 | /** 32 | * Don't highlight content of these tags. 33 | * @type {string[]} 34 | */ 35 | IGNORE_TAGS = [ 36 | "SCRIPT", 37 | "STYLE", 38 | "SELECT", 39 | "OPTION", 40 | "BUTTON", 41 | "OBJECT", 42 | "APPLET", 43 | "VIDEO", 44 | "AUDIO", 45 | "CANVAS", 46 | "EMBED", 47 | "PARAM", 48 | "METER", 49 | "PROGRESS" 50 | ]; 51 | exports.DATA_ATTR = DATA_ATTR; 52 | exports.TIMESTAMP_ATTR = TIMESTAMP_ATTR; 53 | exports.NODE_TYPE = NODE_TYPE; 54 | exports.IGNORE_TAGS = IGNORE_TAGS; 55 | function activator(type) { 56 | return new type(); 57 | } 58 | exports.activator = activator; 59 | /** 60 | * Groups given highlights by timestamp. 61 | * @param {Array} highlights 62 | * @returns {Array} Grouped highlights. 63 | */ 64 | function groupHighlights(highlights) { 65 | var order = [], chunks = {}, grouped = []; 66 | highlights.forEach(function (hl) { 67 | var timestamp = hl.getAttribute(TIMESTAMP_ATTR); 68 | if (typeof chunks[timestamp] === "undefined") { 69 | chunks[timestamp] = []; 70 | order.push(timestamp); 71 | } 72 | chunks[timestamp].push(hl); 73 | }); 74 | order.forEach(function (timestamp) { 75 | var group = chunks[timestamp]; 76 | grouped.push({ 77 | chunks: group, 78 | timestamp: timestamp, 79 | toString: function () { 80 | return group 81 | .map(function (h) { 82 | return h.textContent; 83 | }) 84 | .join(""); 85 | } 86 | }); 87 | }); 88 | return grouped; 89 | } 90 | exports.groupHighlights = groupHighlights; 91 | /** 92 | * Fills undefined values in obj with default properties with the same name from source object. 93 | * @param {object} obj - target object, can't be null, must be initialized first 94 | * @param {object} source - source object with default values 95 | * @returns {object} 96 | */ 97 | function defaults(obj, source) { 98 | if (obj == null) 99 | obj = {}; 100 | for (var prop in source) { 101 | if (Object.prototype.hasOwnProperty.call(source, prop) && 102 | obj[prop] === void 0) { 103 | obj[prop] = source[prop]; 104 | } 105 | } 106 | return obj; 107 | } 108 | exports.defaults = defaults; 109 | /** 110 | * Returns array without duplicated values. 111 | * @param {Array} arr 112 | * @returns {Array} 113 | */ 114 | function unique(arr) { 115 | return arr.filter(function (value, idx, self) { 116 | return self.indexOf(value) === idx; 117 | }); 118 | } 119 | exports.unique = unique; 120 | /** 121 | * Takes range object as parameter and refines it boundaries 122 | * @param range 123 | * @returns {object} refined boundaries and initial state of highlighting algorithm. 124 | */ 125 | function refineRangeBoundaries(range) { 126 | var startContainer = range.startContainer, endContainer = range.endContainer, goDeeper = true; 127 | var ancestor = range.commonAncestorContainer; 128 | if (range.endOffset === 0) { 129 | while (endContainer && 130 | !endContainer.previousSibling && 131 | endContainer.parentNode !== ancestor) { 132 | endContainer = endContainer.parentNode; 133 | } 134 | if (endContainer) 135 | endContainer = endContainer.previousSibling; 136 | } 137 | else if (endContainer.nodeType === NODE_TYPE.TEXT_NODE) { 138 | if (endContainer && 139 | endContainer.nodeValue && 140 | range.endOffset < endContainer.nodeValue.length) { 141 | var t = endContainer; 142 | t.splitText(range.endOffset); 143 | } 144 | } 145 | else if (range.endOffset > 0) { 146 | endContainer = endContainer.childNodes.item(range.endOffset - 1); 147 | } 148 | if (startContainer.nodeType === NODE_TYPE.TEXT_NODE) { 149 | if (startContainer && 150 | startContainer.nodeValue && 151 | range.startOffset === startContainer.nodeValue.length) { 152 | goDeeper = false; 153 | } 154 | else if (startContainer instanceof Node && range.startOffset > 0) { 155 | var t = startContainer; 156 | startContainer = t.splitText(range.startOffset); 157 | if (startContainer && endContainer === startContainer.previousSibling) { 158 | endContainer = startContainer; 159 | } 160 | } 161 | } 162 | else if (range.startOffset < startContainer.childNodes.length) { 163 | startContainer = startContainer.childNodes.item(range.startOffset); 164 | } 165 | else { 166 | startContainer = startContainer.nextSibling; 167 | } 168 | return { 169 | startContainer: startContainer, 170 | endContainer: endContainer, 171 | goDeeper: goDeeper 172 | }; 173 | } 174 | exports.refineRangeBoundaries = refineRangeBoundaries; 175 | function bindEvents(el, scope) { 176 | el.addEventListener("mouseup", scope.highlightHandler.bind(scope)); 177 | el.addEventListener("touchend", scope.highlightHandler.bind(scope)); 178 | } 179 | exports.bindEvents = bindEvents; 180 | function unbindEvents(el, scope) { 181 | el.removeEventListener("mouseup", scope.highlightHandler.bind(scope)); 182 | el.removeEventListener("touchend", scope.highlightHandler.bind(scope)); 183 | } 184 | exports.unbindEvents = unbindEvents; 185 | /** 186 | * Utility functions to make DOM manipulation easier. 187 | * @param {Node|HTMLElement} [el] - base DOM element to manipulate 188 | * @returns {object} 189 | */ 190 | var dom = function (el) { 191 | return /** @lends dom **/ { 192 | /** 193 | * Adds class to element. 194 | * @param {string} className 195 | */ 196 | addClass: function (className) { 197 | if (el instanceof HTMLElement) 198 | if (el.classList) { 199 | el.classList.add(className); 200 | } 201 | else { 202 | el.className += " " + className; 203 | } 204 | }, 205 | /** 206 | * Removes class from element. 207 | * @param {string} className 208 | */ 209 | removeClass: function (className) { 210 | if (el instanceof HTMLElement) { 211 | if (el.classList) { 212 | el.classList.remove(className); 213 | } 214 | else { 215 | el.className = el.className.replace(new RegExp("(^|\\b)" + className + "(\\b|$)", "gi"), " "); 216 | } 217 | } 218 | }, 219 | /** 220 | * Prepends child nodes to base element. 221 | * @param {Node[]} nodesToPrepend 222 | */ 223 | prepend: function (nodesToPrepend) { 224 | var nodes = Array.prototype.slice.call(nodesToPrepend); 225 | var i = nodes.length; 226 | if (el) 227 | while (i--) { 228 | el.insertBefore(nodes[i], el.firstChild); 229 | } 230 | }, 231 | /** 232 | * Appends child nodes to base element. 233 | * @param {Node[]} nodesToAppend 234 | */ 235 | append: function (nodesToAppend) { 236 | if (el) { 237 | var nodes = Array.prototype.slice.call(nodesToAppend); 238 | for (var i = 0, len = nodes.length; i < len; ++i) { 239 | el.appendChild(nodes[i]); 240 | } 241 | } 242 | }, 243 | /** 244 | * Inserts base element after refEl. 245 | * @param {Node} refEl - node after which base element will be inserted 246 | * @returns {Node} - inserted element 247 | */ 248 | insertAfter: function (refEl) { 249 | return refEl.parentNode.insertBefore(el, refEl.nextSibling); 250 | }, 251 | /** 252 | * Inserts base element before refEl. 253 | * @param {Node} refEl - node before which base element will be inserted 254 | * @returns {Node} - inserted element 255 | */ 256 | insertBefore: function (refEl) { 257 | return refEl.parentNode 258 | ? refEl.parentNode.insertBefore(el, refEl) 259 | : refEl; 260 | }, 261 | /** 262 | * Removes base element from DOM. 263 | */ 264 | remove: function () { 265 | if (el && el.parentNode) { 266 | el.parentNode.removeChild(el); 267 | el = null; 268 | } 269 | }, 270 | /** 271 | * Returns true if base element contains given child. 272 | * @param {Node|HTMLElement} child 273 | * @returns {boolean} 274 | */ 275 | contains: function (child) { 276 | return el && el !== child && el.contains(child); 277 | }, 278 | /** 279 | * Wraps base element in wrapper element. 280 | * @param {HTMLElement} wrapper 281 | * @returns {HTMLElement} wrapper element 282 | */ 283 | wrap: function (wrapper) { 284 | if (el) { 285 | if (el.parentNode) { 286 | el.parentNode.insertBefore(wrapper, el); 287 | } 288 | wrapper.appendChild(el); 289 | } 290 | return wrapper; 291 | }, 292 | /** 293 | * Unwraps base element. 294 | * @returns {Node[]} - child nodes of unwrapped element. 295 | */ 296 | unwrap: function () { 297 | if (el) { 298 | var nodes = Array.prototype.slice.call(el.childNodes); 299 | var wrapper_1; 300 | // debugger; 301 | nodes.forEach(function (node) { 302 | wrapper_1 = node.parentNode; 303 | var d = dom(node); 304 | if (d && node.parentNode) 305 | d.insertBefore(node.parentNode); 306 | dom(wrapper_1).remove(); 307 | }); 308 | return nodes; 309 | } 310 | }, 311 | /** 312 | * Returns array of base element parents. 313 | * @returns {HTMLElement[]} 314 | */ 315 | parents: function () { 316 | var parent; 317 | var path = []; 318 | if (el) { 319 | while ((parent = el.parentNode)) { 320 | path.push(parent); 321 | el = parent; 322 | } 323 | } 324 | return path; 325 | }, 326 | /** 327 | * Normalizes text nodes within base element, ie. merges sibling text nodes and assures that every 328 | * element node has only one text node. 329 | * It should does the same as standard element.normalize, but IE implements it incorrectly. 330 | */ 331 | normalizeTextNodes: function () { 332 | if (!el) { 333 | return; 334 | } 335 | if (el.nodeType === NODE_TYPE.TEXT_NODE && 336 | el.nodeValue && 337 | el.parentNode) { 338 | while (el.nextSibling && 339 | el.nextSibling.nodeType === NODE_TYPE.TEXT_NODE) { 340 | el.nodeValue += el.nextSibling.nodeValue; 341 | el.parentNode.removeChild(el.nextSibling); 342 | } 343 | } 344 | else { 345 | dom(el.firstChild).normalizeTextNodes(); 346 | } 347 | dom(el.nextSibling).normalizeTextNodes(); 348 | }, 349 | /** 350 | * Returns element background color. 351 | * @returns {CSSStyleDeclaration.backgroundColor} 352 | */ 353 | color: function () { 354 | return el instanceof HTMLElement && el.style 355 | ? el.style.backgroundColor 356 | : null; 357 | }, 358 | /** 359 | * Creates dom element from given html string. 360 | * @param {string} html 361 | * @returns {NodeList} 362 | */ 363 | fromHTML: function (html) { 364 | var div = document.createElement("div"); 365 | div.innerHTML = html; 366 | return div.childNodes; 367 | }, 368 | /** 369 | * Returns first range of the window of base element. 370 | * @returns {Range} 371 | */ 372 | getRange: function () { 373 | var selection = dom(el).getSelection(); 374 | var range; 375 | if (selection && selection.rangeCount > 0) { 376 | range = selection.getRangeAt(0); 377 | } 378 | return range; 379 | }, 380 | /** 381 | * Removes all ranges of the window of base element. 382 | */ 383 | removeAllRanges: function () { 384 | var selection = dom(el).getSelection(); 385 | if (selection) 386 | selection.removeAllRanges(); 387 | }, 388 | /** 389 | * Returns selection object of the window of base element. 390 | * @returns {Selection} 391 | */ 392 | getSelection: function () { 393 | var win = dom(el).getWindow(); 394 | return win ? win.getSelection() : null; 395 | }, 396 | /** 397 | * Returns window of the base element. 398 | * @returns {Window} 399 | */ 400 | getWindow: function () { 401 | var doc = dom(el).getDocument(); 402 | return doc instanceof Document ? doc.defaultView : null; 403 | }, 404 | /** 405 | * Returns document of the base element. 406 | * @returns {HTMLDocument} 407 | */ 408 | getDocument: function () { 409 | // if ownerDocument is null then el is the document itself. 410 | if (el) 411 | return el.ownerDocument || el; 412 | } 413 | }; 414 | }; 415 | exports.dom = dom; 416 | /** 417 | * Returns true if elements a i b have the same color. 418 | * @param {Node} a 419 | * @param {Node} b 420 | * @returns {boolean} 421 | */ 422 | function haveSameColor(a, b) { 423 | return dom(a).color() === dom(b).color(); 424 | } 425 | exports.haveSameColor = haveSameColor; 426 | /** 427 | * Sorts array of DOM elements by its depth in DOM tree. 428 | * @param {HTMLElement[]} arr - array to sort. 429 | * @param {boolean} descending - order of sort. 430 | */ 431 | function sortByDepth(arr, descending) { 432 | arr.sort(function (a, b) { 433 | return (dom(descending ? b : a).parents().length - 434 | dom(descending ? a : b).parents().length); 435 | }); 436 | } 437 | exports.sortByDepth = sortByDepth; 438 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, createWrapper, highlightRange } from "./Library"; 2 | import { TextHighlighter } from "./TextHighlighter"; 3 | import { optionsImpl } from "./types"; 4 | export { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, optionsImpl, createWrapper, highlightRange, TextHighlighter }; 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.TextHighlighter = exports.highlightRange = exports.createWrapper = exports.optionsImpl = exports.removeHighlights = exports.serializeHighlights = exports.deserializeHighlights = exports.doHighlight = void 0; 4 | var Library_1 = require("./Library"); 5 | Object.defineProperty(exports, "doHighlight", { enumerable: true, get: function () { return Library_1.doHighlight; } }); 6 | Object.defineProperty(exports, "deserializeHighlights", { enumerable: true, get: function () { return Library_1.deserializeHighlights; } }); 7 | Object.defineProperty(exports, "serializeHighlights", { enumerable: true, get: function () { return Library_1.serializeHighlights; } }); 8 | Object.defineProperty(exports, "removeHighlights", { enumerable: true, get: function () { return Library_1.removeHighlights; } }); 9 | Object.defineProperty(exports, "createWrapper", { enumerable: true, get: function () { return Library_1.createWrapper; } }); 10 | Object.defineProperty(exports, "highlightRange", { enumerable: true, get: function () { return Library_1.highlightRange; } }); 11 | var TextHighlighter_1 = require("./TextHighlighter"); 12 | Object.defineProperty(exports, "TextHighlighter", { enumerable: true, get: function () { return TextHighlighter_1.TextHighlighter; } }); 13 | var types_1 = require("./types"); 14 | Object.defineProperty(exports, "optionsImpl", { enumerable: true, get: function () { return types_1.optionsImpl; } }); 15 | -------------------------------------------------------------------------------- /lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface TextHighlighterI { 2 | doHighlight: any; 3 | deserializeHighlights: any; 4 | serializeHighlights: any; 5 | removeHighlights: any; 6 | optionsImpl: any; 7 | } 8 | export interface TextHighlighterSelf { 9 | el?: HTMLElement; 10 | options?: optionsImpl; 11 | } 12 | export declare type TextHighlighterType = (element: HTMLElement, options: optionsImpl) => void; 13 | export interface TextRange { 14 | collapse(arg0: boolean): any; 15 | select(): void; 16 | parentElement(): any; 17 | findText(text: any, arg1: number, arg2: number): any; 18 | moveToElementText(el: any): any; 19 | } 20 | export declare class highlightI { 21 | highlightHandler: any; 22 | options: optionsImpl | undefined; 23 | el: HTMLElement | undefined; 24 | } 25 | export interface optionsI { 26 | color?: string; 27 | highlightedClass?: string; 28 | contextClass?: string; 29 | onRemoveHighlight?: { 30 | (...e: any[]): boolean; 31 | }; 32 | onBeforeHighlight?: { 33 | (...e: any[]): boolean; 34 | }; 35 | onAfterHighlight?: { 36 | (...e: any[]): boolean; 37 | }; 38 | } 39 | export declare class optionsImpl implements optionsI { 40 | color?: string | undefined; 41 | highlightedClass?: string | undefined; 42 | contextClass?: string | undefined; 43 | onRemoveHighlight?: { 44 | (...e: any[]): boolean; 45 | }; 46 | onBeforeHighlight?: { 47 | (...e: any[]): boolean; 48 | }; 49 | onAfterHighlight?: { 50 | (...e: any[]): boolean; 51 | }; 52 | } 53 | export declare class paramsImp { 54 | container?: HTMLElement; 55 | andSelf?: boolean; 56 | grouped?: any; 57 | } 58 | export interface hlDescriptorI { 59 | wrapper: string; 60 | textContent: string; 61 | color: string; 62 | hlpaths?: number[]; 63 | path: string; 64 | offset: number; 65 | length: number; 66 | } 67 | -------------------------------------------------------------------------------- /lib/types/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.paramsImp = exports.optionsImpl = exports.highlightI = void 0; 4 | // eslint-disable-next-line @typescript-eslint/class-name-casing 5 | var highlightI = /** @class */ (function () { 6 | function highlightI() { 7 | } 8 | return highlightI; 9 | }()); 10 | exports.highlightI = highlightI; 11 | // eslint-disable-next-line @typescript-eslint/class-name-casing 12 | var optionsImpl = /** @class */ (function () { 13 | function optionsImpl() { 14 | } 15 | return optionsImpl; 16 | }()); 17 | exports.optionsImpl = optionsImpl; 18 | // class containerI{ 19 | // querySelectorAll: (arg0: string): any; 20 | // hasAttribute: (arg0: string) => any; 21 | // } 22 | // eslint-disable-next-line @typescript-eslint/class-name-casing 23 | var paramsImp = /** @class */ (function () { 24 | function paramsImp() { 25 | } 26 | return paramsImp; 27 | }()); 28 | exports.paramsImp = paramsImp; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funktechno/texthighlighter", 3 | "version": "2.1.2", 4 | "description": "a no dependency typescript supported tool for highlighting user selected content", 5 | "main": "index.js", 6 | "files": [ 7 | "lib/**/*" 8 | ], 9 | "scripts": { 10 | "build": "tsc && node build/replace", 11 | "build:client": "browserify client/index.ts -p [ tsify --noImplicitAny ] > dist/TextHighlighter.js && node build/replace", 12 | "prepare": "npm run build", 13 | "prepublishOnly": "npm test && npm run lint", 14 | "preversion": "npm run lint", 15 | "version": "npm run format && git add -A src", 16 | "postversion": "git push && git push --tags", 17 | "test": "jest -c ./jest.config.js --forceExit --verbose -i --no-cache", 18 | "test:coverage": "jest --forceExit --coverage --verbose", 19 | "test:watch": "jest --watchAll", 20 | "lint": "tsc --noEmit && eslint \"{src,client}/**/*.{js,ts}\"", 21 | "lint:fix": "tsc --noEmit && eslint \"{src,client}/**/*.{js,ts}\" --fix" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/funktechno/texthighlighter.git" 26 | }, 27 | "keywords": [ 28 | "typescript", 29 | "highlight", 30 | "text", 31 | "mobile", 32 | "dom", 33 | "html", 34 | "color", 35 | "annotation", 36 | "selection", 37 | "range" 38 | ], 39 | "author": "lastlink", 40 | "license": "ISC", 41 | "bugs": { 42 | "url": "https://github.com/funktechno/texthighlighter/issues" 43 | }, 44 | "homepage": "https://github.com/funktechno/texthighlighter#readme", 45 | "devDependencies": { 46 | "@types/eslint": "^7.2.4", 47 | "@types/jest": "^26.0.4", 48 | "@types/node": "^16.4.7", 49 | "@typescript-eslint/eslint-plugin": "^2.24.0", 50 | "@typescript-eslint/parser": "^2.24.0", 51 | "browserify": "^17.0.0", 52 | "eslint": "^6.8.0", 53 | "five-server": "0.0.28", 54 | "jest": "^26.1.0", 55 | "jest-junit": "^12.0.0", 56 | "jest-serial-runner": "^1.1.0", 57 | "replace-in-file": "^6.2.0", 58 | "ts-jest": "^26.1.1", 59 | "ts-loader": "^8.0.9", 60 | "tsify": "^5.0.2", 61 | "typescript": "^4.0.5", 62 | "ts-node": "^10.1.0" 63 | }, 64 | "directories": { 65 | "lib": "lib", 66 | "test": "tests" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Library.ts: -------------------------------------------------------------------------------- 1 | // TextHighLighterv2 library 2 | // Port by: lastlink 3 | import { optionsImpl, hlDescriptorI, paramsImp } from "./types"; 4 | import { 5 | TIMESTAMP_ATTR, 6 | IGNORE_TAGS, 7 | NODE_TYPE, 8 | DATA_ATTR, 9 | dom, 10 | refineRangeBoundaries, 11 | sortByDepth, 12 | unique, 13 | haveSameColor, 14 | defaults, 15 | groupHighlights 16 | } from "./Utils"; 17 | 18 | /** 19 | * Creates wrapper for highlights. 20 | * TextHighlighter instance calls this method each time it needs to create highlights and pass options retrieved 21 | * in constructor. 22 | * @param {object} options - the same object as in TextHighlighter constructor. 23 | * @returns {HTMLElement} 24 | * @memberof TextHighlighter 25 | * @static 26 | */ 27 | function createWrapper(options: optionsImpl) { 28 | const span = document.createElement("span"); 29 | if (options.color) { 30 | span.style.backgroundColor = options.color; 31 | span.setAttribute("data-backgroundcolor", options.color); 32 | } 33 | if (options.highlightedClass) span.className = options.highlightedClass; 34 | return span; 35 | } 36 | 37 | /** 38 | * Highlights range. 39 | * Wraps text of given range object in wrapper element. 40 | * @param {Range} range 41 | * @param {HTMLElement} wrapper 42 | * @returns {Array} - array of created highlights. 43 | * @memberof TextHighlighter 44 | */ 45 | const highlightRange = function ( 46 | el: HTMLElement, 47 | range: Range, 48 | wrapper: { cloneNode: (arg0: boolean) => any } 49 | ): HTMLElement[] { 50 | if (!range || range.collapsed) { 51 | return []; 52 | } 53 | 54 | const result = refineRangeBoundaries(range); 55 | const startContainer = result.startContainer, 56 | endContainer = result.endContainer, 57 | highlights = []; 58 | 59 | let goDeeper = result.goDeeper, 60 | done = false, 61 | node = startContainer, 62 | highlight, 63 | wrapperClone, 64 | nodeParent; 65 | 66 | do { 67 | if (node && goDeeper && node.nodeType === NODE_TYPE.TEXT_NODE) { 68 | if ( 69 | node.parentNode instanceof HTMLElement && 70 | node.parentNode.tagName && 71 | node.nodeValue && 72 | IGNORE_TAGS.indexOf(node.parentNode.tagName) === -1 && 73 | node.nodeValue.trim() !== "" 74 | ) { 75 | wrapperClone = wrapper.cloneNode(true); 76 | wrapperClone.setAttribute(DATA_ATTR, true); 77 | nodeParent = node.parentNode; 78 | 79 | // highlight if a node is inside the el 80 | if (dom(el).contains(nodeParent) || nodeParent === el) { 81 | highlight = dom(node).wrap(wrapperClone); 82 | highlights.push(highlight); 83 | } 84 | } 85 | 86 | goDeeper = false; 87 | } 88 | if ( 89 | node === endContainer && 90 | endContainer && 91 | !(endContainer.hasChildNodes() && goDeeper) 92 | ) { 93 | done = true; 94 | } 95 | 96 | if ( 97 | node instanceof HTMLElement && 98 | node.tagName && 99 | IGNORE_TAGS.indexOf(node.tagName) > -1 100 | ) { 101 | if ( 102 | endContainer instanceof HTMLElement && 103 | endContainer.parentNode === node 104 | ) { 105 | done = true; 106 | } 107 | goDeeper = false; 108 | } 109 | if ( 110 | goDeeper && 111 | (node instanceof Text || node instanceof HTMLElement) && 112 | node.hasChildNodes() 113 | ) { 114 | node = node.firstChild; 115 | } else if (node && node.nextSibling) { 116 | node = node.nextSibling; 117 | goDeeper = true; 118 | } else if (node) { 119 | node = node.parentNode; 120 | goDeeper = false; 121 | } 122 | } while (!done); 123 | 124 | return highlights; 125 | }; 126 | 127 | // : { 128 | // nodeType: number; 129 | // hasAttribute: (arg0: string) => any; 130 | // } 131 | 132 | /** 133 | * Returns true if element is a highlight. 134 | * All highlights have 'data-highlighted' attribute. 135 | * @param el - element to check. 136 | * @returns {boolean} 137 | * @memberof TextHighlighter 138 | */ 139 | const isHighlight = function (el: HTMLElement) { 140 | return ( 141 | el && el.nodeType === NODE_TYPE.ELEMENT_NODE && el.hasAttribute(DATA_ATTR) 142 | ); 143 | }; 144 | /** 145 | * Flattens highlights structure. 146 | * Note: this method changes input highlights - their order and number after calling this method may change. 147 | * @param {Array} highlights - highlights to flatten. 148 | * @memberof TextHighlighter 149 | */ 150 | export const flattenNestedHighlights = function (highlights: any[]) { 151 | let again; 152 | // self = this; 153 | 154 | sortByDepth(highlights, true); 155 | 156 | const flattenOnce = () => { 157 | let again = false; 158 | 159 | highlights.forEach((hl: Node, i: number | number) => { 160 | const parent = hl.parentElement; 161 | if (parent) { 162 | const parentPrev = parent.previousSibling, 163 | parentNext = parent.nextSibling; 164 | 165 | if (isHighlight(parent)) { 166 | if (!haveSameColor(parent, hl)) { 167 | if (!hl.nextSibling && parentNext) { 168 | const newLocal: any = parentNext || parent; 169 | if (newLocal) { 170 | dom(hl).insertBefore(newLocal); 171 | again = true; 172 | } 173 | } 174 | 175 | if (!hl.previousSibling && parentPrev) { 176 | const newLocal: any = parentPrev || parent; 177 | if (newLocal) { 178 | dom(hl).insertAfter(newLocal); 179 | again = true; 180 | } 181 | } 182 | 183 | if (!parent.hasChildNodes()) { 184 | dom(parent).remove(); 185 | } 186 | } else { 187 | if (hl && hl.firstChild) parent.replaceChild(hl.firstChild, hl); 188 | highlights[i] = parent; 189 | again = true; 190 | } 191 | } 192 | } 193 | }); 194 | 195 | return again; 196 | }; 197 | 198 | do { 199 | again = flattenOnce(); 200 | } while (again); 201 | }; 202 | 203 | /** 204 | * Merges sibling highlights and normalizes descendant text nodes. 205 | * Note: this method changes input highlights - their order and number after calling this method may change. 206 | * @param highlights 207 | * @memberof TextHighlighter 208 | */ 209 | export const mergeSiblingHighlights = function (highlights: any[]) { 210 | // const self = this; 211 | 212 | const shouldMerge = (current: Node, node: Node) => { 213 | return ( 214 | node && 215 | node.nodeType === NODE_TYPE.ELEMENT_NODE && 216 | haveSameColor(current, node) && 217 | isHighlight(node as HTMLElement) 218 | ); 219 | }; 220 | // : { 221 | // previousSibling: any; 222 | // nextSibling: any; 223 | // } 224 | highlights.forEach(function (highlight: any) { 225 | const prev = highlight.previousSibling, 226 | next = highlight.nextSibling; 227 | 228 | if (shouldMerge(highlight, prev)) { 229 | dom(highlight).prepend(prev.childNodes); 230 | dom(prev).remove(); 231 | } 232 | if (shouldMerge(highlight, next)) { 233 | dom(highlight).append(next.childNodes); 234 | dom(next).remove(); 235 | } 236 | 237 | dom(highlight).normalizeTextNodes(); 238 | }); 239 | }; 240 | 241 | /** 242 | * Normalizes highlights. Ensures that highlighting is done with use of the smallest possible number of 243 | * wrapping HTML elements. 244 | * Flattens highlights structure and merges sibling highlights. Normalizes text nodes within highlights. 245 | * @param {Array} highlights - highlights to normalize. 246 | * @returns {Array} - array of normalized highlights. Order and number of returned highlights may be different than 247 | * input highlights. 248 | * @memberof TextHighlighter 249 | */ 250 | export const normalizeHighlights = function (highlights: any[]) { 251 | let normalizedHighlights; 252 | 253 | flattenNestedHighlights(highlights); 254 | mergeSiblingHighlights(highlights); 255 | 256 | // omit removed nodes 257 | normalizedHighlights = highlights.filter(function (hl: { 258 | parentElement: any; 259 | }) { 260 | return hl.parentElement ? hl : null; 261 | }); 262 | 263 | normalizedHighlights = unique(normalizedHighlights); 264 | normalizedHighlights.sort(function ( 265 | a: { offsetTop: number; offsetLeft: number }, 266 | b: { offsetTop: number; offsetLeft: number } 267 | ) { 268 | return a.offsetTop - b.offsetTop || a.offsetLeft - b.offsetLeft; 269 | }); 270 | 271 | return normalizedHighlights; 272 | }; 273 | 274 | /** 275 | * highlight selected element 276 | * @param el 277 | * @param options 278 | * @param keepRange 279 | */ 280 | const doHighlight = function ( 281 | el: HTMLElement, 282 | keepRange: boolean, 283 | options?: optionsImpl 284 | ): boolean { 285 | const range = dom(el).getRange(); 286 | let wrapper, createdHighlights, normalizedHighlights, timestamp: string; 287 | if (!options) options = new optionsImpl(); 288 | 289 | options = defaults(options, { 290 | color: "#ffff7b", 291 | highlightedClass: "highlighted", 292 | contextClass: "highlighter-context", 293 | 294 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 295 | onRemoveHighlight: function (...e: any[]) { 296 | return true; 297 | }, 298 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 299 | onBeforeHighlight: function (...e: any[]) { 300 | return true; 301 | }, 302 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 303 | onAfterHighlight: function (...e: any[]) { 304 | return true; 305 | } 306 | }); 307 | 308 | if (!range || range.collapsed) { 309 | return false; 310 | } 311 | let highlightMade = false; 312 | 313 | if (options.onBeforeHighlight && options.onBeforeHighlight(range) === true) { 314 | timestamp = (+new Date()).toString(); 315 | wrapper = createWrapper(options); 316 | wrapper.setAttribute(TIMESTAMP_ATTR, timestamp); 317 | 318 | createdHighlights = highlightRange(el, range, wrapper); 319 | if (createdHighlights.length > 0) highlightMade = true; 320 | normalizedHighlights = normalizeHighlights(createdHighlights); 321 | if (options.onAfterHighlight) 322 | options.onAfterHighlight(range, normalizedHighlights, timestamp); 323 | } 324 | 325 | if (!keepRange) { 326 | dom(el).removeAllRanges(); 327 | } 328 | return highlightMade; 329 | }; 330 | 331 | /** 332 | * Deserializes highlights. 333 | * @throws exception when can't parse JSON or JSON has invalid structure. 334 | * @param {object} json - JSON object with highlights definition. 335 | * @returns {Array} - array of deserialized highlights. 336 | * @memberof TextHighlighter 337 | */ 338 | const deserializeHighlights = function (el: HTMLElement, json: string) { 339 | let hlDescriptors: hlDescriptorI[]; 340 | const highlights: { appendChild: (arg0: any) => void }[] = []; 341 | // const self = this; 342 | 343 | if (!json) { 344 | return highlights; 345 | } 346 | 347 | try { 348 | // const r = String.raw`${json}`; 349 | hlDescriptors = JSON.parse(json); 350 | } catch (e) { 351 | throw "Can't parse JSON: " + e; 352 | } 353 | 354 | function deserializationFn(hlDescriptor: hlDescriptorI) { 355 | const hl = hlDescriptor; 356 | hl.hlpaths = hl.path.split(":").map(Number); 357 | if (!hl.hlpaths || hl.hlpaths.length == 0) return; 358 | // { 359 | // wrapper: hlDescriptor[0], 360 | // text: hlDescriptor[1], 361 | // path: hlDescriptor[2].split(":"), 362 | // offset: hlDescriptor[3], 363 | // length: hlDescriptor[4] 364 | // }; 365 | let elIndex = hl.hlpaths.pop(), 366 | node: Node | Text = el as Node, 367 | highlight, 368 | idx; 369 | 370 | // should have a value and not be 0 (false is = to 0) 371 | if (elIndex != 0 && !elIndex) return; 372 | 373 | while (hl.hlpaths.length > 0) { 374 | idx = hl.hlpaths.shift(); 375 | if (idx || idx == 0) 376 | node = node.childNodes[idx] as Node; 377 | } 378 | 379 | if ( 380 | node.childNodes[elIndex - 1] && 381 | node.childNodes[elIndex - 1].nodeType === NODE_TYPE.TEXT_NODE 382 | ) { 383 | elIndex -= 1; 384 | } 385 | 386 | node = node.childNodes[elIndex] as Text; 387 | if (node instanceof Text) { 388 | const hlNode = node.splitText(hl.offset); 389 | hlNode.splitText(hl.length); 390 | 391 | if (hlNode.nextSibling && !hlNode.nextSibling.nodeValue) { 392 | dom(hlNode.nextSibling).remove(); 393 | } 394 | 395 | if (hlNode.previousSibling && !hlNode.previousSibling.nodeValue) { 396 | dom(hlNode.previousSibling).remove(); 397 | } 398 | if (hl && hl.wrapper) { 399 | const tmpHtml = dom(hlNode).fromHTML(hl.wrapper)[0] as HTMLElement; 400 | if (tmpHtml) { 401 | highlight = dom(hlNode).wrap(tmpHtml); 402 | highlights.push(highlight); 403 | } 404 | } 405 | } 406 | } 407 | 408 | hlDescriptors.forEach(function (hlDescriptor: hlDescriptorI) { 409 | try { 410 | deserializationFn(hlDescriptor); 411 | } catch (e) { 412 | if (console && console.warn) { 413 | console.warn("Can't deserialize highlight descriptor. Cause: " + e); 414 | } 415 | } 416 | }); 417 | 418 | return highlights; 419 | }; 420 | 421 | export const find = function (el: HTMLElement, text: string, caseSensitive: boolean, options?: optionsImpl) { 422 | const wnd = dom(el).getWindow(); 423 | if (wnd) { 424 | const scrollX = wnd.scrollX, 425 | scrollY = wnd.scrollY, 426 | caseSens = (typeof caseSensitive === "undefined" ? true : caseSensitive); 427 | 428 | // dom(el).removeAllRanges(); 429 | // const test = wnd.innerh 430 | 431 | if ("find" in wnd) { 432 | while ((wnd as any).find(text, caseSens)) { 433 | doHighlight(el, true, options); 434 | } 435 | } else if ((wnd.document.body as any).createTextRange) { 436 | const textRange = (wnd.document.body as any).createTextRange(); 437 | textRange.moveToElementText(el); 438 | while (textRange.findText(text, 1, caseSens ? 4 : 0)) { 439 | if (!dom(el).contains(textRange.parentElement()) && textRange.parentElement() !== el) { 440 | break; 441 | } 442 | 443 | textRange.select(); 444 | doHighlight(el, true, options); 445 | textRange.collapse(false); 446 | } 447 | } 448 | 449 | dom(el).removeAllRanges(); 450 | wnd.scrollTo(scrollX, scrollY); 451 | } 452 | }; 453 | 454 | /** 455 | * Returns highlights from given container. 456 | * @param params 457 | * @param {HTMLElement} [params.container] - return highlights from this element. Default: the element the 458 | * highlighter is applied to. 459 | * @param {boolean} [params.andSelf] - if set to true and container is a highlight itself, add container to 460 | * returned results. Default: true. 461 | * @param {boolean} [params.grouped] - if set to true, highlights are grouped in logical groups of highlights added 462 | * in the same moment. Each group is an object which has got array of highlights, 'toString' method and 'timestamp' 463 | * property. Default: false. 464 | * @returns {Array} - array of highlights. 465 | * @memberof TextHighlighter 466 | */ 467 | export const getHighlights = function (el: HTMLElement, params?: paramsImp) { 468 | if (!params) params = new paramsImp(); 469 | params = defaults(params, { 470 | container: el, 471 | andSelf: true, 472 | grouped: false 473 | }); 474 | if (params.container) { 475 | const nodeList = params.container.querySelectorAll("[" + DATA_ATTR + "]"); 476 | let highlights = Array.prototype.slice.call(nodeList); 477 | 478 | if (params.andSelf === true && params.container.hasAttribute(DATA_ATTR)) { 479 | highlights.push(params.container); 480 | } 481 | 482 | if (params.grouped) { 483 | highlights = groupHighlights(highlights); 484 | } 485 | return highlights; 486 | } 487 | }; 488 | 489 | /** 490 | * Serializes all highlights in the element the highlighter is applied to. 491 | * @returns {string} - stringified JSON with highlights definition 492 | * @memberof TextHighlighter 493 | */ 494 | const serializeHighlights = function (el: HTMLElement | null) { 495 | if (!el) return; 496 | const highlights = getHighlights(el), 497 | refEl = el, 498 | hlDescriptors: hlDescriptorI[] = []; 499 | 500 | if (!highlights) return; 501 | 502 | function getElementPath( 503 | el: HTMLElement | ParentNode | ChildNode, 504 | refElement: any 505 | ) { 506 | const path = []; 507 | let childNodes; 508 | if (el) 509 | do { 510 | if (el instanceof HTMLElement && el.parentNode) { 511 | childNodes = Array.prototype.slice.call(el.parentNode.childNodes); 512 | path.unshift(childNodes.indexOf(el)); 513 | el = el.parentNode; 514 | } 515 | } while (el !== refElement || !el); 516 | 517 | return path; 518 | } 519 | 520 | sortByDepth(highlights, false); 521 | 522 | // { 523 | // textContent: string | any[]; 524 | // cloneNode: (arg0: boolean) => any; 525 | // previousSibling: { nodeType: number; length: number }; 526 | // } 527 | highlights.forEach(function (highlight: HTMLElement) { 528 | if (highlight && highlight.textContent) { 529 | let offset = 0, // Hl offset from previous sibling within parent node. 530 | wrapper = highlight.cloneNode(true) as HTMLElement | string; 531 | const length = highlight.textContent.length, 532 | hlPath = getElementPath(highlight, refEl); 533 | let color = ""; 534 | if (wrapper instanceof HTMLElement) { 535 | const c = wrapper.getAttribute("data-backgroundcolor"); 536 | if (c) color = c.trim(); 537 | wrapper.innerHTML = ""; 538 | wrapper = wrapper.outerHTML; 539 | } 540 | 541 | if ( 542 | highlight.previousSibling && 543 | highlight.previousSibling.nodeType === NODE_TYPE.TEXT_NODE && 544 | highlight.previousSibling instanceof Text 545 | ) { 546 | offset = highlight.previousSibling.length; 547 | } 548 | const hl: hlDescriptorI = { 549 | wrapper, 550 | textContent: highlight.textContent, 551 | path: hlPath.join(":"), 552 | color, 553 | offset, 554 | length 555 | }; 556 | 557 | hlDescriptors.push(hl); 558 | } 559 | }); 560 | return JSON.stringify(hlDescriptors); 561 | }; 562 | 563 | const removeHighlights = function (element: HTMLElement, options?: optionsImpl) { 564 | const container = element, 565 | highlights = getHighlights(element, { container: container }); 566 | // self = this; 567 | if (!highlights) return; 568 | 569 | if (!options) options = new optionsImpl(); 570 | 571 | options = defaults(options, { 572 | color: "#ffff7b", 573 | highlightedClass: "highlighted", 574 | contextClass: "highlighter-context", 575 | 576 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 577 | onRemoveHighlight: function (...e: any[]): boolean { 578 | return true; 579 | }, 580 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 581 | onBeforeHighlight: function (...e: any[]) { 582 | return true; 583 | }, 584 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 585 | onAfterHighlight: function (...e: any[]) { 586 | return true; 587 | } 588 | }); 589 | 590 | function mergeSiblingTextNodes(textNode: { 591 | previousSibling: any; 592 | nextSibling: any; 593 | nodeValue: any; 594 | }) { 595 | const prev = textNode.previousSibling, 596 | next = textNode.nextSibling; 597 | 598 | if (prev && prev.nodeType === NODE_TYPE.TEXT_NODE) { 599 | textNode.nodeValue = prev.nodeValue + textNode.nodeValue; 600 | dom(prev).remove(); 601 | } 602 | if (next && next.nodeType === NODE_TYPE.TEXT_NODE) { 603 | textNode.nodeValue = textNode.nodeValue + next.nodeValue; 604 | dom(next).remove(); 605 | } 606 | } 607 | function removeHighlight(highlight: any) { 608 | if (!highlight) return; 609 | const textNodes = dom(highlight).unwrap(); 610 | if (textNodes) 611 | textNodes.forEach(function (node) { 612 | mergeSiblingTextNodes(node); 613 | }); 614 | } 615 | 616 | sortByDepth(highlights, true); 617 | 618 | highlights.forEach((hl: any) => { 619 | if ( 620 | options && 621 | options.onRemoveHighlight && 622 | options.onRemoveHighlight(hl) === true 623 | ) { 624 | removeHighlight(hl); 625 | } 626 | }); 627 | }; 628 | 629 | export { 630 | doHighlight, 631 | deserializeHighlights, 632 | serializeHighlights, 633 | removeHighlights, 634 | createWrapper, 635 | highlightRange 636 | }; 637 | -------------------------------------------------------------------------------- /src/TextHighlighter.ts: -------------------------------------------------------------------------------- 1 | import { createWrapper, deserializeHighlights, doHighlight, find, flattenNestedHighlights, getHighlights, highlightRange, mergeSiblingHighlights, normalizeHighlights, removeHighlights, serializeHighlights } from "./Library"; 2 | import { optionsImpl, paramsImp, TextHighlighterSelf, TextHighlighterType } from "./types"; 3 | import { bindEvents, DATA_ATTR, defaults, dom, NODE_TYPE, unbindEvents } from "./Utils"; 4 | 5 | const TextHighlighter: TextHighlighterType = function (this: TextHighlighterSelf, element: HTMLElement, options?: optionsImpl) { 6 | 7 | if (!element) { 8 | throw "Missing anchor element"; 9 | } 10 | 11 | this.el = element; 12 | this.options = defaults(options, { 13 | color: "#ffff7b", 14 | highlightedClass: "highlighted", 15 | contextClass: "highlighter-context", 16 | onRemoveHighlight: function () { return true; }, 17 | onBeforeHighlight: function () { return true; }, 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | onAfterHighlight: function (...e: any[]) { 20 | return true; 21 | } 22 | }); 23 | if (this.options && this.options.contextClass) 24 | dom(this.el).addClass(this.options.contextClass); 25 | bindEvents(this.el, this); 26 | return this; 27 | }; 28 | 29 | /** 30 | * Permanently disables highlighting. 31 | * Unbinds events and remove context element class. 32 | * @memberof TextHighlighter 33 | */ 34 | TextHighlighter.prototype.destroy = function () { 35 | unbindEvents(this.el, this); 36 | dom(this.el).removeClass(this.options.contextClass); 37 | }; 38 | 39 | TextHighlighter.prototype.highlightHandler = function () { 40 | this.doHighlight(); 41 | }; 42 | 43 | TextHighlighter.prototype.doHighlight = function (keepRange: boolean) { 44 | doHighlight(this.el, keepRange, this.options); 45 | }; 46 | 47 | TextHighlighter.prototype.highlightRange = function (range: Range, wrapper: { cloneNode: (arg0: boolean) => any }) { 48 | highlightRange(this.el, range, wrapper); 49 | }; 50 | 51 | TextHighlighter.prototype.normalizeHighlights = function (highlights: any[]) { 52 | normalizeHighlights(highlights); 53 | }; 54 | 55 | TextHighlighter.prototype.flattenNestedHighlights = function (highlights: any[]) { 56 | flattenNestedHighlights(highlights); 57 | }; 58 | 59 | TextHighlighter.prototype.mergeSiblingHighlights = function (highlights: any[]) { 60 | mergeSiblingHighlights(highlights); 61 | }; 62 | 63 | TextHighlighter.prototype.setColor = function (color: string) { 64 | this.options.color = color; 65 | }; 66 | 67 | TextHighlighter.prototype.getColor = function () { 68 | return this.options.color; 69 | }; 70 | 71 | TextHighlighter.prototype.removeHighlights = function (element: HTMLElement) { 72 | const container = element || this.el; 73 | removeHighlights(container, this.options); 74 | }; 75 | 76 | TextHighlighter.prototype.getHighlights = function (params?: paramsImp) { 77 | getHighlights(this.el, params); 78 | }; 79 | 80 | TextHighlighter.prototype.isHighlight = function (el: HTMLElement) { 81 | return el && el.nodeType === NODE_TYPE.ELEMENT_NODE && el.hasAttribute(DATA_ATTR); 82 | }; 83 | 84 | TextHighlighter.prototype.serializeHighlights = function () { 85 | return serializeHighlights(this.el); 86 | }; 87 | 88 | TextHighlighter.prototype.deserializeHighlights = function (json: string) { 89 | deserializeHighlights(this.el, json); 90 | }; 91 | 92 | TextHighlighter.prototype.find = function (text: string, caseSensitive: boolean) { 93 | find(this.el, text, caseSensitive); 94 | }; 95 | 96 | (TextHighlighter as any).createWrapper = function (options: optionsImpl) { 97 | createWrapper(options); 98 | }; 99 | 100 | export { TextHighlighter }; 101 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | // highlight extensions 2 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 3 | // type H_HTMLElement = HTMLElement; 4 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 5 | // interface ie_HTMLElement extends HTMLElement { 6 | // createTextRange(): TextRange; 7 | // } 8 | 9 | // eslint-disable-next-line @typescript-eslint/class-name-casing 10 | // interface H_Node extends Node { 11 | // splitText(endOffset: number): any; 12 | // } 13 | // eslint-disable-next-line @typescript-eslint/class-name-casing,@typescript-eslint/camelcase 14 | // interface H_Window extends Window { 15 | // find(text: any, caseSens: any): boolean; 16 | // } 17 | 18 | const /** 19 | * Attribute added by default to every highlight. 20 | * @type {string} 21 | */ 22 | DATA_ATTR = "data-highlighted", 23 | /** 24 | * Attribute used to group highlight wrappers. 25 | * @type {string} 26 | */ 27 | TIMESTAMP_ATTR = "data-timestamp", 28 | NODE_TYPE = { 29 | ELEMENT_NODE: 1, 30 | TEXT_NODE: 3 31 | }, 32 | /** 33 | * Don't highlight content of these tags. 34 | * @type {string[]} 35 | */ 36 | IGNORE_TAGS = [ 37 | "SCRIPT", 38 | "STYLE", 39 | "SELECT", 40 | "OPTION", 41 | "BUTTON", 42 | "OBJECT", 43 | "APPLET", 44 | "VIDEO", 45 | "AUDIO", 46 | "CANVAS", 47 | "EMBED", 48 | "PARAM", 49 | "METER", 50 | "PROGRESS" 51 | ]; 52 | 53 | function activator(type: { new(): T }): T { 54 | return new type(); 55 | } 56 | 57 | /** 58 | * Groups given highlights by timestamp. 59 | * @param {Array} highlights 60 | * @returns {Array} Grouped highlights. 61 | */ 62 | function groupHighlights(highlights: any) { 63 | const order: any = [], 64 | chunks: any = {}, 65 | grouped: any = []; 66 | 67 | highlights.forEach(function (hl: any) { 68 | const timestamp = hl.getAttribute(TIMESTAMP_ATTR); 69 | 70 | if (typeof chunks[timestamp] === "undefined") { 71 | chunks[timestamp] = []; 72 | order.push(timestamp); 73 | } 74 | 75 | chunks[timestamp].push(hl); 76 | }); 77 | 78 | order.forEach(function (timestamp: any) { 79 | const group = chunks[timestamp]; 80 | 81 | grouped.push({ 82 | chunks: group, 83 | timestamp: timestamp, 84 | toString: function () { 85 | return group 86 | .map(function (h: { textContent: any }) { 87 | return h.textContent; 88 | }) 89 | .join(""); 90 | } 91 | }); 92 | }); 93 | 94 | return grouped; 95 | } 96 | 97 | /** 98 | * Fills undefined values in obj with default properties with the same name from source object. 99 | * @param {object} obj - target object, can't be null, must be initialized first 100 | * @param {object} source - source object with default values 101 | * @returns {object} 102 | */ 103 | function defaults(obj: T, source: T): T { 104 | if (obj == null) 105 | obj = {} as T; 106 | for (const prop in source) { 107 | if ( 108 | Object.prototype.hasOwnProperty.call(source, prop) && 109 | obj[prop] === void 0 110 | ) { 111 | obj[prop] = source[prop]; 112 | } 113 | } 114 | 115 | return obj; 116 | } 117 | 118 | /** 119 | * Returns array without duplicated values. 120 | * @param {Array} arr 121 | * @returns {Array} 122 | */ 123 | function unique(arr: any) { 124 | return arr.filter(function (value: any, idx: any, self: string | any[]) { 125 | return self.indexOf(value) === idx; 126 | }); 127 | } 128 | 129 | /** 130 | * Takes range object as parameter and refines it boundaries 131 | * @param range 132 | * @returns {object} refined boundaries and initial state of highlighting algorithm. 133 | */ 134 | function refineRangeBoundaries(range: Range) { 135 | let startContainer: 136 | | Node 137 | | (Node & ParentNode) 138 | | null = range.startContainer as HTMLElement, 139 | endContainer: Node | (Node & ParentNode) | null = range.endContainer, 140 | goDeeper = true; 141 | const ancestor = range.commonAncestorContainer; 142 | 143 | if (range.endOffset === 0) { 144 | while ( 145 | endContainer && 146 | !endContainer.previousSibling && 147 | endContainer.parentNode !== ancestor 148 | ) { 149 | endContainer = endContainer.parentNode; 150 | } 151 | if (endContainer) endContainer = endContainer.previousSibling; 152 | } else if (endContainer.nodeType === NODE_TYPE.TEXT_NODE) { 153 | if ( 154 | endContainer && 155 | endContainer.nodeValue && 156 | range.endOffset < endContainer.nodeValue.length 157 | ) { 158 | const t = endContainer as Text; 159 | t.splitText(range.endOffset); 160 | } 161 | } else if (range.endOffset > 0) { 162 | endContainer = endContainer.childNodes.item(range.endOffset - 1); 163 | } 164 | 165 | if (startContainer.nodeType === NODE_TYPE.TEXT_NODE) { 166 | if ( 167 | startContainer && 168 | startContainer.nodeValue && 169 | range.startOffset === startContainer.nodeValue.length 170 | ) { 171 | goDeeper = false; 172 | } else if (startContainer instanceof Node && range.startOffset > 0) { 173 | const t = startContainer as Text; 174 | startContainer = t.splitText(range.startOffset); 175 | if (startContainer && endContainer === startContainer.previousSibling) { 176 | endContainer = startContainer; 177 | } 178 | } 179 | } else if (range.startOffset < startContainer.childNodes.length) { 180 | startContainer = startContainer.childNodes.item(range.startOffset); 181 | } else { 182 | startContainer = startContainer.nextSibling; 183 | } 184 | 185 | return { 186 | startContainer: startContainer, 187 | endContainer: endContainer, 188 | goDeeper: goDeeper 189 | }; 190 | } 191 | 192 | export function bindEvents(el: HTMLElement, scope: any) { 193 | el.addEventListener("mouseup", scope.highlightHandler.bind(scope)); 194 | el.addEventListener("touchend", scope.highlightHandler.bind(scope)); 195 | } 196 | 197 | export function unbindEvents(el: HTMLElement, scope: any) { 198 | el.removeEventListener("mouseup", scope.highlightHandler.bind(scope)); 199 | el.removeEventListener("touchend", scope.highlightHandler.bind(scope)); 200 | } 201 | 202 | /** 203 | * Utility functions to make DOM manipulation easier. 204 | * @param {Node|HTMLElement} [el] - base DOM element to manipulate 205 | * @returns {object} 206 | */ 207 | const dom = function (el: Node | HTMLElement | null | undefined) { 208 | return /** @lends dom **/ { 209 | /** 210 | * Adds class to element. 211 | * @param {string} className 212 | */ 213 | addClass: function (className: string) { 214 | if (el instanceof HTMLElement) 215 | if (el.classList) { 216 | el.classList.add(className); 217 | } else { 218 | el.className += " " + className; 219 | } 220 | }, 221 | 222 | /** 223 | * Removes class from element. 224 | * @param {string} className 225 | */ 226 | removeClass: function (className: string) { 227 | if (el instanceof HTMLElement) { 228 | if (el.classList) { 229 | el.classList.remove(className); 230 | } else { 231 | el.className = el.className.replace( 232 | new RegExp("(^|\\b)" + className + "(\\b|$)", "gi"), 233 | " " 234 | ); 235 | } 236 | } 237 | }, 238 | 239 | /** 240 | * Prepends child nodes to base element. 241 | * @param {Node[]} nodesToPrepend 242 | */ 243 | prepend: function (nodesToPrepend: any) { 244 | const nodes = Array.prototype.slice.call(nodesToPrepend); 245 | let i = nodes.length; 246 | 247 | if (el) 248 | while (i--) { 249 | el.insertBefore(nodes[i], el.firstChild); 250 | } 251 | }, 252 | 253 | /** 254 | * Appends child nodes to base element. 255 | * @param {Node[]} nodesToAppend 256 | */ 257 | append: function (nodesToAppend: any) { 258 | if (el) { 259 | const nodes = Array.prototype.slice.call(nodesToAppend); 260 | 261 | for (let i = 0, len = nodes.length; i < len; ++i) { 262 | el.appendChild(nodes[i]); 263 | } 264 | } 265 | }, 266 | 267 | /** 268 | * Inserts base element after refEl. 269 | * @param {Node} refEl - node after which base element will be inserted 270 | * @returns {Node} - inserted element 271 | */ 272 | insertAfter: function (refEl: { 273 | parentNode: { insertBefore: (arg0: any, arg1: any) => any }; 274 | nextSibling: any; 275 | }) { 276 | return refEl.parentNode.insertBefore(el, refEl.nextSibling); 277 | }, 278 | 279 | /** 280 | * Inserts base element before refEl. 281 | * @param {Node} refEl - node before which base element will be inserted 282 | * @returns {Node} - inserted element 283 | */ 284 | insertBefore: function (refEl: { 285 | parentNode: { insertBefore: (arg0: any, arg1: any) => any }; 286 | }) { 287 | return refEl.parentNode 288 | ? refEl.parentNode.insertBefore(el, refEl) 289 | : refEl; 290 | }, 291 | 292 | /** 293 | * Removes base element from DOM. 294 | */ 295 | remove: function () { 296 | if (el && el.parentNode) { 297 | el.parentNode.removeChild(el); 298 | el = null; 299 | } 300 | }, 301 | 302 | /** 303 | * Returns true if base element contains given child. 304 | * @param {Node|HTMLElement} child 305 | * @returns {boolean} 306 | */ 307 | contains: function (child: any) { 308 | return el && el !== child && el.contains(child); 309 | }, 310 | 311 | /** 312 | * Wraps base element in wrapper element. 313 | * @param {HTMLElement} wrapper 314 | * @returns {HTMLElement} wrapper element 315 | */ 316 | wrap: function (wrapper: HTMLElement) { 317 | if (el) { 318 | if (el.parentNode) { 319 | el.parentNode.insertBefore(wrapper, el); 320 | } 321 | 322 | wrapper.appendChild(el); 323 | } 324 | return wrapper; 325 | }, 326 | 327 | /** 328 | * Unwraps base element. 329 | * @returns {Node[]} - child nodes of unwrapped element. 330 | */ 331 | unwrap: function () { 332 | if (el) { 333 | const nodes = Array.prototype.slice.call(el.childNodes); 334 | let wrapper; 335 | // debugger; 336 | nodes.forEach(function (node) { 337 | wrapper = node.parentNode; 338 | const d = dom(node); 339 | if (d && node.parentNode) d.insertBefore(node.parentNode); 340 | dom(wrapper).remove(); 341 | }); 342 | 343 | return nodes; 344 | } 345 | }, 346 | 347 | /** 348 | * Returns array of base element parents. 349 | * @returns {HTMLElement[]} 350 | */ 351 | parents: function () { 352 | let parent; 353 | const path = []; 354 | if (el) { 355 | while ((parent = el.parentNode)) { 356 | path.push(parent); 357 | el = parent; 358 | } 359 | } 360 | 361 | return path; 362 | }, 363 | 364 | /** 365 | * Normalizes text nodes within base element, ie. merges sibling text nodes and assures that every 366 | * element node has only one text node. 367 | * It should does the same as standard element.normalize, but IE implements it incorrectly. 368 | */ 369 | normalizeTextNodes: function () { 370 | if (!el) { 371 | return; 372 | } 373 | 374 | if ( 375 | el.nodeType === NODE_TYPE.TEXT_NODE && 376 | el.nodeValue && 377 | el.parentNode 378 | ) { 379 | while ( 380 | el.nextSibling && 381 | el.nextSibling.nodeType === NODE_TYPE.TEXT_NODE 382 | ) { 383 | el.nodeValue += el.nextSibling.nodeValue; 384 | el.parentNode.removeChild(el.nextSibling); 385 | } 386 | } else { 387 | dom(el.firstChild).normalizeTextNodes(); 388 | } 389 | dom(el.nextSibling).normalizeTextNodes(); 390 | }, 391 | 392 | /** 393 | * Returns element background color. 394 | * @returns {CSSStyleDeclaration.backgroundColor} 395 | */ 396 | color: function () { 397 | return el instanceof HTMLElement && el.style 398 | ? el.style.backgroundColor 399 | : null; 400 | }, 401 | 402 | /** 403 | * Creates dom element from given html string. 404 | * @param {string} html 405 | * @returns {NodeList} 406 | */ 407 | fromHTML: function (html: string) { 408 | const div = document.createElement("div"); 409 | div.innerHTML = html; 410 | return div.childNodes; 411 | }, 412 | 413 | /** 414 | * Returns first range of the window of base element. 415 | * @returns {Range} 416 | */ 417 | getRange: function () { 418 | const selection = dom(el).getSelection(); 419 | let range; 420 | 421 | if (selection && selection.rangeCount > 0) { 422 | range = selection.getRangeAt(0); 423 | } 424 | 425 | return range; 426 | }, 427 | 428 | /** 429 | * Removes all ranges of the window of base element. 430 | */ 431 | removeAllRanges: function () { 432 | const selection = dom(el).getSelection(); 433 | if (selection) selection.removeAllRanges(); 434 | }, 435 | 436 | /** 437 | * Returns selection object of the window of base element. 438 | * @returns {Selection} 439 | */ 440 | getSelection: function () { 441 | const win = dom(el).getWindow(); 442 | return win ? win.getSelection() : null; 443 | }, 444 | 445 | /** 446 | * Returns window of the base element. 447 | * @returns {Window} 448 | */ 449 | getWindow: function () { 450 | const doc = dom(el).getDocument() as Document; 451 | return doc instanceof Document ? doc.defaultView : null; 452 | }, 453 | 454 | /** 455 | * Returns document of the base element. 456 | * @returns {HTMLDocument} 457 | */ 458 | getDocument: function () { 459 | // if ownerDocument is null then el is the document itself. 460 | if (el) return el.ownerDocument || el; 461 | } 462 | }; 463 | }; 464 | 465 | /** 466 | * Returns true if elements a i b have the same color. 467 | * @param {Node} a 468 | * @param {Node} b 469 | * @returns {boolean} 470 | */ 471 | function haveSameColor(a: Node, b: Node) { 472 | return dom(a).color() === dom(b).color(); 473 | } 474 | 475 | /** 476 | * Sorts array of DOM elements by its depth in DOM tree. 477 | * @param {HTMLElement[]} arr - array to sort. 478 | * @param {boolean} descending - order of sort. 479 | */ 480 | function sortByDepth(arr: any, descending: any) { 481 | arr.sort(function (a: any, b: any) { 482 | return ( 483 | dom(descending ? b : a).parents().length - 484 | dom(descending ? a : b).parents().length 485 | ); 486 | }); 487 | } 488 | 489 | export { 490 | DATA_ATTR, 491 | TIMESTAMP_ATTR, 492 | NODE_TYPE, 493 | IGNORE_TAGS, 494 | dom, 495 | refineRangeBoundaries, 496 | sortByDepth, 497 | unique, 498 | haveSameColor, 499 | // eslint-disable-n 500 | defaults, 501 | groupHighlights, 502 | activator 503 | }; 504 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, createWrapper, highlightRange } from "../src/Library"; 2 | import { TextHighlighter } from "./TextHighlighter"; 3 | import { optionsImpl } from "./types"; 4 | 5 | export { 6 | doHighlight, 7 | deserializeHighlights, 8 | serializeHighlights, 9 | removeHighlights, 10 | optionsImpl, 11 | createWrapper, 12 | highlightRange, 13 | TextHighlighter 14 | }; -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface TextHighlighterI { 2 | doHighlight: any; 3 | deserializeHighlights: any; 4 | serializeHighlights: any; 5 | removeHighlights: any; 6 | optionsImpl: any; 7 | } 8 | 9 | export interface TextHighlighterSelf { 10 | el?: HTMLElement; 11 | options?: optionsImpl; 12 | } 13 | 14 | export type TextHighlighterType = (element: HTMLElement, options: optionsImpl) => void; 15 | 16 | export interface TextRange { 17 | collapse(arg0: boolean): any; 18 | select(): void; 19 | parentElement(): any; 20 | findText(text: any, arg1: number, arg2: number): any; 21 | moveToElementText(el: any): any; 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/class-name-casing 25 | export class highlightI { 26 | highlightHandler: any; 27 | options: optionsImpl | undefined; 28 | el: HTMLElement | undefined; 29 | } 30 | // eslint-disable-next-line @typescript-eslint/class-name-casing 31 | export interface optionsI { 32 | color?: string; 33 | highlightedClass?: string; 34 | contextClass?: string; 35 | onRemoveHighlight?: { (...e: any[]): boolean }; 36 | onBeforeHighlight?: { (...e: any[]): boolean }; 37 | onAfterHighlight?: { (...e: any[]): boolean }; 38 | } 39 | // eslint-disable-next-line @typescript-eslint/class-name-casing 40 | export class optionsImpl implements optionsI { 41 | color?: string | undefined; 42 | highlightedClass?: string | undefined; 43 | contextClass?: string | undefined; 44 | onRemoveHighlight?: { (...e: any[]): boolean }; 45 | onBeforeHighlight?: { (...e: any[]): boolean }; 46 | onAfterHighlight?: { (...e: any[]): boolean }; 47 | // constructor() {} 48 | } 49 | 50 | // class containerI{ 51 | // querySelectorAll: (arg0: string): any; 52 | // hasAttribute: (arg0: string) => any; 53 | // } 54 | // eslint-disable-next-line @typescript-eslint/class-name-casing 55 | export class paramsImp { 56 | container?: HTMLElement; 57 | andSelf?: boolean; 58 | grouped?: any; 59 | } 60 | 61 | // eslint-disable-next-line @typescript-eslint/class-name-casing 62 | export interface hlDescriptorI { 63 | wrapper: string; 64 | textContent: string; 65 | color: string; 66 | hlpaths?: number[]; 67 | path: string; 68 | offset: number; 69 | length: number; 70 | } -------------------------------------------------------------------------------- /tests/data/results/doHighlight.json: -------------------------------------------------------------------------------- 1 | { 2 | "emptyHighlights": "[]" 3 | } -------------------------------------------------------------------------------- /tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights } from "../src/index"; 5 | import { getExpected, updateExpected } from "./utils/helper"; 6 | const dataSource = "doHighlight"; 7 | describe("doHighlight", () => { 8 | test("mock test", async () => { 9 | expect(true).toBe(true);// Set up our document body 10 | document.body.innerHTML = 11 | "
" + 12 | `

Lorem ipsum

13 |

14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus malesuada sagittis. Morbi 15 | purus odio, blandit ac urna sed, interdum pharetra leo. Cras congue id est sit amet mattis. 16 | Sed in metus et orci eleifend commodo. Phasellus at odio imperdiet, efficitur augue in, pulvinar 17 | sapien. Pellentesque leo nulla, porta non lectus eu, ullamcorper semper est. Nunc convallis 18 | risus vel mauris accumsan, in rutrum odio sodales. Vestibulum ante ipsum primis in faucibus 19 | orci luctus et ultrices posuere cubilia Curae; Sed at tempus mauris. Fusce blandit felis sit amet 20 | magna lacinia blandit. 21 |

22 | dummy image 23 |

24 | Maecenas faucibus hendrerit lectus, in auctor felis tristique at. Pellentesque a felis ut nibh 25 | malesuada auctor. Ut egestas elit ac ultrices ullamcorper. Pellentesque enim est, varius 26 | ultrices velit eget, consectetur aliquam tortor. Aliquam sit amet nibh id tellus sollicitudin 27 | faucibus. Nunc euismod augue tempus, ornare justo interdum, consectetur lacus. Pellentesque a 28 | molestie tellus, eget convallis lectus. 29 |

30 |

31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae nunc sed risus blandit convallis 32 | id id risus. Morbi tortor metus, imperdiet sed ipsum quis, condimentum mattis tellus. Fusce orci nisi, 33 | ultricies vel hendrerit id, egestas id turpis. Proin cursus diam tortor, sed ullamcorper eros commodo 34 | vitae. Aenean et maximus sapien. Nam felis velit, ullamcorper eu turpis ut, hendrerit accumsan augue. 35 | Nulla et purus sem. Ut at hendrerit purus. Phasellus mollis commodo ante eu mollis. In nec 36 | dui vel mauris lacinia vulputate id nec turpis. Aliquam vestibulum, elit sit amet fringilla 37 | malesuada, quam nunc eleifend nunc, id iaculis est neque pretium libero. 38 |

` + 39 | "
"; 40 | 41 | const domEle: HTMLElement | null = document.getElementById("sandbox"); 42 | // const d = deserializeHighlights(domEle, tmp); 43 | const result = serializeHighlights(domEle); 44 | 45 | const dataKey = "emptyHighlights"; 46 | 47 | const expectedResult = await getExpected(dataSource,dataKey); 48 | if(result != expectedResult) { 49 | await updateExpected(dataSource, dataKey, result); 50 | } 51 | 52 | // console.log(result); 53 | expect(result).toBe(expectedResult); 54 | // expect(Greeter("Carl")).toBe("Hello Carl"); 55 | 56 | }); 57 | }); -------------------------------------------------------------------------------- /tests/utils/helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // import {fs} from "fs"; 3 | import * as fs from "fs"; 4 | // const fs = require("fs"); 5 | const path = require("path"); 6 | const base_DIR = path.join(__dirname, "../data/results"); 7 | 8 | export async function getExpectedDict(testData: string): Promise> { 9 | const filePath = path.join(base_DIR, testData + ".json"); 10 | console.log(filePath); 11 | if (await fs.existsSync(filePath)) { 12 | const expectedResults: Record = await JSON.parse(fs.readFileSync(filePath, "utf8")); 13 | return expectedResults; 14 | } else 15 | return {}; 16 | } 17 | 18 | export async function getExpected(testData: string, key: string): Promise { 19 | const expectedResults = await getExpectedDict(testData); 20 | if (expectedResults[key]) { 21 | return expectedResults[key]; 22 | } else { 23 | return null; 24 | } 25 | } 26 | 27 | export async function updateExpected(testData: string, key: string, result: T | null) { 28 | const expectedResults = await getExpectedDict(testData); 29 | 30 | expectedResults[key] = result; 31 | 32 | const filePath = path.join(base_DIR, testData + ".json"); 33 | const stringResults = JSON.stringify(expectedResults, null, 4); 34 | await fs.writeFileSync(filePath, stringResults); 35 | } -------------------------------------------------------------------------------- /tests/utils/types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funktechno/texthighlighter/7dae66399da0e76f5a21f4a0e9afd6fedf292a92/tests/utils/types.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJS", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "importHelpers": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "outDir": "./lib", 11 | "lib": [ 12 | "es2018", 13 | "dom" 14 | ], 15 | "strict": true, 16 | "types": [ 17 | "jest", 18 | "node" 19 | ] 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "**/tests/*" 27 | ] 28 | } --------------------------------------------------------------------------------