├── .editorconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── get-selection-more.png └── get-selection-more.svg ├── karma.conf.js ├── package.json ├── rollup.config.js ├── src └── get-selection-more.ts ├── test ├── get-paragraph.spec.tsx ├── get-sentence.spec.tsx ├── get-text.spec.tsx ├── index.ts ├── tsconfig.json └── typings │ └── global │ └── index.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.6.0 4 | services: 5 | - xvfb 6 | addons: 7 | firefox: latest 8 | chrome: stable 9 | script: 10 | - yarn build 11 | - yarn test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 CRIMX 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get Selection More [![npm-version](https://img.shields.io/npm/v/get-selection-more.svg)](https://www.npmjs.com/package/get-selection-more) [![Build Status](https://img.shields.io/travis/com/crimx/get-selection-more/master)](https://travis-ci.com/crimx/get-selection-more) [![Coverage Status](https://img.shields.io/coveralls/github/crimx/get-selection-more/master)](https://coveralls.io/github/crimx/get-selection-more?branch=master) 2 | 3 | ![Get Selection More](./assets/get-selection-more.png) 4 | 5 | ## APIs 6 | 7 | ```typescript 8 | /** 9 | * Returns the selected text 10 | */ 11 | function getText(win?: Window): string 12 | function getTextFromSelection(selection: Selection | null, win?: Window): string 13 | /** 14 | * Returns the paragraph containing the selection text. 15 | */ 16 | function getParagraph(win?: Window): string 17 | function getParagraphFromSelection(selection: Selection | null): string 18 | /** 19 | * Returns the sentence containing the selection text. 20 | */ 21 | function getSentence(win?: Window): string 22 | function getSentenceFromSelection(selection: Selection | null): string 23 | ``` 24 | 25 | Optionally pass `window` of other frame to get selection within that frame. 26 | 27 | ## Usage 28 | 29 | ```javascript 30 | import { getText, getParagraph, getSentence } from 'get-selection-more' 31 | 32 | document.addEventListener('selectionchange', () => { 33 | console.log(getText(), getParagraph(), getSentence()) 34 | }) 35 | ``` 36 | 37 | Or load the UMD module directly which exposes `getSelectionMore` global. 38 | -------------------------------------------------------------------------------- /assets/get-selection-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crimx/get-selection-more/4dadf90da0c190b6857a4e4fff9f872bc32032c5/assets/get-selection-more.png -------------------------------------------------------------------------------- /assets/get-selection-more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Get Selection More 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = config => { 4 | config.set({ 5 | files: ['test/index.ts'], 6 | 7 | singleRun: !!process.env.CI, 8 | 9 | frameworks: ['mocha', 'chai'], 10 | 11 | preprocessors: { 12 | 'test/index.ts': ['webpack'] 13 | }, 14 | 15 | mime: { 16 | 'text/x-typescript': ['ts', 'tsx'] 17 | }, 18 | 19 | browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'], 20 | 21 | reporters: process.env.CI 22 | ? ['nyan', 'coverage-istanbul', 'coveralls'] 23 | : ['nyan', 'coverage-istanbul'], 24 | 25 | webpackMiddleware: { 26 | noInfo: true, 27 | stats: 'errors-only' 28 | }, 29 | webpack: { 30 | mode: 'development', 31 | entry: './src/get-selection-more.ts', 32 | output: { 33 | filename: '[name].js' 34 | }, 35 | devtool: 'inline-source-map', 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.tsx?$/, 40 | use: { 41 | loader: 'ts-loader', 42 | options: { 43 | configFile: 'test/tsconfig.json' 44 | } 45 | }, 46 | exclude: [path.join(__dirname, 'node_modules')] 47 | }, 48 | { 49 | test: /\.tsx?$/, 50 | include: [path.join(__dirname, 'src')], 51 | enforce: 'post', 52 | use: { 53 | loader: 'istanbul-instrumenter-loader', 54 | options: { esModules: true } 55 | } 56 | } 57 | ] 58 | }, 59 | resolve: { 60 | extensions: ['.tsx', '.ts', '.js', '.json'] 61 | } 62 | }, 63 | 64 | coverageIstanbulReporter: process.env.CI 65 | ? { 66 | reports: ['lcovonly', 'text-summary'], 67 | dir: path.join(__dirname, 'coverage'), 68 | combineBrowserReports: true, 69 | fixWebpackSourcePaths: true 70 | } 71 | : { 72 | reports: ['html', 'lcovonly', 'text-summary'], 73 | dir: path.join(__dirname, 'coverage/%browser%/'), 74 | fixWebpackSourcePaths: true, 75 | 'report-config': { 76 | html: { outdir: 'html' } 77 | } 78 | }, 79 | 80 | coverageReporter: { 81 | type: 'lcovonly', 82 | dir: 'coverage/' 83 | }, 84 | 85 | nyanReporter: { 86 | renderOnRunCompleteOnly: process.env.CI 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-selection-more", 3 | "version": "1.1.0", 4 | "description": "Get text and context sentence from window.getSelection()", 5 | "keywords": [ 6 | "getSelection", 7 | "selection", 8 | "context", 9 | "sentence", 10 | "text" 11 | ], 12 | "scripts": { 13 | "prebuild": "rimraf dist", 14 | "build": "tsc && rollup -c rollup.config.js", 15 | "start": "rollup -c rollup.config.js -w", 16 | "test": "karma start" 17 | }, 18 | "main": "dist/get-selection-more.umd.js", 19 | "module": "dist/get-selection-more.es.js", 20 | "typings": "dist/types/get-selection-more.d.ts", 21 | "files": [ 22 | "dist" 23 | ], 24 | "repository": "https://github.com/crimx/get-selection-more", 25 | "author": "CRIMX ", 26 | "license": "MIT", 27 | "engines": { 28 | "node": ">=10.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "^4.1.7", 32 | "@types/mocha": "^5.2.7", 33 | "@types/webpack-env": "^1.13.9", 34 | "chai": "^4.2.0", 35 | "istanbul-instrumenter-loader": "^3.0.1", 36 | "karma": "^4.1.0", 37 | "karma-chai": "^0.1.0", 38 | "karma-chrome-launcher": "^2.2.0", 39 | "karma-coverage-istanbul-reporter": "^2.0.5", 40 | "karma-coveralls": "^2.1.0", 41 | "karma-firefox-launcher": "^1.1.0", 42 | "karma-mocha": "^1.3.0", 43 | "karma-nyan-reporter": "^0.2.5", 44 | "karma-rollup-preprocessor": "^7.0.0", 45 | "karma-webpack": "^4.0.2", 46 | "lodash.camelcase": "^4.3.0", 47 | "mocha": "^6.1.4", 48 | "rimraf": "^2.6.3", 49 | "rollup": "^1.15.2", 50 | "rollup-plugin-sourcemaps": "^0.4.2", 51 | "rollup-plugin-terser": "^5.0.0", 52 | "rollup-plugin-typescript2": "^0.21.1", 53 | "ts-loader": "^6.0.2", 54 | "tslint": "^5.17.0", 55 | "tslint-config-prettier": "^1.18.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "tsx-dom": "^0.8.3", 58 | "typescript": "^3.5.1", 59 | "webpack": "^4.34.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const sourceMaps = require('rollup-plugin-sourcemaps') 2 | const typescript = require('rollup-plugin-typescript2') 3 | const camelCase = require('lodash.camelcase') 4 | const { terser } = require('rollup-plugin-terser') 5 | 6 | const pkg = require('./package.json') 7 | 8 | const entryName = 'get-selection-more' 9 | 10 | function baseConfig() { 11 | return { 12 | input: `src/${entryName}.ts`, 13 | output: [], 14 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 15 | external: [], 16 | watch: { 17 | include: 'src/**' 18 | }, 19 | plugins: [ 20 | // Resolve source maps to the original source 21 | sourceMaps(), 22 | // Minify 23 | terser() 24 | ] 25 | } 26 | } 27 | 28 | function esConfig() { 29 | const config = baseConfig() 30 | config.output = [{ file: pkg.module, format: 'es', sourcemap: true }] 31 | config.plugins.unshift(typescript({ useTsconfigDeclarationDir: true })) 32 | return config 33 | } 34 | 35 | function umdConfig() { 36 | const config = baseConfig() 37 | config.output = [{ file: pkg.main, name: camelCase(entryName), format: 'umd', sourcemap: true }] 38 | config.plugins.unshift( 39 | typescript({ 40 | tsconfigOverride: { 41 | compilerOptions: { 42 | target: 'es5' 43 | } 44 | }, 45 | useTsconfigDeclarationDir: true 46 | }) 47 | ) 48 | return config 49 | } 50 | 51 | module.exports = [esConfig(), umdConfig()] 52 | -------------------------------------------------------------------------------- /src/get-selection-more.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the selected text 3 | */ 4 | export function getTextFromSelection(selection: Selection | null, win = window): string { 5 | // When called on an as HTMLIFrameElement 110 | $root.appendChild(iframe) 111 | 112 | const el = ( 113 |
114 | this is zero. this is sentence one. 115 |

116 | this is sentence two. 117 |

118 |

119 | this is sentence three. 120 |

121 |
122 | ) 123 | iframe.contentDocument!.body.appendChild(el) 124 | 125 | if (!iframe.contentWindow!.getSelection()) { 126 | // buggy firefox 127 | return 128 | } 129 | 130 | const range = iframe.contentDocument!.createRange() 131 | range.setStart(iframe.contentDocument!.getElementById('start')!.firstChild!, 2) 132 | range.setEnd(iframe.contentDocument!.getElementById('end')!.firstChild!, 3) 133 | iframe.contentWindow!.getSelection()!.addRange(range) 134 | 135 | expect(getSentence()).equal('') 136 | expect(getSentence(iframe.contentWindow as typeof window)).equal( 137 | 'this is sentence one. this is sentence two.' 138 | ) 139 | }) 140 | 141 | it('should ignore comment nodes', () => { 142 | const el = ( 143 |
144 | this is zero. this is sentence one. 145 |

146 | this is sentence two. 147 |

148 |

149 | this is sentence three. 150 |

151 |
152 | ) 153 | $root.appendChild(el) 154 | 155 | el.insertBefore(document.createComment('this is a comment'), document.getElementById('start')) 156 | 157 | document 158 | .getElementById('end')! 159 | .insertBefore( 160 | document.createComment('this is a comment'), 161 | document.getElementById('end')!.firstChild 162 | ) 163 | 164 | const range = document.createRange() 165 | range.setStart(document.getElementById('start')!.firstChild!, 2) 166 | range.setEnd(document.getElementById('end')!.firstChild!, 3) 167 | window.getSelection()!.addRange(range) 168 | 169 | expect(getSentence()).equal('this is sentence one. this is sentence two.') 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /test/get-text.spec.tsx: -------------------------------------------------------------------------------- 1 | import { getText, getTextFromSelection } from '../src/get-selection-more' 2 | import { h } from 'tsx-dom' 3 | 4 | describe('getText', () => { 5 | let $root =
6 | 7 | beforeEach(() => { 8 | if ($root) { 9 | $root.remove() 10 | } 11 | $root =
12 | document.body.appendChild($root) 13 | 14 | window.getSelection()!.removeAllRanges() 15 | }) 16 | 17 | after(() => { 18 | if ($root) { 19 | $root.remove() 20 | } 21 | }) 22 | 23 | it('should return empty text when no selection', () => { 24 | expect(getText()).to.be.equal('') 25 | expect(getTextFromSelection(window.getSelection())).to.be.equal('') 26 | }) 27 | 28 | it('should return whole text when a element node is selected', () => { 29 | const el =
test
30 | $root.appendChild(el) 31 | 32 | const range = document.createRange() 33 | range.selectNode(el) 34 | window.getSelection()!.addRange(range) 35 | 36 | expect(getText()).to.be.equal('test') 37 | }) 38 | 39 | it('should return text of all the children when a element node is selected', () => { 40 | const el = ( 41 |
42 | testtesttest 43 |
44 | ) 45 | $root.appendChild(el) 46 | 47 | const range = document.createRange() 48 | range.selectNode(el) 49 | window.getSelection()!.addRange(range) 50 | 51 | expect(getText()).equal('testtesttest') 52 | }) 53 | 54 | it('should return selected text when part of a text node is selected', () => { 55 | const el = ( 56 |
57 | testtesttest 58 |
59 | ) 60 | $root.appendChild(el) 61 | 62 | const range = document.createRange() 63 | range.setStart(el.firstElementChild!.firstChild!, 2) 64 | range.setEnd(el.firstElementChild!.firstChild!, 3) 65 | window.getSelection()!.addRange(range) 66 | 67 | expect(getText()).to.equal('s') 68 | }) 69 | 70 | it('should return selected text when part of two nodes are selected', () => { 71 | const el = ( 72 |
73 | testtesttest 74 |
75 | ) 76 | $root.appendChild(el) 77 | 78 | const range = document.createRange() 79 | range.setStart(el.firstElementChild!.firstChild!, 2) 80 | range.setEnd(el.lastChild!, 3) 81 | window.getSelection()!.addRange(range) 82 | 83 | expect(getText()).to.equal('sttes') 84 | }) 85 | 86 | it('should return selected text when part of more block elements are selected', () => { 87 | const el = ( 88 |
89 |
test
90 |
test
91 |
test
92 |
93 | ) 94 | $root.appendChild(el) 95 | 96 | const range = document.createRange() 97 | range.setStart(el.firstElementChild!.firstChild!, 2) 98 | range.setEnd(el.lastElementChild!.firstChild!, 3) 99 | window.getSelection()!.addRange(range) 100 | 101 | expect(getText()).to.equal('st\ntest\ntes') 102 | }) 103 | 104 | it('should return selected text on input', () => { 105 | const el = as HTMLInputElement 106 | $root.appendChild(el) 107 | 108 | el.focus() 109 | el.setSelectionRange(0, 4) 110 | 111 | expect(getText()).to.equal('test') 112 | }) 113 | 114 | it('should return empty text on focused input with no selection', () => { 115 | const el = as HTMLInputElement 116 | $root.appendChild(el) 117 | 118 | el.focus() 119 | 120 | expect(getText()).to.equal('') 121 | }) 122 | 123 | it('should return selected text in iframe', () => { 124 | const iframe =