├── .editorconfig
├── .gitignore
├── .npmrc
├── .travis.yml
├── .yo-rc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── custom-typings.d.ts
├── demo
├── demo.component.ts
├── demo.module.ts
├── entry.ts
└── index.ejs
├── karma.conf.ts
├── package-lock.json
├── package.json
├── src
├── highlight-tag.interface.ts
├── index.ts
├── text-input-element.directive.ts
├── text-input-highlight-container.directive.ts
├── text-input-highlight.component.ts
├── text-input-highlight.module.ts
└── text-input-highlight.scss
├── test
├── entry.ts
└── text-input-highlight.component.spec.ts
├── tsconfig-compodoc.json
├── tsconfig-ngc.json
├── tsconfig.json
├── tslint.json
├── webpack.config.ts
└── webpack.config.umd.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .DS_Store
3 | node_modules/
4 | coverage/
5 | npm-debug.log
6 | dist/
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-prefix=^
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - '10'
5 |
6 | script: npm test
7 |
8 | notifications:
9 | email: false
10 |
11 | cache:
12 | directories:
13 | - node_modules
14 |
15 | after_success:
16 | - npm run codecov
17 |
18 | addons:
19 | chrome: stable
20 |
21 | dist: trusty
22 |
23 | sudo: required
24 |
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-angular-library": {
3 | "githubUsername": "mattlewis92",
4 | "githubRepoName": "angular-text-input-highlight",
5 | "npmModuleName": "angular-text-input-highlight",
6 | "moduleGlobal": "angularTextInputHighlight",
7 | "ngModuleName": "TextInputHighlightModule",
8 | "selectorPrefix": "mwl",
9 | "projectTitle": "angular text input highlight",
10 | "projectDescription": "A component that can highlight parts of text in an input or textarea. Useful for displaying mentions etc",
11 | "authorName": "Matt Lewis",
12 | "packageManager": "npm"
13 | }
14 | }
--------------------------------------------------------------------------------
/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 |
6 | ## [1.4.3](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.4.2...v1.4.3) (2020-10-27)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * enable word-wrap to fix positioning of highlight elements ([ff55187](https://github.com/mattlewis92/angular-text-input-highlight/commit/ff55187))
12 |
13 |
14 |
15 |
16 | ## [1.4.2](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.4.1...v1.4.2) (2020-03-05)
17 |
18 |
19 | ### Bug Fixes
20 |
21 | * allow latest angular versions in peer dependencies ([0f07483](https://github.com/mattlewis92/angular-text-input-highlight/commit/0f07483))
22 |
23 |
24 |
25 |
26 | ## [1.4.1](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.4.0...v1.4.1) (2020-02-12)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * update highlight positioning when resizing textarea ([6f8d633](https://github.com/mattlewis92/angular-text-input-highlight/commit/6f8d633))
32 |
33 |
34 |
35 |
36 | # [1.4.0](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.3.1...v1.4.0) (2018-06-20)
37 |
38 |
39 | ### Features
40 |
41 | * add original MouseEvent to tag events ([#5](https://github.com/mattlewis92/angular-text-input-highlight/issues/5)) ([8985b76](https://github.com/mattlewis92/angular-text-input-highlight/commit/8985b76))
42 |
43 |
44 |
45 |
46 | ## [1.3.1](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.3.0...v1.3.1) (2018-05-05)
47 |
48 |
49 | ### Bug Fixes
50 |
51 | * allow angular 6 peer dependency ([4a04f5d](https://github.com/mattlewis92/angular-text-input-highlight/commit/4a04f5d))
52 |
53 |
54 |
55 |
56 | # [1.3.0](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.2.2...v1.3.0) (2017-11-13)
57 |
58 |
59 | ### Features
60 |
61 | * introduce textInputValue input ([7980ee1](https://github.com/mattlewis92/angular-text-input-highlight/commit/7980ee1))
62 |
63 |
64 |
65 |
66 | ## [1.2.2](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.2.1...v1.2.2) (2017-11-09)
67 |
68 |
69 | ### Bug Fixes
70 |
71 | * make long tags wrap multiple lines ([8488879](https://github.com/mattlewis92/angular-text-input-highlight/commit/8488879))
72 |
73 |
74 |
75 |
76 | ## [1.2.1](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.2.0...v1.2.1) (2017-11-02)
77 |
78 |
79 | ### Bug Fixes
80 |
81 | * add support for angular 5 ([faf48b7](https://github.com/mattlewis92/angular-text-input-highlight/commit/faf48b7))
82 |
83 |
84 |
85 |
86 | # [1.2.0](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.1.1...v1.2.0) (2017-10-11)
87 |
88 |
89 | ### Bug Fixes
90 |
91 | * handle the component being destroyed before the initial render is complete ([cb2b3c1](https://github.com/mattlewis92/angular-text-input-highlight/commit/cb2b3c1))
92 |
93 |
94 | ### Features
95 |
96 | * expose refresh method as part of the public api ([da95817](https://github.com/mattlewis92/angular-text-input-highlight/commit/da95817))
97 |
98 |
99 |
100 |
101 | ## [1.1.1](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.1.0...v1.1.1) (2017-09-26)
102 |
103 |
104 | ### Bug Fixes
105 |
106 | * recompute highlight element size when the window is resized ([70a5ddc](https://github.com/mattlewis92/angular-text-input-highlight/commit/70a5ddc))
107 |
108 |
109 |
110 |
111 | # [1.1.0](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.0.4...v1.1.0) (2017-09-01)
112 |
113 |
114 | ### Bug Fixes
115 |
116 | * allow html special characters in the textarea value ([d86f155](https://github.com/mattlewis92/angular-text-input-highlight/commit/d86f155))
117 |
118 |
119 | ### Features
120 |
121 | * add tagClick, tagMouseEnter and tagMouseLeave outputs ([2fb7817](https://github.com/mattlewis92/angular-text-input-highlight/commit/2fb7817))
122 |
123 |
124 |
125 |
126 | ## [1.0.4](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.0.3...v1.0.4) (2017-08-14)
127 |
128 |
129 | ### Bug Fixes
130 |
131 | * tags should wrap multiple lines ([f3ba5ee](https://github.com/mattlewis92/angular-text-input-highlight/commit/f3ba5ee))
132 |
133 |
134 |
135 |
136 | ## [1.0.3](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.0.2...v1.0.3) (2017-08-07)
137 |
138 |
139 | ### Bug Fixes
140 |
141 | * **ie:** fix styles in IE 11 and edge ([21a6abd](https://github.com/mattlewis92/angular-text-input-highlight/commit/21a6abd))
142 |
143 |
144 |
145 |
146 | ## [1.0.2](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.0.1...v1.0.2) (2017-07-22)
147 |
148 |
149 | ### Bug Fixes
150 |
151 | * add spacing between tags on adjoining rows ([ca65707](https://github.com/mattlewis92/angular-text-input-highlight/commit/ca65707))
152 | * delay copying textarea styles to give it time to render ([fcf8abe](https://github.com/mattlewis92/angular-text-input-highlight/commit/fcf8abe))
153 |
154 |
155 |
156 |
157 | ## [1.0.1](https://github.com/mattlewis92/angular-text-input-highlight/compare/v1.0.0...v1.0.1) (2017-07-22)
158 |
159 |
160 | ### Bug Fixes
161 |
162 | * remove fixed width from textarea ([89c638b](https://github.com/mattlewis92/angular-text-input-highlight/commit/89c638b))
163 |
164 |
165 |
166 |
167 | # 1.0.0 (2017-07-22)
168 |
169 |
170 | ### Features
171 |
172 | * initial implementation ([b7f2955](https://github.com/mattlewis92/angular-text-input-highlight/commit/b7f2955))
173 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Matt Lewis
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular text input highlight
2 | [](https://travis-ci.org/mattlewis92/angular-text-input-highlight)
3 | [](https://codecov.io/gh/mattlewis92/angular-text-input-highlight)
4 | [](http://badge.fury.io/js/angular-text-input-highlight)
5 | [](https://david-dm.org/mattlewis92/angular-text-input-highlight?type=dev)
6 | [](https://github.com/mattlewis92/angular-text-input-highlight/issues)
7 | [](https://github.com/mattlewis92/angular-text-input-highlight/stargazers)
8 | [](https://raw.githubusercontent.com/mattlewis92/angular-text-input-highlight/master/LICENSE)
9 |
10 | ## Demo
11 | https://mattlewis92.github.io/angular-text-input-highlight/
12 |
13 | ## Table of contents
14 |
15 | - [About](#about)
16 | - [Installation](#installation)
17 | - [Documentation](#documentation)
18 | - [Development](#development)
19 | - [License](#license)
20 |
21 | ## About
22 |
23 | A component that can highlight parts of text in a textarea. Useful for displaying mentions etc
24 |
25 | ## Installation
26 |
27 | Install through npm:
28 | ```
29 | npm install --save angular-text-input-highlight
30 | ```
31 |
32 | Include the stylesheet somewhere in your app:
33 | ```
34 | node_modules/angular-text-input-highlight/text-input-highlight.css
35 | ```
36 |
37 | Then include in your apps module:
38 |
39 | ```typescript
40 | import { NgModule } from '@angular/core';
41 | import { TextInputHighlightModule } from 'angular-text-input-highlight';
42 |
43 | @NgModule({
44 | imports: [
45 | TextInputHighlightModule
46 | ]
47 | })
48 | export class MyModule {}
49 | ```
50 |
51 | Finally use in one of your apps components:
52 | ```typescript
53 | import { Component, ViewEncapsulation } from '@angular/core';
54 | import { HighlightTag } from 'angular-text-input-highlight';
55 |
56 | @Component({
57 | template: `
58 |
59 |
64 |
67 |
68 |
69 | `,
70 | styles: [
71 | `
72 | // by default you won't see the highlighted tags until
73 | // you add a CSS class with a background color
74 | .bg-blue {
75 | background-color: lightblue;
76 | }
77 | .bg-pink {
78 | background-color: lightcoral;
79 | }
80 | `
81 | ],
82 | encapsulation: ViewEncapsulation.None
83 | })
84 | class MyComponent {
85 |
86 | text = 'this is some text';
87 |
88 | tags: HighlightTag[] = [{
89 | indices: { start: 8, end: 12 },
90 | cssClass: 'bg-blue',
91 | data: { user: { id: 1 } }
92 | }];
93 |
94 | }
95 | ```
96 |
97 | You may also find it useful to view the [demo source](https://github.com/mattlewis92/angular-text-input-highlight/blob/master/demo/demo.component.ts).
98 |
99 | ### Usage without a module bundler
100 | ```
101 |
102 |
105 | ```
106 |
107 | ## Documentation
108 | All documentation is auto-generated from the source via [compodoc](https://compodoc.github.io/compodoc/) and can be viewed here:
109 | https://mattlewis92.github.io/angular-text-input-highlight/docs/
110 |
111 | ## Credits
112 | This component borrows heavily from the ideas of the [ui-mention](https://github.com/angular-ui/ui-mention) package.
113 |
114 | ## Development
115 |
116 | ### Prepare your environment
117 | * Install [Node.js](http://nodejs.org/) and NPM
118 | * Install local dev dependencies: `npm install` while current directory is this repo
119 |
120 | ### Development server
121 | Run `npm start` to start a development server on port 8000 with auto reload + tests.
122 |
123 | ### Testing
124 | Run `npm test` to run tests once or `npm run test:watch` to continually run tests.
125 |
126 | ### Release
127 | ```bash
128 | npm run release
129 | ```
130 |
131 | ## License
132 |
133 | MIT
134 |
--------------------------------------------------------------------------------
/custom-typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'fork-ts-checker-webpack-plugin';
2 | declare module 'webpack-angular-externals';
3 | declare module 'webpack-rxjs-externals';
--------------------------------------------------------------------------------
/demo/demo.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core';
2 | import { HighlightTag } from '../src/';
3 |
4 | @Component({
5 | selector: 'mwl-demo-app',
6 | template: `
7 |
8 |
16 |
23 |
24 |
25 |
26 |
27 | Tag clicked! {{ tagClicked.data }}
28 |
29 | `,
30 | styles: [
31 | `
32 | .bg-blue {
33 | background-color: lightblue;
34 | }
35 | .bg-pink {
36 | background-color: lightcoral;
37 | }
38 | .bg-blue-dark {
39 | background-color: #86c5da;
40 | }
41 | .bg-pink-dark {
42 | background-color: #eb5252;
43 | }
44 | textarea {
45 | width: 500px;
46 | }
47 | `
48 | ],
49 | encapsulation: ViewEncapsulation.None
50 | })
51 | export class DemoComponent implements OnInit {
52 | text: string = 'Hello @mattlewis92 how are you today?\n\nLook I have a #different background color!\n\n@angular is pretty awesome!';
53 |
54 | tags: HighlightTag[] = [];
55 |
56 | tagClicked: HighlightTag;
57 |
58 | ngOnInit(): void {
59 | this.addTags();
60 | }
61 |
62 | addTags() {
63 | this.tags = [];
64 | const matchMentions = /(@\w+) ?/g;
65 | let mention;
66 | // tslint:disable-next-line
67 | while ((mention = matchMentions.exec(this.text))) {
68 | this.tags.push({
69 | indices: {
70 | start: mention.index,
71 | end: mention.index + mention[1].length
72 | },
73 | data: mention[1]
74 | });
75 | }
76 |
77 | const matchHashtags = /(#\w+) ?/g;
78 | let hashtag;
79 | // tslint:disable-next-line
80 | while ((hashtag = matchHashtags.exec(this.text))) {
81 | this.tags.push({
82 | indices: {
83 | start: hashtag.index,
84 | end: hashtag.index + hashtag[1].length
85 | },
86 | cssClass: 'bg-pink',
87 | data: hashtag[1]
88 | });
89 | }
90 | }
91 |
92 | addDarkClass(elm: HTMLElement) {
93 | if (elm.classList.contains('bg-blue')) {
94 | elm.classList.add('bg-blue-dark');
95 | } else if (elm.classList.contains('bg-pink')) {
96 | elm.classList.add('bg-pink-dark');
97 | }
98 | }
99 |
100 | removeDarkClass(elm: HTMLElement) {
101 | elm.classList.remove('bg-blue-dark');
102 | elm.classList.remove('bg-pink-dark');
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/demo/demo.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { FormsModule } from '@angular/forms';
4 | import { TextInputHighlightModule } from '../src';
5 | import { DemoComponent } from './demo.component';
6 |
7 | @NgModule({
8 | declarations: [DemoComponent],
9 | imports: [BrowserModule, FormsModule, TextInputHighlightModule],
10 | bootstrap: [DemoComponent]
11 | })
12 | export class DemoModule {}
13 |
--------------------------------------------------------------------------------
/demo/entry.ts:
--------------------------------------------------------------------------------
1 | import '../src/text-input-highlight.scss';
2 | import 'core-js';
3 | import 'zone.js/dist/zone';
4 | import { enableProdMode } from '@angular/core';
5 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
6 | import { DemoModule } from './demo.module';
7 |
8 | declare const ENV: string;
9 | if (ENV === 'production') {
10 | enableProdMode();
11 | }
12 |
13 | platformBrowserDynamic().bootstrapModule(DemoModule);
14 |
--------------------------------------------------------------------------------
/demo/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | angular text input highlight
9 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
37 |
38 |
39 | Loading demo...
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/karma.conf.ts:
--------------------------------------------------------------------------------
1 | import * as webpack from 'webpack';
2 | import * as path from 'path';
3 | import * as ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
4 |
5 | export default (config: any) => {
6 |
7 | config.set({
8 |
9 | // base path that will be used to resolve all patterns (eg. files, exclude)
10 | basePath: './',
11 |
12 | // frameworks to use
13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
14 | frameworks: ['mocha'],
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'test/entry.ts'
19 | ],
20 |
21 | // preprocess matching files before serving them to the browser
22 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
23 | preprocessors: {
24 | 'test/entry.ts': ['webpack', 'sourcemap']
25 | },
26 |
27 | webpack: {
28 | resolve: {
29 | extensions: ['.ts', '.js']
30 | },
31 | module: {
32 | rules: [{
33 | test: /\.ts$/,
34 | loader: 'tslint-loader',
35 | exclude: /node_modules/,
36 | enforce: 'pre',
37 | options: {
38 | emitErrors: config.singleRun,
39 | failOnHint: config.singleRun
40 | }
41 | }, {
42 | test: /\.ts$/,
43 | loader: 'ts-loader',
44 | exclude: /node_modules/,
45 | options: {
46 | transpileOnly: !config.singleRun
47 | }
48 | }, {
49 | test: /src(\\|\/).+\.ts$/,
50 | exclude: /(node_modules|\.spec\.ts$)/,
51 | loader: 'istanbul-instrumenter-loader',
52 | enforce: 'post'
53 | }]
54 | },
55 | plugins: [
56 | new webpack.SourceMapDevToolPlugin({
57 | filename: null,
58 | test: /\.(ts|js)($|\?)/i
59 | }),
60 | new webpack.ContextReplacementPlugin(
61 | /angular(\\|\/)core(\\|\/)@angular/,
62 | path.join(__dirname, 'src')
63 | ),
64 | ...(config.singleRun ? [
65 | new webpack.NoEmitOnErrorsPlugin()
66 | ] : [
67 | new ForkTsCheckerWebpackPlugin({
68 | watch: ['./src', './test']
69 | })
70 | ])
71 | ]
72 | },
73 |
74 | mochaReporter: {
75 | showDiff: true,
76 | output: 'autowatch'
77 | },
78 |
79 | coverageIstanbulReporter: {
80 | reports: ['text-summary', 'html', 'lcovonly'],
81 | fixWebpackSourcePaths: true
82 | },
83 |
84 | mime: {
85 | 'text/x-typescript': ['ts']
86 | },
87 |
88 | // test results reporter to use
89 | // possible values: 'dots', 'progress'
90 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
91 | reporters: ['mocha', 'coverage-istanbul'],
92 |
93 | // level of logging
94 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
95 | logLevel: config.LOG_INFO,
96 |
97 | // start these browsers
98 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
99 | browsers: ['ChromeHeadless']
100 |
101 | });
102 |
103 | };
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-text-input-highlight",
3 | "version": "1.4.3",
4 | "description": "A component that can highlight parts of text in a textarea. Useful for displaying mentions etc",
5 | "main": "./bundles/angular-text-input-highlight.umd.js",
6 | "module": "./index.js",
7 | "typings": "./index.d.ts",
8 | "scripts": {
9 | "start": "concurrently --raw \"webpack-dev-server --open\" \"npm run test:watch\"",
10 | "build:demo": "webpack -p",
11 | "build:umd": "webpack --config webpack.config.umd.ts",
12 | "build:ngc": "ngc -p tsconfig-ngc.json",
13 | "build:sass": "node-sass src/text-input-highlight.scss dist/text-input-highlight.css",
14 | "build:dist": "npm run build:umd && npm run build:ngc && npm run build:sass",
15 | "build:clean": "del-cli dist",
16 | "test": "karma start --single-run && npm run build:dist && npm run build:clean",
17 | "test:watch": "karma start --auto-watch",
18 | "commit": "git-cz",
19 | "compodoc": "compodoc -p tsconfig-compodoc.json -d docs --disableGraph --disableCoverage --disablePrivate --disableInternal --disableLifeCycleHooks --disableProtected",
20 | "gh-pages": "git checkout gh-pages && git merge master --no-edit --no-ff && npm run build:demo && npm run compodoc && git add . && git commit -m \"chore: build demo and docs\" && git push && git checkout master",
21 | "copyfiles": "copyfiles package.json LICENSE README.md CHANGELOG.md dist",
22 | "prerelease": "npm test",
23 | "release:git": "standard-version && git push --follow-tags origin master",
24 | "release:npm": "npm run build:dist && npm run copyfiles && npm publish dist",
25 | "release": "npm run release:git && npm run release:npm",
26 | "postrelease": "npm run build:clean && npm run gh-pages",
27 | "codecov": "cat coverage/lcov.info | codecov",
28 | "prettier": "prettier --single-quote --parser typescript --write"
29 | },
30 | "lint-staged": {
31 | "{demo,src,test}/**/*.ts": [
32 | "npm run prettier",
33 | "git add"
34 | ]
35 | },
36 | "config": {
37 | "commitizen": {
38 | "path": "@commitlint/prompt"
39 | }
40 | },
41 | "commitlint": {
42 | "extends": [
43 | "@commitlint/config-conventional"
44 | ],
45 | "rules": {
46 | "subject-case": [
47 | 0
48 | ]
49 | }
50 | },
51 | "repository": {
52 | "type": "git",
53 | "url": "git+https://github.com/mattlewis92/angular-text-input-highlight.git"
54 | },
55 | "keywords": [
56 | "angular4",
57 | "angular2",
58 | "angular",
59 | "highlight",
60 | "textarea"
61 | ],
62 | "author": "Matt Lewis",
63 | "license": "MIT",
64 | "bugs": {
65 | "url": "https://github.com/mattlewis92/angular-text-input-highlight/issues"
66 | },
67 | "homepage": "https://github.com/mattlewis92/angular-text-input-highlight#readme",
68 | "devDependencies": {
69 | "@angular/common": "^4.4.4",
70 | "@angular/compiler": "^4.4.4",
71 | "@angular/compiler-cli": "^4.4.4",
72 | "@angular/core": "^4.4.4",
73 | "@angular/forms": "^4.4.4",
74 | "@angular/language-service": "^4.4.4",
75 | "@angular/platform-browser": "^4.4.4",
76 | "@angular/platform-browser-dynamic": "^4.4.4",
77 | "@commitlint/cli": "^4.1.1",
78 | "@commitlint/config-conventional": "^8.3.4",
79 | "@commitlint/prompt": "^8.3.5",
80 | "@compodoc/compodoc": "^1.1.11",
81 | "@types/chai": "^4.0.4",
82 | "@types/html-webpack-plugin": "^2.11.2",
83 | "@types/mocha": "^2.2.43",
84 | "@types/node": "^8.0.34",
85 | "@types/sinon": "^2.3.6",
86 | "@types/sinon-chai": "^2.7.29",
87 | "@types/webpack": "^3.0.13",
88 | "chai": "^4.1.2",
89 | "codecov": "^2.3.0",
90 | "codelyzer": "^3.2.1",
91 | "commitizen": "^2.8.1",
92 | "concurrently": "^3.0.0",
93 | "copyfiles": "^1.2.0",
94 | "core-js": "^2.5.1",
95 | "css-loader": "^0.28.7",
96 | "del-cli": "^1.0.0",
97 | "fork-ts-checker-webpack-plugin": "^0.2.8",
98 | "html-webpack-plugin": "^2.30.1",
99 | "husky": "^4.2.1",
100 | "istanbul-instrumenter-loader": "^3.0.0",
101 | "karma": "^1.7.1",
102 | "karma-chrome-launcher": "^2.1.1",
103 | "karma-coverage-istanbul-reporter": "^1.0.0",
104 | "karma-mocha": "^1.3.0",
105 | "karma-mocha-reporter": "^2.2.4",
106 | "karma-sourcemap-loader": "^0.3.7",
107 | "karma-webpack": "^2.0.5",
108 | "lint-staged": "^4.2.3",
109 | "mocha": "^4.0.1",
110 | "node-sass": "^4.13.1",
111 | "prettier": "^1.7.4",
112 | "rxjs": "^5.4.3",
113 | "sass-loader": "^6.0.6",
114 | "sinon": "^4.0.1",
115 | "sinon-chai": "^2.14.0",
116 | "standard-version": "^4.0.0",
117 | "style-loader": "^0.19.0",
118 | "ts-loader": "^2.3.7",
119 | "ts-node": "^3.3.0",
120 | "tslint": "^5.7.0",
121 | "tslint-config-mwl": "^0.2.0",
122 | "tslint-loader": "^3.5.3",
123 | "typescript": "^2.5.3",
124 | "webpack": "^3.6.0",
125 | "webpack-angular-externals": "^1.0.0",
126 | "webpack-dev-server": "^2.11.5",
127 | "webpack-rxjs-externals": "^1.0.0",
128 | "zone.js": "^0.8.18"
129 | },
130 | "peerDependencies": {
131 | "@angular/core": ">=2.0.0"
132 | },
133 | "husky": {
134 | "hooks": {
135 | "commit-msg": "commitlint -e",
136 | "pre-commit": "lint-staged"
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/highlight-tag.interface.ts:
--------------------------------------------------------------------------------
1 | export interface HighlightTag {
2 | indices: {
3 | start: number;
4 | end: number;
5 | };
6 | cssClass?: string;
7 | data?: any;
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './text-input-highlight.module';
2 | export * from './highlight-tag.interface';
3 |
--------------------------------------------------------------------------------
/src/text-input-element.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | @Directive({
4 | selector: 'textarea[mwlTextInputElement]',
5 | host: {
6 | '[class.text-input-element]': 'true'
7 | }
8 | })
9 | export class TextInputElementDirective {}
10 |
--------------------------------------------------------------------------------
/src/text-input-highlight-container.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[mwlTextInputHighlightContainer]',
5 | host: {
6 | '[class.text-input-highlight-container]': 'true'
7 | }
8 | })
9 | export class TextInputHighlightContainerDirective {}
10 |
--------------------------------------------------------------------------------
/src/text-input-highlight.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectorRef,
3 | Component,
4 | ElementRef,
5 | EventEmitter,
6 | HostListener,
7 | Input,
8 | OnChanges,
9 | OnDestroy,
10 | Output,
11 | Renderer2,
12 | SimpleChanges,
13 | ViewChild
14 | } from '@angular/core';
15 | import { HighlightTag } from './highlight-tag.interface';
16 |
17 | const styleProperties = Object.freeze([
18 | 'direction', // RTL support
19 | 'boxSizing',
20 | 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
21 | 'height',
22 | 'overflowX',
23 | 'overflowY', // copy the scrollbar for IE
24 |
25 | 'borderTopWidth',
26 | 'borderRightWidth',
27 | 'borderBottomWidth',
28 | 'borderLeftWidth',
29 | 'borderStyle',
30 |
31 | 'paddingTop',
32 | 'paddingRight',
33 | 'paddingBottom',
34 | 'paddingLeft',
35 |
36 | // https://developer.mozilla.org/en-US/docs/Web/CSS/font
37 | 'fontStyle',
38 | 'fontVariant',
39 | 'fontWeight',
40 | 'fontStretch',
41 | 'fontSize',
42 | 'fontSizeAdjust',
43 | 'lineHeight',
44 | 'fontFamily',
45 |
46 | 'textAlign',
47 | 'textTransform',
48 | 'textIndent',
49 | 'textDecoration', // might not make a difference, but better be safe
50 |
51 | 'letterSpacing',
52 | 'wordSpacing',
53 |
54 | 'tabSize',
55 | 'MozTabSize'
56 | ]);
57 |
58 | const tagIndexIdPrefix = 'text-highlight-tag-id-';
59 |
60 | function indexIsInsideTag(index: number, tag: HighlightTag) {
61 | return tag.indices.start < index && index < tag.indices.end;
62 | }
63 |
64 | function overlaps(tagA: HighlightTag, tagB: HighlightTag) {
65 | return (
66 | indexIsInsideTag(tagB.indices.start, tagA) ||
67 | indexIsInsideTag(tagB.indices.end, tagA)
68 | );
69 | }
70 |
71 | function isCoordinateWithinRect(rect: ClientRect, x: number, y: number) {
72 | return rect.left < x && x < rect.right && (rect.top < y && y < rect.bottom);
73 | }
74 |
75 | function escapeHtml(str: string): string {
76 | return str.replace(//g, '>');
77 | }
78 |
79 | export interface TagMouseEvent {
80 | tag: HighlightTag;
81 | target: HTMLElement;
82 | event: MouseEvent;
83 | }
84 |
85 | @Component({
86 | selector: 'mwl-text-input-highlight',
87 | template: `
88 |
93 |
94 | `
95 | })
96 | export class TextInputHighlightComponent implements OnChanges, OnDestroy {
97 | /**
98 | * The CSS class to add to highlighted tags
99 | */
100 | @Input() tagCssClass: string = '';
101 |
102 | /**
103 | * An array of indices of the textarea value to highlight
104 | */
105 | @Input() tags: HighlightTag[] = [];
106 |
107 | /**
108 | * The textarea to highlight
109 | */
110 | @Input() textInputElement: HTMLTextAreaElement;
111 |
112 | /**
113 | * The textarea value, in not provided will fall back to trying to grab it automatically from the textarea
114 | */
115 | @Input() textInputValue: string;
116 |
117 | /**
118 | * Called when the area over a tag is clicked
119 | */
120 | @Output() tagClick = new EventEmitter();
121 |
122 | /**
123 | * Called when the area over a tag is moused over
124 | */
125 | @Output() tagMouseEnter = new EventEmitter();
126 |
127 | /**
128 | * Called when the area over the tag has the mouse is removed from it
129 | */
130 | @Output() tagMouseLeave = new EventEmitter();
131 |
132 | /**
133 | * @private
134 | */
135 | highlightElementContainerStyle: { [key: string]: string } = {};
136 |
137 | /**
138 | * @private
139 | */
140 | highlightedText: string;
141 |
142 | @ViewChild('highlightElement') private highlightElement: ElementRef;
143 |
144 | private textareaEventListeners: Array<() => void> = [];
145 |
146 | private highlightTagElements: Array<{
147 | element: HTMLElement;
148 | clientRect: ClientRect;
149 | }>;
150 |
151 | private mouseHoveredTag: TagMouseEvent | undefined;
152 |
153 | private isDestroyed = false;
154 |
155 | constructor(private renderer: Renderer2, private cdr: ChangeDetectorRef) {}
156 |
157 | /**
158 | * Manually call this function to refresh the highlight element if the textarea styles have changed
159 | */
160 | refresh() {
161 | const computed: any = getComputedStyle(this.textInputElement);
162 | styleProperties.forEach(prop => {
163 | this.highlightElementContainerStyle[prop] = computed[prop];
164 | });
165 | }
166 |
167 | /**
168 | * @private
169 | */
170 | ngOnChanges(changes: SimpleChanges): void {
171 | if (changes.textInputElement) {
172 | this.textInputElementChanged();
173 | }
174 |
175 | if (changes.tags || changes.tagCssClass || changes.textInputValue) {
176 | this.addTags();
177 | }
178 | }
179 |
180 | /**
181 | * @private
182 | */
183 | ngOnDestroy(): void {
184 | this.isDestroyed = true;
185 | this.textareaEventListeners.forEach(unregister => unregister());
186 | }
187 |
188 | /**
189 | * @private
190 | */
191 | @HostListener('window:resize')
192 | onWindowResize() {
193 | this.refresh();
194 | }
195 |
196 | private textInputElementChanged() {
197 | const elementType = this.textInputElement.tagName.toLowerCase();
198 | if (elementType !== 'textarea') {
199 | throw new Error(
200 | 'The angular-text-input-highlight component must be passed ' +
201 | 'a textarea to the `textInputElement` input. Instead received a ' +
202 | elementType
203 | );
204 | }
205 |
206 | setTimeout(() => {
207 | // in case the element was destroyed before the timeout fires
208 | if (!this.isDestroyed) {
209 | this.refresh();
210 |
211 | this.textareaEventListeners.forEach(unregister => unregister());
212 | this.textareaEventListeners = [
213 | this.renderer.listen(this.textInputElement, 'input', () => {
214 | this.addTags();
215 | }),
216 | this.renderer.listen(this.textInputElement, 'scroll', () => {
217 | this.highlightElement.nativeElement.scrollTop = this.textInputElement.scrollTop;
218 | this.highlightTagElements = this.highlightTagElements.map(tag => {
219 | tag.clientRect = tag.element.getBoundingClientRect();
220 | return tag;
221 | });
222 | }),
223 | this.renderer.listen(this.textInputElement, 'mouseup', () => {
224 | this.refresh();
225 | })
226 | ];
227 |
228 | // only add event listeners if the host component actually asks for it
229 | if (this.tagClick.observers.length > 0) {
230 | const onClick = this.renderer.listen(
231 | this.textInputElement,
232 | 'click',
233 | event => {
234 | this.handleTextareaMouseEvent(event, 'click');
235 | }
236 | );
237 | this.textareaEventListeners.push(onClick);
238 | }
239 |
240 | if (this.tagMouseEnter.observers.length > 0) {
241 | const onMouseMove = this.renderer.listen(
242 | this.textInputElement,
243 | 'mousemove',
244 | event => {
245 | this.handleTextareaMouseEvent(event, 'mousemove');
246 | }
247 | );
248 | const onMouseLeave = this.renderer.listen(
249 | this.textInputElement,
250 | 'mouseleave',
251 | event => {
252 | if (this.mouseHoveredTag) {
253 | this.tagMouseLeave.emit(this.mouseHoveredTag);
254 | this.mouseHoveredTag = undefined;
255 | }
256 | }
257 | );
258 | this.textareaEventListeners.push(onMouseMove);
259 | this.textareaEventListeners.push(onMouseLeave);
260 | }
261 |
262 | this.addTags();
263 | }
264 | });
265 | }
266 |
267 | private addTags() {
268 | const textInputValue =
269 | typeof this.textInputValue !== 'undefined'
270 | ? this.textInputValue
271 | : this.textInputElement.value;
272 |
273 | const prevTags: HighlightTag[] = [];
274 | const parts: string[] = [];
275 |
276 | [...this.tags]
277 | .sort((tagA, tagB) => {
278 | return tagA.indices.start - tagB.indices.start;
279 | })
280 | .forEach(tag => {
281 | if (tag.indices.start > tag.indices.end) {
282 | throw new Error(
283 | `Highlight tag with indices [${tag.indices.start}, ${tag.indices
284 | .end}] cannot start after it ends.`
285 | );
286 | }
287 |
288 | prevTags.forEach(prevTag => {
289 | if (overlaps(prevTag, tag)) {
290 | throw new Error(
291 | `Highlight tag with indices [${tag.indices.start}, ${tag.indices
292 | .end}] overlaps with tag [${prevTag.indices.start}, ${prevTag
293 | .indices.end}]`
294 | );
295 | }
296 | });
297 |
298 | // TODO - implement this as an ngFor of items that is generated in the template for a cleaner solution
299 |
300 | const expectedTagLength = tag.indices.end - tag.indices.start;
301 | const tagContents = textInputValue.slice(
302 | tag.indices.start,
303 | tag.indices.end
304 | );
305 | if (tagContents.length === expectedTagLength) {
306 | const previousIndex =
307 | prevTags.length > 0 ? prevTags[prevTags.length - 1].indices.end : 0;
308 | const before = textInputValue.slice(previousIndex, tag.indices.start);
309 | parts.push(escapeHtml(before));
310 | const cssClass = tag.cssClass || this.tagCssClass;
311 | const tagId = tagIndexIdPrefix + this.tags.indexOf(tag);
312 | // text-highlight-tag-id-${id} is used instead of a data attribute to prevent an angular sanitization warning
313 | parts.push(
314 | `${escapeHtml(
315 | tagContents
316 | )}`
317 | );
318 | prevTags.push(tag);
319 | }
320 | });
321 | const remainingIndex =
322 | prevTags.length > 0 ? prevTags[prevTags.length - 1].indices.end : 0;
323 | const remaining = textInputValue.slice(remainingIndex);
324 | parts.push(escapeHtml(remaining));
325 | parts.push(' ');
326 | this.highlightedText = parts.join('');
327 | this.cdr.detectChanges();
328 | this.highlightTagElements = Array.from(
329 | this.highlightElement.nativeElement.getElementsByTagName('span')
330 | ).map((element: HTMLElement) => {
331 | return { element, clientRect: element.getBoundingClientRect() };
332 | });
333 | }
334 |
335 | private handleTextareaMouseEvent(
336 | event: MouseEvent,
337 | eventName: 'click' | 'mousemove'
338 | ) {
339 | const matchingTagIndex = this.highlightTagElements.findIndex(elm =>
340 | isCoordinateWithinRect(elm.clientRect, event.clientX, event.clientY)
341 | );
342 | if (matchingTagIndex > -1) {
343 | const target = this.highlightTagElements[matchingTagIndex].element;
344 | const tagClass = Array.from(target.classList).find(className =>
345 | className.startsWith(tagIndexIdPrefix)
346 | );
347 | if (tagClass) {
348 | const tagId = tagClass.replace(tagIndexIdPrefix, '');
349 | const tag: HighlightTag = this.tags[+tagId];
350 | const tagMouseEvent = { tag, target, event };
351 | if (eventName === 'click') {
352 | this.tagClick.emit(tagMouseEvent);
353 | } else if (!this.mouseHoveredTag) {
354 | this.mouseHoveredTag = tagMouseEvent;
355 | this.tagMouseEnter.emit(tagMouseEvent);
356 | }
357 | }
358 | } else if (eventName === 'mousemove' && this.mouseHoveredTag) {
359 | this.mouseHoveredTag.event = event;
360 | this.tagMouseLeave.emit(this.mouseHoveredTag);
361 | this.mouseHoveredTag = undefined;
362 | }
363 | }
364 | }
365 |
--------------------------------------------------------------------------------
/src/text-input-highlight.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { TextInputHighlightComponent } from './text-input-highlight.component';
4 | import { TextInputHighlightContainerDirective } from './text-input-highlight-container.directive';
5 | import { TextInputElementDirective } from './text-input-element.directive';
6 |
7 | @NgModule({
8 | declarations: [
9 | TextInputHighlightComponent,
10 | TextInputHighlightContainerDirective,
11 | TextInputElementDirective
12 | ],
13 | imports: [CommonModule],
14 | exports: [
15 | TextInputHighlightComponent,
16 | TextInputHighlightContainerDirective,
17 | TextInputElementDirective
18 | ]
19 | })
20 | export class TextInputHighlightModule {}
21 |
--------------------------------------------------------------------------------
/src/text-input-highlight.scss:
--------------------------------------------------------------------------------
1 | .text-input-highlight-container {
2 | position: relative;
3 |
4 | .text-input-element {
5 | background: none;
6 | position: relative;
7 | z-index: 2;
8 | }
9 |
10 | .text-highlight-element {
11 | overflow: hidden !important;
12 | word-break: break-word;
13 | white-space: pre-wrap;
14 | position: absolute;
15 | top: 0;
16 | bottom: 0;
17 | left: 0;
18 | right: 0;
19 | background: white;
20 | color: rgba(0, 0, 0, 0);
21 | z-index: 1;
22 | }
23 |
24 | .text-highlight-tag {
25 | border-radius: 8px;
26 | padding: 1px 3px;
27 | margin: -1px -3px;
28 | overflow-wrap: break-word;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/entry.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import 'core-js';
4 | import 'zone.js/dist/zone';
5 | import 'zone.js/dist/long-stack-trace-zone';
6 | import 'zone.js/dist/async-test';
7 | import 'zone.js/dist/fake-async-test';
8 | import 'zone.js/dist/sync-test';
9 | import 'zone.js/dist/proxy';
10 | import 'zone.js/dist/mocha-patch';
11 | import 'rxjs/Rx';
12 | import { use } from 'chai';
13 | import * as sinonChai from 'sinon-chai';
14 | import { TestBed } from '@angular/core/testing';
15 | import {
16 | BrowserDynamicTestingModule,
17 | platformBrowserDynamicTesting
18 | } from '@angular/platform-browser-dynamic/testing';
19 |
20 | use(sinonChai);
21 |
22 | TestBed.initTestEnvironment(
23 | BrowserDynamicTestingModule,
24 | platformBrowserDynamicTesting()
25 | );
26 |
27 | declare const require: any;
28 | const testsContext: any = require.context('./', true, /\.spec/);
29 | testsContext.keys().forEach(testsContext);
30 |
--------------------------------------------------------------------------------
/test/text-input-highlight.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ComponentFixture,
3 | fakeAsync,
4 | flush,
5 | TestBed
6 | } from '@angular/core/testing';
7 | import { expect } from 'chai';
8 | import * as sinon from 'sinon';
9 | import { TextInputHighlightModule } from '../src';
10 | import { Component } from '@angular/core';
11 | import { HighlightTag } from '../src/highlight-tag.interface';
12 | import { By } from '@angular/platform-browser';
13 | import { TextInputHighlightComponent } from '../src/text-input-highlight.component';
14 | import { FormsModule } from '@angular/forms';
15 |
16 | @Component({
17 | template: `
18 |
19 |
24 |
31 |
32 |
33 | `,
34 | styles: [
35 | `
36 | textarea {
37 | height: 50px;
38 | width: 100px;
39 | }
40 | `
41 | ]
42 | })
43 | class TestComponent {
44 | text: string;
45 | tags: HighlightTag[] = [];
46 | tagCssClass: string;
47 | tagClick = sinon.spy();
48 | tagMouseEnter = sinon.spy();
49 | tagMouseLeave = sinon.spy();
50 | textInputValue: string;
51 | refresh = sinon.spy();
52 | }
53 |
54 | function createComponent({
55 | text,
56 | tags
57 | }: {
58 | text: string;
59 | tags: HighlightTag[];
60 | }) {
61 | const fixture: ComponentFixture = TestBed.createComponent(
62 | TestComponent
63 | );
64 | fixture.componentInstance.text = text;
65 | fixture.componentInstance.tags = tags;
66 | fixture.detectChanges();
67 | const textarea = fixture.debugElement.query(By.css('textarea'));
68 | const highlight = fixture.debugElement.query(
69 | By.directive(TextInputHighlightComponent)
70 | );
71 | return { fixture, textarea, highlight };
72 | }
73 |
74 | function flushTagsChanges(fixture: ComponentFixture) {
75 | fixture.detectChanges();
76 | flush();
77 | fixture.detectChanges();
78 | }
79 |
80 | describe('mwl-text-input-highlight component', () => {
81 | beforeEach(() => {
82 | TestBed.configureTestingModule({
83 | imports: [FormsModule, TextInputHighlightModule],
84 | declarations: [TestComponent]
85 | });
86 | });
87 |
88 | it(
89 | 'should highlight all given tags',
90 | fakeAsync(() => {
91 | const { highlight, fixture } = createComponent({
92 | text: 'this is some text',
93 | tags: [{ indices: { start: 8, end: 12 } }]
94 | });
95 | flushTagsChanges(fixture);
96 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
97 | 'this is some text '
98 | );
99 | })
100 | );
101 |
102 | it(
103 | 'should update the highlight element as the user types',
104 | fakeAsync(() => {
105 | const { highlight, fixture, textarea } = createComponent({
106 | text: 'this is some text',
107 | tags: [{ indices: { start: 8, end: 12 } }]
108 | });
109 | flushTagsChanges(fixture);
110 | const errorStub = sinon.stub(console, 'error'); // silence an error I cant debug easily
111 | textarea.nativeElement.value =
112 | 'this is some text that the user has typed in';
113 | textarea.triggerEventHandler('input', {});
114 | flushTagsChanges(fixture);
115 | errorStub.restore();
116 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
117 | 'this is some text that the user has typed in '
118 | );
119 | })
120 | );
121 |
122 | it(
123 | 'should allow a default highlight css tag to be set',
124 | fakeAsync(() => {
125 | TestBed.overrideComponent(TestComponent, {
126 | set: {
127 | template: `
128 |
129 |
134 |
138 |
139 |
140 | `
141 | }
142 | });
143 | const { highlight, fixture } = createComponent({
144 | text: 'this is some text',
145 | tags: [{ indices: { start: 8, end: 12 } }]
146 | });
147 | fixture.componentInstance.tagCssClass = 'foo-class';
148 | flushTagsChanges(fixture);
149 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
150 | 'this is some text '
151 | );
152 | })
153 | );
154 |
155 | it(
156 | 'should copy all styling across from the textarea to the highlight element',
157 | fakeAsync(() => {
158 | const { highlight, fixture } = createComponent({
159 | text: 'this is some text',
160 | tags: [{ indices: { start: 8, end: 12 } }]
161 | });
162 | flushTagsChanges(fixture);
163 | expect(highlight.nativeElement.children[0].style.height).to.equal('50px');
164 | })
165 | );
166 |
167 | it('should throw when a non textarea element is passed to textInputElement', () => {
168 | TestBed.overrideComponent(TestComponent, {
169 | set: {
170 | template: `
171 |
172 |
173 |
178 |
182 |
183 |
184 | `
185 | }
186 | });
187 | expect(
188 | fakeAsync(() => {
189 | createComponent({
190 | text: 'this is some text',
191 | tags: [{ indices: { start: 8, end: 12 } }]
192 | });
193 | })
194 | ).to.throw();
195 | });
196 |
197 | it(
198 | 'tag ordering should not affect highlighting',
199 | fakeAsync(() => {
200 | const { highlight, fixture } = createComponent({
201 | text: 'this is some text',
202 | tags: [
203 | { indices: { start: 8, end: 12 } },
204 | { indices: { start: 0, end: 4 } }
205 | ]
206 | });
207 | flushTagsChanges(fixture);
208 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
209 | 'this is some text '
210 | );
211 | })
212 | );
213 |
214 | it(
215 | 'should allow an individual tag css class to be overridden',
216 | fakeAsync(() => {
217 | const { highlight, fixture } = createComponent({
218 | text: 'this is some text',
219 | tags: [
220 | { indices: { start: 8, end: 12 } },
221 | { indices: { start: 0, end: 4 }, cssClass: 'foo-class' }
222 | ]
223 | });
224 | flushTagsChanges(fixture);
225 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
226 | 'this is some text '
227 | );
228 | })
229 | );
230 |
231 | it('should throw when the start index of a tag is more than the end index', () => {
232 | expect(
233 | fakeAsync(() => {
234 | const { fixture } = createComponent({
235 | text: 'this is some text',
236 | tags: [{ indices: { start: 12, end: 8 } }]
237 | });
238 | flushTagsChanges(fixture);
239 | })
240 | ).to.throw();
241 | });
242 |
243 | it(
244 | 'should skip tags where the tag indices dont exist on the textarea value',
245 | fakeAsync(() => {
246 | const { highlight, fixture } = createComponent({
247 | text: 'this is some text',
248 | tags: [{ indices: { start: 8, end: 100 } }]
249 | });
250 | flushTagsChanges(fixture);
251 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
252 | 'this is some text '
253 | );
254 | })
255 | );
256 |
257 | it('should throw when tag indices overlap', () => {
258 | expect(
259 | fakeAsync(() => {
260 | const { fixture } = createComponent({
261 | text: 'this is some text',
262 | tags: [
263 | { indices: { start: 8, end: 12 } },
264 | { indices: { start: 6, end: 10 } }
265 | ]
266 | });
267 | flushTagsChanges(fixture);
268 | })
269 | ).to.throw();
270 | });
271 |
272 | it(
273 | 'should fire the mouse clicked event',
274 | fakeAsync(() => {
275 | const { highlight, fixture } = createComponent({
276 | text: 'this is some text',
277 | tags: [{ indices: { start: 8, end: 12 } }]
278 | });
279 | flushTagsChanges(fixture);
280 | const spanRect = fixture.nativeElement
281 | .querySelector('.text-highlight-tag')
282 | .getBoundingClientRect();
283 | const eventObj: any = {
284 | clientX: spanRect.left + 1,
285 | clientY: spanRect.top + 1
286 | };
287 | fixture.debugElement
288 | .query(By.css('textarea'))
289 | .triggerEventHandler('click', eventObj);
290 | fixture.detectChanges();
291 | expect(fixture.componentInstance.tagClick).to.have.been.calledWith({
292 | target: highlight.nativeElement.querySelector('span'),
293 | tag: { indices: { start: 8, end: 12 } },
294 | event: eventObj
295 | });
296 | })
297 | );
298 |
299 | it(
300 | 'should not fire the mouse clicked event',
301 | fakeAsync(() => {
302 | const { highlight, fixture } = createComponent({
303 | text: 'this is some text',
304 | tags: [{ indices: { start: 8, end: 12 } }]
305 | });
306 | flushTagsChanges(fixture);
307 | const spanRect = fixture.nativeElement
308 | .querySelector('.text-highlight-tag')
309 | .getBoundingClientRect();
310 | fixture.debugElement
311 | .query(By.css('textarea'))
312 | .triggerEventHandler('click', {
313 | clientX: spanRect.left - 1,
314 | clientY: spanRect.top + 1
315 | });
316 | fixture.detectChanges();
317 | expect(fixture.componentInstance.tagClick.callCount).to.equal(0);
318 | })
319 | );
320 |
321 | it(
322 | 'should fire the mouse enter and leave events',
323 | fakeAsync(() => {
324 | const { highlight, fixture } = createComponent({
325 | text: 'this is some text',
326 | tags: [{ indices: { start: 8, end: 12 } }]
327 | });
328 | flushTagsChanges(fixture);
329 | const spanRect = fixture.nativeElement
330 | .querySelector('.text-highlight-tag')
331 | .getBoundingClientRect();
332 | const eventObj = {
333 | clientX: spanRect.left + 1,
334 | clientY: spanRect.top + 1
335 | };
336 | fixture.debugElement
337 | .query(By.css('textarea'))
338 | .triggerEventHandler('mousemove', eventObj);
339 | fixture.detectChanges();
340 | expect(fixture.componentInstance.tagMouseEnter).to.have.been.calledWith({
341 | target: highlight.nativeElement.querySelector('span'),
342 | tag: { indices: { start: 8, end: 12 } },
343 | event: eventObj
344 | });
345 | expect(fixture.componentInstance.tagMouseLeave.callCount).to.equal(0);
346 | fixture.debugElement
347 | .query(By.css('textarea'))
348 | .triggerEventHandler('mousemove', {
349 | clientX: spanRect.left + 1,
350 | clientY: spanRect.top + 2
351 | });
352 | fixture.detectChanges();
353 | expect(fixture.componentInstance.tagMouseEnter.callCount).to.equal(1);
354 |
355 | const leaveEventObj = {
356 | clientX: spanRect.left - 1,
357 | clientY: spanRect.top - 1
358 | };
359 | fixture.debugElement
360 | .query(By.css('textarea'))
361 | .triggerEventHandler('mousemove', leaveEventObj);
362 | fixture.detectChanges();
363 | expect(fixture.componentInstance.tagMouseLeave).to.have.been.calledWith({
364 | target: highlight.nativeElement.querySelector('span'),
365 | tag: { indices: { start: 8, end: 12 } },
366 | event: leaveEventObj
367 | });
368 | })
369 | );
370 |
371 | it(
372 | 'should fire the mouse leave event when the textarea is not moused over anymore',
373 | fakeAsync(() => {
374 | const { highlight, fixture } = createComponent({
375 | text: 'this is some text',
376 | tags: [{ indices: { start: 8, end: 12 } }]
377 | });
378 | flushTagsChanges(fixture);
379 | const spanRect = fixture.nativeElement
380 | .querySelector('.text-highlight-tag')
381 | .getBoundingClientRect();
382 | const eventObj = {
383 | clientX: spanRect.left + 1,
384 | clientY: spanRect.top + 1
385 | };
386 | fixture.debugElement
387 | .query(By.css('textarea'))
388 | .triggerEventHandler('mousemove', eventObj);
389 | fixture.detectChanges();
390 | expect(fixture.componentInstance.tagMouseEnter).to.have.been.calledWith({
391 | target: highlight.nativeElement.querySelector('span'),
392 | tag: { indices: { start: 8, end: 12 } },
393 | event: eventObj
394 | });
395 | expect(fixture.componentInstance.tagMouseLeave.callCount).to.equal(0);
396 | const leaveEventObj = {
397 | clientX: spanRect.left + 1,
398 | clientY: spanRect.top + 1
399 | };
400 | fixture.debugElement
401 | .query(By.css('textarea'))
402 | .triggerEventHandler('mouseleave', leaveEventObj);
403 | fixture.detectChanges();
404 | expect(fixture.componentInstance.tagMouseLeave).to.have.been.calledWith({
405 | target: highlight.nativeElement.querySelector('span'),
406 | tag: { indices: { start: 8, end: 12 } },
407 | event: leaveEventObj
408 | });
409 | })
410 | );
411 |
412 | it('should refresh when textarea is resized', fakeAsync(() => {
413 | const { textarea, fixture } = createComponent({
414 | text: 'this is some text',
415 | tags: [{ indices: { start: 8, end: 12 } }]
416 | });
417 | flushTagsChanges(fixture);
418 | textarea.triggerEventHandler('mouseup', {})
419 | fixture.detectChanges();
420 | expect(fixture.componentInstance.refresh.calledOnce);
421 | }));
422 |
423 | it(
424 | 'should not break with html characters',
425 | fakeAsync(() => {
426 | const { highlight, fixture } = createComponent({
427 | text: 'this some text '
433 | );
434 | })
435 | );
436 |
437 | it(
438 | 'should update the textareas dimensions when the window is resized',
439 | fakeAsync(() => {
440 | const { highlight, fixture, textarea } = createComponent({
441 | text: 'this is some text',
442 | tags: [
443 | { indices: { start: 8, end: 12 } },
444 | { indices: { start: 0, end: 4 } }
445 | ]
446 | });
447 | flushTagsChanges(fixture);
448 | const highlightElement = highlight.query(
449 | By.css('.text-highlight-element')
450 | ).nativeElement;
451 | expect(highlightElement.offsetWidth).to.equal(106);
452 | textarea.nativeElement.style.width = '200px';
453 | highlight.componentInstance.onWindowResize();
454 | fixture.detectChanges();
455 | expect(highlightElement.offsetWidth).to.equal(206);
456 | })
457 | );
458 |
459 | it(
460 | 'should not throw when the fixture is destroyed before the first render completes',
461 | fakeAsync(() => {
462 | const { fixture } = createComponent({
463 | text: 'this is some text',
464 | tags: [{ indices: { start: 8, end: 12 } }]
465 | });
466 | fixture.destroy();
467 | expect(() => flush()).not.to.throw();
468 | })
469 | );
470 |
471 | it('should allow the textarea value to be overridden', () => {
472 | const { highlight, fixture } = createComponent({
473 | text: 'this is some text',
474 | tags: [{ indices: { start: 8, end: 12 } }]
475 | });
476 | fixture.componentInstance.textInputValue = 'this is some text';
477 | fixture.detectChanges();
478 | expect(highlight.nativeElement.children[0].innerHTML).to.deep.equal(
479 | 'this is some text '
480 | );
481 | });
482 | });
483 |
--------------------------------------------------------------------------------
/tsconfig-compodoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "experimentalDecorators": true,
7 | "lib": ["es2015", "dom"]
8 | },
9 | "include": [
10 | "src"
11 | ],
12 | "exclude": [
13 | "node_modules",
14 | "demo",
15 | "test",
16 | "karma.conf.ts",
17 | "webpack.config.ts",
18 | "webpack.config.umd.ts"
19 | ]
20 | }
--------------------------------------------------------------------------------
/tsconfig-ngc.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "experimentalDecorators": true,
8 | "baseUrl": "",
9 | "stripInternal": true,
10 | "outDir": "./dist",
11 | "sourceMap": true,
12 | "inlineSources": true,
13 | "types": [],
14 | "lib": ["es2015", "dom"],
15 | "importHelpers": true
16 | },
17 | "files": [
18 | "src/index.ts"
19 | ],
20 | "angularCompilerOptions": {
21 | "strictMetadataEmit": true,
22 | "genDir": "./dist/",
23 | "skipTemplateCodegen": true,
24 | "trace": true
25 | }
26 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "emitDecoratorMetadata": true,
8 | "experimentalDecorators": true,
9 | "lib": ["es2015", "dom"],
10 | "strict": true,
11 | "skipLibCheck": true
12 | },
13 | "include": [
14 | "src/**/*",
15 | "test/**/*",
16 | "demo/**/*",
17 | "custom-typings.d.ts"
18 | ],
19 | "exclude": [
20 | "node_modules"
21 | ]
22 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint-config-mwl",
3 | "rules": {
4 | "directive-selector": [true, "attribute", "mwl", "camelCase"],
5 | "component-selector": [true, "element", "mwl", "kebab-case"],
6 | "pipe-naming": [true, "camelCase", "mwl"],
7 | "no-inferrable-types": false,
8 | "use-host-property-decorator": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import * as webpack from 'webpack';
2 | import * as path from 'path';
3 | import * as HtmlWebpackPlugin from 'html-webpack-plugin';
4 | import * as ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
5 |
6 | const IS_PROD: boolean = process.argv.indexOf('-p') > -1;
7 |
8 | export default {
9 | devtool: IS_PROD ? 'source-map' : 'eval',
10 | entry: path.join(__dirname, 'demo', 'entry.ts'),
11 | output: {
12 | filename: IS_PROD ? '[name]-[chunkhash].js' : '[name].js'
13 | },
14 | module: {
15 | rules: [{
16 | test: /\.ts$/,
17 | loader: 'tslint-loader',
18 | exclude: /node_modules/,
19 | enforce: 'pre'
20 | }, {
21 | test: /\.ts$/,
22 | loader: 'ts-loader',
23 | exclude: /node_modules/,
24 | options: {
25 | transpileOnly: !IS_PROD
26 | }
27 | }, {
28 | test: /\.scss$/,
29 | use: [
30 | 'style-loader',
31 | 'css-loader',
32 | 'sass-loader'
33 | ],
34 | exclude: /node_modules/
35 | }]
36 | },
37 | resolve: {
38 | extensions: ['.ts', '.js']
39 | },
40 | devServer: {
41 | port: 8000,
42 | inline: true,
43 | hot: true,
44 | historyApiFallback: true
45 | },
46 | plugins: [
47 | ...(IS_PROD ? [] : [
48 | new webpack.HotModuleReplacementPlugin(),
49 | new ForkTsCheckerWebpackPlugin({
50 | watch: ['./src', './demo']
51 | })
52 | ]),
53 | new webpack.DefinePlugin({
54 | ENV: JSON.stringify(IS_PROD ? 'production' : 'development')
55 | }),
56 | new webpack.ContextReplacementPlugin(
57 | /angular(\\|\/)core(\\|\/)@angular/,
58 | path.join(__dirname, 'src')
59 | ),
60 | new HtmlWebpackPlugin({
61 | template: path.join(__dirname, 'demo', 'index.ejs')
62 | })
63 | ]
64 | };
65 |
--------------------------------------------------------------------------------
/webpack.config.umd.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs';
3 | import * as webpack from 'webpack';
4 | import * as angularExternals from 'webpack-angular-externals';
5 | import * as rxjsExternals from 'webpack-rxjs-externals';
6 |
7 | const pkg = JSON.parse(fs.readFileSync('./package.json').toString());
8 |
9 | export default {
10 | entry: {
11 | 'angular-text-input-highlight.umd': path.join(__dirname, 'src', 'index.ts'),
12 | 'angular-text-input-highlight.umd.min': path.join(__dirname, 'src', 'index.ts'),
13 | },
14 | output: {
15 | path: path.join(__dirname, 'dist', 'bundles'),
16 | filename: '[name].js',
17 | libraryTarget: 'umd',
18 | library: 'angularTextInputHighlight'
19 | },
20 | externals: [
21 | angularExternals(),
22 | rxjsExternals()
23 | ],
24 | devtool: 'source-map',
25 | module: {
26 | rules: [{
27 | test: /\.ts$/,
28 | loader: 'tslint-loader',
29 | exclude: /node_modules/,
30 | enforce: 'pre',
31 | options: {
32 | emitErrors: true,
33 | failOnHint: true
34 | }
35 | }, {
36 | test: /\.ts$/,
37 | loader: 'ts-loader',
38 | exclude: /node_modules/
39 | }]
40 | },
41 | resolve: {
42 | extensions: ['.ts', '.js']
43 | },
44 | plugins: [
45 | new webpack.optimize.UglifyJsPlugin({
46 | include: /\.min\.js$/,
47 | sourceMap: true
48 | }),
49 | new webpack.ContextReplacementPlugin(
50 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
51 | path.join(__dirname, 'src')
52 | ),
53 | new webpack.BannerPlugin({
54 | banner: `
55 | /**
56 | * ${pkg.name} - ${pkg.description}
57 | * @version v${pkg.version}
58 | * @author ${pkg.author}
59 | * @link ${pkg.homepage}
60 | * @license ${pkg.license}
61 | */
62 | `.trim(),
63 | raw: true,
64 | entryOnly: true
65 | })
66 | ]
67 | };
68 |
--------------------------------------------------------------------------------