├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── ngx-clipboard.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── cSpell.json ├── spell.json └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package.json ├── projects └── ngx-clipboard │ ├── .eslintrc.json │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── interface.ts │ │ ├── ngx-clipboard-if-supported.directive.spec.ts │ │ ├── ngx-clipboard-if-supported.directive.ts │ │ ├── ngx-clipboard.directive.spec.ts │ │ ├── ngx-clipboard.directive.ts │ │ ├── ngx-clipboard.module.ts │ │ ├── ngx-clipboard.service.spec.ts │ │ └── ngx-clipboard.service.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── tslint.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "plugin:@angular-eslint/template/process-inline-templates", 14 | "plugin:prettier/recommended", 15 | "prettier" 16 | ], 17 | "rules": { 18 | "@angular-eslint/component-selector": [ 19 | "error", 20 | { 21 | "prefix": "ngx", 22 | "style": "kebab-case", 23 | "type": "element" 24 | } 25 | ], 26 | "@angular-eslint/directive-selector": [ 27 | "error", 28 | { 29 | "prefix": "ngx", 30 | "style": "camelCase", 31 | "type": "attribute" 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "files": ["*.html"], 38 | "extends": ["plugin:@angular-eslint/template/recommended"], 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ngx-clipboard 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/ngx-clipboard.yml: -------------------------------------------------------------------------------- 1 | name: ngx-clipboard 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | inputs: 7 | isBeta: 8 | description: 'Is this a beta release?' 9 | required: false 10 | default: 'false' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '20.x' 22 | cache: 'yarn' 23 | 24 | - uses: actions/cache@v4 25 | id: angular-cache 26 | with: 27 | path: | 28 | .angular/cache 29 | key: ${{ runner.os }}-ng-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: ${{ runner.os }}-ng 31 | 32 | - name: Install dependencies 33 | run: yarn --pure-lockfile --non-interactive --no-progress 34 | 35 | # Fix linting errors first 36 | # - name: Run TSLint 37 | # run: yarn lint 38 | 39 | - name: Run tests 40 | run: yarn test --configuration=ci 41 | 42 | - name: Build host app 43 | run: yarn ng build library-host --configuration=production 44 | 45 | - name: Build library 46 | run: yarn build 47 | 48 | - name: Publish library 49 | if: github.event_name == 'workflow_dispatch' 50 | run: | 51 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc && npm publish ./dist/lib --access public --tag ${{ github.event.inputs.isBeta == 'true' && 'next' || 'latest' }} 52 | env: 53 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /old 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | # IDE - VSCode 20 | .vscode/* 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | 26 | # misc 27 | /.angular/cache 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | package-lock.json 41 | .angular/cache 42 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .angular 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "insertPragma": false, 5 | "jsxBracketSameLine": false, 6 | "printWidth": 120, 7 | "rangeStart": 0, 8 | "requirePragma": false, 9 | "semi": true, 10 | "singleQuote": true, 11 | "tabWidth": 4, 12 | "trailingComma": "none", 13 | "useTabs": false 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.1 4 | "version": "0.1", 5 | 6 | // language - current active spelling language 7 | "language": "en", 8 | 9 | // words - list of words to be always considered correct 10 | "words": ["webpack"], 11 | 12 | // flagWords - list of words to be always considered incorrect 13 | // This is useful for offensive words and common spelling errors. 14 | // For example "hte" should be "the" 15 | "flagWords": ["hte"] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/spell.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "ignoreWordsList": ["npm", "Angular2", "plunker"], 4 | "mistakeTypeToStatus": { 5 | "Passive voice": "Hint", 6 | "Spelling": "Error", 7 | "Complex Expression": "Disable", 8 | "Hidden Verbs": "Information", 9 | "Hyphen Required": "Disable", 10 | "Redundant Expression": "Disable", 11 | "Did you mean...": "Disable", 12 | "Repeated Word": "Warning", 13 | "Missing apostrophe": "Warning", 14 | "Cliches": "Disable", 15 | "Missing Word": "Disable", 16 | "Make I uppercase": "Warning" 17 | }, 18 | "languageIDs": ["markdown", "plaintext"], 19 | "ignoreRegExp": ["/\\(.*\\.(jpg|jpeg|png|md|gif|JPG|JPEG|PNG|MD|GIF)\\)/g", "/((http|https|ftp|git)\\S*)/g"] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "command": "tsc", 6 | "args": ["-p", "."], 7 | "problemMatcher": "$tsc", 8 | "tasks": [ 9 | { 10 | "label": "tsc", 11 | "type": "shell", 12 | "command": "tsc", 13 | "args": ["-p", "."], 14 | "problemMatcher": "$tsc", 15 | "group": { 16 | "_id": "build", 17 | "isDefault": false 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sam Lin 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 | [![Financial Contributors on Open Collective](https://opencollective.com/ngx-clipboard/all/badge.svg?label=financial+contributors)](https://opencollective.com/ngx-clipboard) 2 | [![ngx-clipboard](https://github.com/maxisam/ngx-clipboard/actions/workflows/ngx-clipboard.yml/badge.svg)](https://github.com/maxisam/ngx-clipboard/actions/workflows/ngx-clipboard.yml) 3 | [![npm](https://img.shields.io/npm/dt/ngx-clipboard.svg?style=flat-square)](https://www.npmjs.com/package/ngx-clipboard) 4 | [![GitHub release](https://img.shields.io/github/release/maxisam/ngx-clipboard.svg?style=flat-square)](https://github.com/maxisam/ngx-clipboard/releases) 5 | [![npm](https://img.shields.io/npm/l/ngx-clipboard.svg?style=flat-square)]() 6 | 7 | # ngx-clipboard , F.K.A [angular2-clipboard](https://www.npmjs.com/package/angular2-clipboard) 8 | 9 | From 6.0.0, there is no other JS dependency anymore. Just Angular. 10 | 11 | It works with angular version 2.0.0 and up 12 | 13 | To make more sense with the future versioning scheme of Angular, the directive selector is now rename to **ngxClipboard** 14 | 15 | ## Dependencies 16 | 17 | - If you need to use it on 2.x, please use version 7.x.x. 18 | - If you need to use it on 4.x, please use version 8.x.x. 19 | - If you need to use it on 5.x, please use version 10.x.x. 20 | - If you need to use it on 8.x, please use version 12.x.x. 21 | - If you need to use it on 9.x, please use version 13.x.x. 22 | - If you need to use it on 10.x - 12.x, please use version 14.0.2. 23 | - If you need to use it on 13.x, please use version 15.x.x. (Also thanks https://github.com/arturovt for updating & tuning) 24 | 25 | The code are pretty much the same, in v8.0.0 it uses InjectionToken which requires angular4 and above. 26 | 27 | ## Install 28 | 29 | You can get it on npm. 30 | 31 | ```bat 32 | npm install ngx-clipboard --save 33 | ``` 34 | 35 | Open your module file e.g `app.module.ts` and update **imports** array 36 | 37 | ```ts 38 | import { ClipboardModule } from 'ngx-clipboard'; 39 | ... 40 | imports: [ 41 | ... 42 | ClipboardModule, 43 | ... 44 | ] 45 | ``` 46 | 47 | ## Usage 48 | 49 | If you use SystemJS to load your files, you might have to update your config: 50 | 51 | ```js 52 | System.config({ 53 | map: { 54 | 'ngx-clipboard': 'node_modules/ngx-clipboard' 55 | } 56 | }); 57 | ``` 58 | 59 | ### Copy source 60 | 61 | This library support multiple kinds of copy source. 62 | 63 | - Setting `cbContent` attribute 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | You can assign the parent **container** to avoid focus trapper issue, #145 70 | 71 | ```html 72 |
73 | 74 |
75 | ``` 76 | 77 | - Setting an input target 78 | 79 | ```html 80 | .... 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | - Using `copy` from `ClipboardService` to copy any text you dynamically created. 88 | 89 | ```ts 90 | import { ClipboardService } from 'ngx-clipboard' 91 | 92 | ... 93 | 94 | constructor(private _clipboardService: ClipboardService){ 95 | ... 96 | } 97 | 98 | copy(text: string){ 99 | this._clipboardService.copy(text) 100 | } 101 | ``` 102 | 103 | ### Callbacks 104 | 105 | - `cbOnSuccess` callback attribute is triggered after copy was successful with `$event: {isSuccess: true, content: string}` 106 | 107 | ```html 108 | 109 | ``` 110 | 111 | Or updating parameters directly like so 112 | 113 | ```html 114 | 115 | ``` 116 | 117 | - `cbOnError` callback attribute is triggered when there's failure in copying with `$event:{isSuccess: false}` 118 | 119 | ### Conditionally render host 120 | 121 | You can also use the structural directive \*ngxClipboardIfSupported to conditionally render the host element 122 | 123 | ```html 124 | 127 | ``` 128 | 129 | _Special thanks to @surajpoddar16 for implementing this feature_ 130 | 131 | ### Handle copy response globally 132 | 133 | To handle copy response globally, you can subscribe to `copyResponse$` exposed by the `ClipboardService` 134 | 135 | ``` 136 | export class ClipboardResponseService { 137 | constructor( 138 | private _clipboardService: ClipboardService, 139 | private _toasterService: ToasterService 140 | ) { 141 | this.handleClipboardResponse(); 142 | } 143 | 144 | handleClipboardResponse() { 145 | this._clipboardService.copyResponse$.subscribe((res: IClipboardResponse) => { 146 | if (res.isSuccess) { 147 | this._toasterService.pop('success', undefined, res.successMessage); 148 | } 149 | }); 150 | } 151 | } 152 | ``` 153 | 154 | _Special thanks to @surajpoddar16 for implementing this feature_ 155 | 156 | ### Clean up temporary textarea used by this module after each copy to clipboard 157 | 158 | This library creates a textarea element at the root of the body for its internal use. By default it only destroys it when the directive is destroyed. If you'd like it to be destroyed after each copy to clipboard, provide root level module configuration like this: 159 | 160 | ```ts 161 | ClipboardService.configure({ cleanUpAfterCopy: true }); 162 | ``` 163 | 164 | Special thanks to [@DmitryEfimenko](https://github.com/DmitryEfimenko) for implementing this feature 165 | 166 | ## Example 167 | 168 | [stackblitz.com](https://stackblitz.com/github/maxisam/ngx-clipboard) 169 | 170 | ## Build project 171 | 172 | ```cmd 173 | npm i && npm run build 174 | ``` 175 | 176 | To run demo code locally 177 | 178 | `npm run start` 179 | 180 | ## Contributing 181 | 182 | - Your commits conform to the conventions established [here](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md) 183 | 184 | ## Troubleshooting 185 | 186 | Please ask your general questions at https://stackoverflow.com/questions/tagged/ngx-clipboard 187 | 188 | ## Shoutouts 🙏 189 | 190 | Kudos to 191 | 192 | [Thierry Templier](http://stackoverflow.com/a/36330518/667767) This project is inspired by his answer on StackOverflow. 193 | 194 | The core function is ported from [clipboard.js](http://zenorocha.github.io/clipboard.js/) by [@zenorocha](https://twitter.com/zenorocha). 195 | 196 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7. 197 | 198 | BrowserStack Logo 199 | 200 | Big thanks to [BrowserStack](https://www.browserstack.com) for letting the maintainers use their service to debug browser issues. 201 | 202 | ## Contributors 203 | 204 | ### Code Contributors 205 | 206 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 207 | 208 | 209 | ### Financial Contributors 210 | 211 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/ngx-clipboard/contribute)] 212 | 213 | #### Individuals 214 | 215 | 216 | 217 | #### Organizations 218 | 219 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/ngx-clipboard/contribute)] 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "library-host": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/library-host", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": ["src/favicon.ico", "src/assets"], 22 | "styles": ["src/styles.css"], 23 | "scripts": [], 24 | "vendorChunk": true, 25 | "extractLicenses": false, 26 | "buildOptimizer": false, 27 | "sourceMap": true, 28 | "optimization": false, 29 | "namedChunks": true 30 | }, 31 | "configurations": { 32 | "production": { 33 | "budgets": [ 34 | { 35 | "type": "anyComponentStyle", 36 | "maximumWarning": "6kb" 37 | } 38 | ], 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true 52 | } 53 | }, 54 | "defaultConfiguration": "" 55 | }, 56 | "serve": { 57 | "builder": "@angular-devkit/build-angular:dev-server", 58 | "options": { 59 | "browserTarget": "library-host:build" 60 | }, 61 | "configurations": { 62 | "production": { 63 | "browserTarget": "library-host:build:production" 64 | } 65 | } 66 | }, 67 | "extract-i18n": { 68 | "builder": "@angular-devkit/build-angular:extract-i18n", 69 | "options": { 70 | "browserTarget": "library-host:build" 71 | } 72 | }, 73 | "test": { 74 | "builder": "@angular-devkit/build-angular:karma", 75 | "options": { 76 | "main": "src/test.ts", 77 | "polyfills": "src/polyfills.ts", 78 | "tsConfig": "tsconfig.spec.json", 79 | "karmaConfig": "src/karma.conf.js", 80 | "styles": ["src/styles.css"], 81 | "scripts": [], 82 | "assets": ["src/favicon.ico", "src/assets"] 83 | } 84 | }, 85 | "lint": { 86 | "builder": "@angular-eslint/builder:lint", 87 | "options": { 88 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 89 | } 90 | } 91 | } 92 | }, 93 | "library-host-e2e": { 94 | "root": "e2e/", 95 | "projectType": "application", 96 | "architect": { 97 | "e2e": { 98 | "builder": "@angular-devkit/build-angular:protractor", 99 | "options": { 100 | "protractorConfig": "e2e/protractor.conf.js", 101 | "devServerTarget": "library-host:serve" 102 | } 103 | } 104 | } 105 | }, 106 | "ngx-clipboard": { 107 | "root": "projects/ngx-clipboard", 108 | "sourceRoot": "projects/ngx-clipboard/src", 109 | "projectType": "library", 110 | "prefix": "ngx", 111 | "architect": { 112 | "build": { 113 | "builder": "@angular-devkit/build-angular:ng-packagr", 114 | "options": { 115 | "tsConfig": "projects/ngx-clipboard/tsconfig.lib.json", 116 | "project": "projects/ngx-clipboard/ng-package.json" 117 | }, 118 | "configurations": { 119 | "production": { 120 | "project": "projects/ngx-clipboard/ng-package.prod.json", 121 | "tsConfig": "projects/ngx-clipboard/tsconfig.lib.prod.json" 122 | } 123 | } 124 | }, 125 | "test": { 126 | "builder": "@angular-devkit/build-angular:karma", 127 | "options": { 128 | "main": "projects/ngx-clipboard/src/test.ts", 129 | "tsConfig": "projects/ngx-clipboard/tsconfig.spec.json", 130 | "karmaConfig": "projects/ngx-clipboard/karma.conf.js" 131 | }, 132 | "configurations": { 133 | "ci": { 134 | "watch": false, 135 | "progress": false, 136 | "browsers": "ChromeHeadlessCI" 137 | } 138 | } 139 | }, 140 | "lint": { 141 | "builder": "@angular-eslint/builder:lint", 142 | "options": { 143 | "lintFilePatterns": ["projects/ngx-clipboard/**/*.ts", "projects/ngx-clipboard/**/*.html"] 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "cli": { 150 | "packageManager": "yarn", 151 | "analytics": "26cce57c-232f-4bfb-ac75-bfc72a238bd7", 152 | "schematicCollections": ["@angular-eslint/schematics"] 153 | }, 154 | "schematics": { 155 | "@angular-eslint/schematics:application": { 156 | "setParserOptionsProject": true 157 | }, 158 | "@angular-eslint/schematics:library": { 159 | "setParserOptionsProject": true 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: ['./src/**/*.e2e-spec.ts'], 9 | capabilities: { 10 | browserName: 'chrome' 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function () {} 19 | }, 20 | onPrepare() { 21 | require('ts-node').register({ 22 | project: require('path').join(__dirname, './tsconfig.e2e.json') 23 | }); 24 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": ["jasmine", "jasminewd2", "node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library-host", 3 | "version": "0.0.0", 4 | "engines": { 5 | "node": ">=14.20.0" 6 | }, 7 | "scripts": { 8 | "prepare": "husky install", 9 | "ng": "ng", 10 | "prettier": "prettier --write \"**/*.{json,md,ts,html,component.html}\"", 11 | "start": "ng serve", 12 | "build": "ng build ngx-clipboard --configuration production && yarn build-copy", 13 | "tslint-check": "tslint-config-prettier-check ./tslint.json", 14 | "test": "ng test ngx-clipboard", 15 | "test:watch": "ng test ngx-clipboard --watch", 16 | "lint": "ng lint ngx-clipboard", 17 | "publish:lib": "yarn publish ./dist/lib", 18 | "publish:lib:next": "yarn publish ./dist/lib --tag next", 19 | "build-copy": "cpx \"./README.md\" \"./dist/lib\"", 20 | "e2e": "ng e2e" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular/animations": "^15.1.4", 25 | "@angular/common": "^15.1.4", 26 | "@angular/compiler": "^15.1.4", 27 | "@angular/core": "^15.1.4", 28 | "@angular/forms": "^15.1.4", 29 | "@angular/platform-browser": "^15.1.4", 30 | "@angular/platform-browser-dynamic": "^15.1.4", 31 | "@angular/router": "^15.1.4", 32 | "ngx-clipboard": "15.0.0", 33 | "ngx-window-token": "7.0.0", 34 | "rxjs": "~7.4.0", 35 | "tslib": "^2.3.1", 36 | "zone.js": "~0.11.4" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^15.1.5", 40 | "@angular-eslint/builder": "15.2.1", 41 | "@angular-eslint/eslint-plugin": "15.2.1", 42 | "@angular-eslint/eslint-plugin-template": "15.2.1", 43 | "@angular-eslint/schematics": "15.2.1", 44 | "@angular-eslint/template-parser": "15.2.1", 45 | "@angular/cli": "^15.1.5", 46 | "@angular/compiler-cli": "^15.1.4", 47 | "@angular/language-service": "^15.1.4", 48 | "@types/jasmine": "~3.10.2", 49 | "@types/jasminewd2": "~2.0.10", 50 | "@types/node": "^16.11.7", 51 | "@typescript-eslint/eslint-plugin": "^5.43.0", 52 | "@typescript-eslint/parser": "^5.43.0", 53 | "cpx": "^1.5.0", 54 | "eslint": "^8.28.0", 55 | "eslint-config-prettier": "^8.3.0", 56 | "eslint-plugin-prettier": "^4.0.0", 57 | "husky": "8.0.3", 58 | "jasmine-core": "~3.10.1", 59 | "jasmine-spec-reporter": "~7.0.0", 60 | "karma": "~6.3.9", 61 | "karma-chrome-launcher": "~3.1.0", 62 | "karma-coverage-istanbul-reporter": "~3.0.3", 63 | "karma-jasmine": "~4.0.1", 64 | "karma-jasmine-html-reporter": "^1.7.0", 65 | "ng-packagr": "^15.1.2", 66 | "lint-staged": "13.1.1", 67 | "prettier": "2.4.1", 68 | "protractor": "~7.0.0", 69 | "ts-node": "~10.4.0", 70 | "typescript": "~4.8.4" 71 | }, 72 | "lint-staged": { 73 | "*.{json,md,ts,html,component.html}": [ 74 | "prettier --write" 75 | ] 76 | }, 77 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 78 | } 79 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["projects/ngx-clipboard/tsconfig.lib.json", "projects/ngx-clipboard/tsconfig.spec.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeHeadlessCI: { 31 | base: 'ChromeHeadless', 32 | flags: ['--no-sandbox', '--disable-gpu'] 33 | } 34 | }, 35 | singleRun: true 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/lib", 4 | "deleteDestPath": false, 5 | "allowedNonPeerDependencies": ["ngx-window-token"], 6 | "lib": { 7 | "entryFile": "src/public_api.ts" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/lib", 4 | "allowedNonPeerDependencies": ["ngx-window-token"], 5 | "lib": { 6 | "entryFile": "src/public_api.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-clipboard", 3 | "description": "angular 2 clipboard", 4 | "version": "16.1.1", 5 | "author": { 6 | "name": "Sam Lin", 7 | "email": "maxisam@gmail.com" 8 | }, 9 | "homepage": "https://github.com/maxisam/ngx-clipboard", 10 | "license": "MIT", 11 | "publishConfig": { 12 | "registry": "https://registry.npmjs.org/", 13 | "access": "public" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/maxisam/ngx-clipboard" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/maxisam/ngx-clipboard/issues" 21 | }, 22 | "keywords": [ 23 | "angular", 24 | "clipboard", 25 | "copy" 26 | ], 27 | "dependencies": { 28 | "ngx-window-token": ">=7.0.0", 29 | "tslib": "^2.0.0" 30 | }, 31 | "peerDependencies": { 32 | "@angular/common": ">=13.0.0", 33 | "@angular/core": ">=13.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/interface.ts: -------------------------------------------------------------------------------- 1 | export interface IClipboardResponse { 2 | isSuccess: boolean; 3 | content?: string; 4 | event?: Event; 5 | successMessage?: string; 6 | } 7 | 8 | export interface ClipboardParams { 9 | cleanUpAfterCopy?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard-if-supported.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { ClipboardModule } from './ngx-clipboard.module'; 5 | import { ClipboardService } from './ngx-clipboard.service'; 6 | 7 | @Component({ 8 | // eslint-disable-next-line @angular-eslint/component-selector 9 | selector: 'test-cmp', 10 | template: ` Copy Foo Bar ` 11 | }) 12 | class TestComponent {} 13 | 14 | function createTestComponent(): ComponentFixture { 15 | return TestBed.createComponent(TestComponent); 16 | } 17 | 18 | describe('ngxClipboardIfSupported directive', () => { 19 | let fixture: ComponentFixture; 20 | let clipboardService: ClipboardService; 21 | let spy: jasmine.Spy; 22 | 23 | beforeEach(() => { 24 | TestBed.configureTestingModule({ 25 | declarations: [TestComponent], 26 | imports: [ClipboardModule] 27 | }); 28 | 29 | clipboardService = TestBed.get(ClipboardService); 30 | fixture = createTestComponent(); 31 | spy = spyOnProperty(clipboardService, 'isSupported', 'get'); 32 | }); 33 | 34 | it( 35 | 'should not render host when copy is not supported', 36 | waitForAsync(() => { 37 | spy.and.returnValue(false); 38 | fixture.detectChanges(); 39 | expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(0); 40 | }) 41 | ); 42 | 43 | it( 44 | 'should render host when copy is supported', 45 | waitForAsync(() => { 46 | spy.and.returnValue(true); 47 | fixture.detectChanges(); 48 | expect(fixture.debugElement.queryAll(By.css('span')).length).toEqual(1); 49 | }) 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard-if-supported.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; 2 | 3 | import { ClipboardService } from './ngx-clipboard.service'; 4 | 5 | @Directive({ 6 | selector: '[ngxClipboardIfSupported]' 7 | }) 8 | export class ClipboardIfSupportedDirective implements OnInit { 9 | constructor( 10 | private _clipboardService: ClipboardService, 11 | private _viewContainerRef: ViewContainerRef, 12 | private _templateRef: TemplateRef 13 | ) {} 14 | 15 | ngOnInit() { 16 | if (this._clipboardService.isSupported) { 17 | this._viewContainerRef.createEmbeddedView(this._templateRef); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | 7 | import { IClipboardResponse } from './interface'; 8 | import { ClipboardModule } from './ngx-clipboard.module'; 9 | import { ClipboardService } from './ngx-clipboard.service'; 10 | 11 | /* 12 | * Shell component with property 'text' that will be used with our tests 13 | */ 14 | @Component({ 15 | // eslint-disable-next-line @angular-eslint/component-selector 16 | selector: 'test-clipboard', 17 | template: ` PlaceHolder HTML to be Replaced ` 18 | }) 19 | export class TestClipboardComponent { 20 | public text = 'text'; 21 | public isCopied: boolean; 22 | public copySuccessMsg = 'copySuccessMsg'; 23 | } 24 | 25 | /** 26 | * Helper function to easily build a component Fixture using the specified template 27 | * From: https://blog.thoughtram.io/angular/2016/12/27/angular-2-advance-testing-with-custom-matchers.html 28 | */ 29 | function createTestComponent(template: string): ComponentFixture { 30 | return TestBed.overrideComponent(TestClipboardComponent, { 31 | set: { template } 32 | }).createComponent(TestClipboardComponent); 33 | } 34 | 35 | describe('Directive: clipboard', () => { 36 | describe('GIVEN: no root configuration', () => { 37 | beforeEach(() => { 38 | TestBed.configureTestingModule({ 39 | declarations: [TestClipboardComponent], 40 | imports: [BrowserModule, ClipboardModule, FormsModule], 41 | providers: [ClipboardService] 42 | }); 43 | }); 44 | 45 | describe('copy when cbContent is set', () => { 46 | let template: string; 47 | let fixture: ComponentFixture; 48 | let clipboardService: ClipboardService; 49 | let spy: jasmine.Spy; 50 | let button: HTMLButtonElement; 51 | beforeEach(() => { 52 | template = ``; 53 | fixture = createTestComponent(template); 54 | clipboardService = fixture.debugElement.injector.get(ClipboardService); 55 | // Setup spy on the `copyText` method, somehow document.execCommand('copy') doesn't work in Karma 56 | spy = spyOn(clipboardService, 'copyText' as keyof ClipboardService); 57 | fixture.detectChanges(); 58 | button = fixture.debugElement.nativeElement.querySelector('button'); 59 | }); 60 | 61 | it( 62 | 'should fire cbOnError if environment does not support copy', 63 | waitForAsync(() => { 64 | spy = spyOnProperty(clipboardService, 'isSupported', 'get').and.returnValue(false); 65 | button.click(); 66 | fixture.whenStable().then(() => { 67 | expect(fixture.componentInstance.isCopied).toBeFalsy(); 68 | }); 69 | }) 70 | ); 71 | 72 | it( 73 | 'should fire cbOnSuccess after copy successfully', 74 | waitForAsync(() => { 75 | spy.and.returnValue(true); 76 | button.click(); 77 | fixture.whenStable().then(() => { 78 | expect(fixture.componentInstance.isCopied).toBeTruthy(); 79 | }); 80 | }) 81 | ); 82 | 83 | it( 84 | 'should fire cbOnError after copy fail', 85 | waitForAsync(() => { 86 | button.click(); 87 | fixture.whenStable().then(() => { 88 | expect(fixture.componentInstance.isCopied).toBeFalsy(); 89 | }); 90 | }) 91 | ); 92 | 93 | it( 94 | 'should create a textarea in dom, and remove it after calling destroy', 95 | waitForAsync(() => { 96 | const doc = fixture.debugElement.injector.get(DOCUMENT); 97 | expect(doc.querySelector('textarea')).toBeFalsy(); 98 | button.click(); 99 | fixture.whenStable().then(() => { 100 | expect(doc.querySelector('textarea')).toBeTruthy(); 101 | clipboardService.destroy(doc.body); 102 | expect(doc.querySelector('textarea')).toBeFalsy(); 103 | }); 104 | }) 105 | ); 106 | 107 | it( 108 | 'given the configuration it should clean up temp textarea after copying automatically', 109 | waitForAsync(() => { 110 | const doc = fixture.debugElement.injector.get(DOCUMENT); 111 | clipboardService.configure({ cleanUpAfterCopy: true }); 112 | clipboardService.copyFromContent('test content'); 113 | fixture.whenStable().then(() => { 114 | const ta = doc.querySelector('textarea'); 115 | expect(ta).toBeFalsy(); 116 | }); 117 | }) 118 | ); 119 | 120 | it( 121 | 'should push copy response to copySubject', 122 | waitForAsync(() => { 123 | spy.and.returnValue(true); 124 | const component = fixture.componentInstance; 125 | clipboardService.copyResponse$.subscribe((res: IClipboardResponse) => { 126 | expect(res).toBeDefined(); 127 | expect(res.isSuccess).toEqual(true); 128 | expect(res.content).toEqual(component.text); 129 | expect(res.successMessage).toEqual(component.copySuccessMsg); 130 | expect(res.event).toBeDefined(); 131 | }); 132 | button.click(); 133 | }) 134 | ); 135 | }); 136 | 137 | describe('copy when cbOnSuccess is not set', () => { 138 | let template: string; 139 | let fixture: ComponentFixture; 140 | let clipboardService: ClipboardService; 141 | let spy: jasmine.Spy; 142 | let button: HTMLButtonElement; 143 | beforeEach(() => { 144 | template = ``; 145 | fixture = createTestComponent(template); 146 | clipboardService = fixture.debugElement.injector.get(ClipboardService); 147 | // Setup spy on the `copyText` method, somehow document.execCommand('copy') doesn't work in Karma 148 | spy = spyOn(clipboardService, 'copyText' as keyof ClipboardService); 149 | fixture.detectChanges(); 150 | button = fixture.debugElement.nativeElement.querySelector('button'); 151 | }); 152 | 153 | it( 154 | 'should not fire cbOnSuccess after copy successfully', 155 | waitForAsync(() => { 156 | spy.and.returnValue(true); 157 | button.click(); 158 | fixture.whenStable().then(() => { 159 | expect(fixture.componentInstance.isCopied).toBeFalsy(); 160 | }); 161 | }) 162 | ); 163 | 164 | it( 165 | 'should push copy response to copySubject', 166 | waitForAsync(() => { 167 | spy.and.returnValue(true); 168 | const component = fixture.componentInstance; 169 | clipboardService.copyResponse$.subscribe((res: IClipboardResponse) => { 170 | expect(res).toBeDefined(); 171 | expect(res.isSuccess).toEqual(true); 172 | expect(res.content).toEqual(component.text); 173 | expect(res.successMessage).toEqual(component.copySuccessMsg); 174 | expect(res.event).toBeDefined(); 175 | }); 176 | button.click(); 177 | }) 178 | ); 179 | }); 180 | 181 | describe('copy when cbContent and container is set', () => { 182 | let template: string; 183 | let fixture: ComponentFixture; 184 | let clipboardService: ClipboardService; 185 | let spy: jasmine.Spy; 186 | let button: HTMLButtonElement; 187 | beforeEach(() => { 188 | template = `
`; 189 | fixture = createTestComponent(template); 190 | clipboardService = fixture.debugElement.injector.get(ClipboardService); 191 | // Setup spy on the `copyText` method, somehow document.execCommand('copy') doesn't work in Karma 192 | spy = spyOn(clipboardService, 'copyText' as keyof ClipboardService); 193 | fixture.detectChanges(); 194 | button = fixture.debugElement.nativeElement.querySelector('button'); 195 | }); 196 | 197 | it( 198 | 'should create a textarea in dom, and remove it after calling destroy', 199 | waitForAsync(() => { 200 | const doc = fixture.debugElement.injector.get(DOCUMENT); 201 | expect(doc.querySelector('textarea')).toBeFalsy(); 202 | button.click(); 203 | fixture.whenStable().then(() => { 204 | const ta = doc.querySelector('textarea'); 205 | expect(ta).toBeTruthy(); 206 | expect(ta!.parentElement!.className).toBe('container'); 207 | clipboardService.destroy(ta!.parentElement!); 208 | expect(doc.querySelector('textarea')).toBeFalsy(); 209 | }); 210 | }) 211 | ); 212 | }); 213 | 214 | describe('copy when using copyFromContent directly', () => { 215 | let template: string; 216 | let fixture: ComponentFixture; 217 | let clipboardService: ClipboardService; 218 | let spy: jasmine.Spy; 219 | let button: HTMLButtonElement; 220 | beforeEach(() => { 221 | template = `
`; 222 | fixture = createTestComponent(template); 223 | clipboardService = fixture.debugElement.injector.get(ClipboardService); 224 | // Setup spy on the `copyText` method, somehow document.execCommand('copy') doesn't work in Karma 225 | spy = spyOn(clipboardService, 'copyText' as keyof ClipboardService); 226 | fixture.detectChanges(); 227 | button = fixture.debugElement.nativeElement.querySelector('button'); 228 | }); 229 | 230 | it( 231 | 'should create a textarea in dom with parent as body, and remove it after calling destroy', 232 | waitForAsync(() => { 233 | const doc = fixture.debugElement.injector.get(DOCUMENT); 234 | expect(doc.querySelector('textarea')).toBeFalsy(); 235 | clipboardService.copyFromContent('test content'); 236 | fixture.whenStable().then(() => { 237 | const ta = doc.querySelector('textarea'); 238 | expect(ta).toBeTruthy(); 239 | expect(ta!.parentElement!.nodeName).toBe('BODY'); 240 | clipboardService.destroy(ta!.parentElement!); 241 | expect(doc.querySelector('textarea')).toBeFalsy(); 242 | }); 243 | }) 244 | ); 245 | }); 246 | 247 | describe('copy when target is set', () => { 248 | let template: string; 249 | let fixture: ComponentFixture; 250 | let clipboardService: ClipboardService; 251 | let spy: jasmine.Spy; 252 | let button: HTMLButtonElement; 253 | let input: HTMLInputElement; 254 | beforeEach(() => { 255 | template = ` 256 | `; 257 | fixture = createTestComponent(template); 258 | clipboardService = fixture.debugElement.injector.get(ClipboardService); 259 | // Setup spy on the `copyText` method, somehow document.execCommand('copy') doesn't work in Karma 260 | spy = spyOn(clipboardService, 'copyText' as keyof ClipboardService); 261 | fixture.detectChanges(); 262 | button = fixture.debugElement.nativeElement.querySelector('button'); 263 | input = fixture.debugElement.nativeElement.querySelector('input'); 264 | // input 'new test' 265 | input.value = 'new test'; 266 | input.dispatchEvent(new Event('input')); 267 | }); 268 | 269 | it( 270 | 'should fire cbOnSuccess after copy successfully', 271 | waitForAsync(() => { 272 | spy.and.returnValue(true); 273 | fixture.detectChanges(); 274 | // button click to trigger copy 275 | button.click(); 276 | fixture.whenStable().then(() => { 277 | expect(fixture.componentInstance.isCopied).toBeTruthy(); 278 | }); 279 | }) 280 | ); 281 | 282 | it( 283 | 'should fire cbOnError if environment does not support copy', 284 | waitForAsync(() => { 285 | spy = spyOnProperty(clipboardService, 'isSupported', 'get').and.returnValue(false); 286 | button.click(); 287 | fixture.whenStable().then(() => { 288 | expect(fixture.componentInstance.isCopied).toBeFalsy(); 289 | }); 290 | }) 291 | ); 292 | 293 | it( 294 | 'should fire cbOnError after copy fail', 295 | waitForAsync(() => { 296 | spy.and.returnValue(false); 297 | fixture.detectChanges(); 298 | // button click to trigger copy 299 | button.click(); 300 | fixture.whenStable().then(() => { 301 | expect(fixture.componentInstance.isCopied).toBeFalsy(); 302 | }); 303 | }) 304 | ); 305 | }); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | NgZone, 7 | OnDestroy, 8 | OnInit, 9 | Output, 10 | Renderer2 11 | } from '@angular/core'; 12 | import { IClipboardResponse } from './interface'; 13 | import { ClipboardService } from './ngx-clipboard.service'; 14 | 15 | @Directive({ selector: '[ngxClipboard]' }) 16 | export class ClipboardDirective implements OnInit, OnDestroy { 17 | // https://github.com/maxisam/ngx-clipboard/issues/239#issuecomment-623330624 18 | // eslint-disable-next-line @angular-eslint/no-input-rename 19 | @Input('ngxClipboard') 20 | public targetElm: HTMLInputElement | HTMLTextAreaElement | undefined | ''; 21 | @Input() 22 | public container: HTMLElement; 23 | 24 | @Input() 25 | public cbContent: string | undefined; 26 | 27 | @Input() 28 | public cbSuccessMsg: string; 29 | 30 | @Output() 31 | public cbOnSuccess: EventEmitter = new EventEmitter(); 32 | 33 | @Output() 34 | public cbOnError: EventEmitter = new EventEmitter(); 35 | 36 | private clickListener: () => void; 37 | 38 | constructor( 39 | private ngZone: NgZone, 40 | private host: ElementRef, 41 | private renderer: Renderer2, 42 | private clipboardSrv: ClipboardService 43 | ) {} 44 | 45 | // eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function 46 | public ngOnInit() { 47 | this.ngZone.runOutsideAngular(() => { 48 | // By default each host listener schedules change detection and also wrapped 49 | // into additional function that calls `markForCheck()`. We're listening the `click` 50 | // event in the context of the root zone to avoid running unnecessary change detections, 51 | // since this directive doesn't do anything template-related (e.g. updates template variables). 52 | this.clickListener = this.renderer.listen(this.host.nativeElement, 'click', this.onClick); 53 | }); 54 | } 55 | 56 | public ngOnDestroy() { 57 | if (this.clickListener) { 58 | this.clickListener(); 59 | } 60 | this.clipboardSrv.destroy(this.container); 61 | } 62 | 63 | private onClick = (event: MouseEvent): void => { 64 | if (!this.clipboardSrv.isSupported) { 65 | this.handleResult(false, undefined, event); 66 | } else if (this.targetElm && this.clipboardSrv.isTargetValid(this.targetElm)) { 67 | this.handleResult(this.clipboardSrv.copyFromInputElement(this.targetElm), this.targetElm.value, event); 68 | } else if (this.cbContent) { 69 | this.handleResult(this.clipboardSrv.copyFromContent(this.cbContent, this.container), this.cbContent, event); 70 | } 71 | }; 72 | 73 | /** 74 | * Fires an event based on the copy operation result. 75 | * @param succeeded 76 | */ 77 | private handleResult(succeeded: boolean, copiedContent: string | undefined, event: MouseEvent): void { 78 | let response: IClipboardResponse = { 79 | isSuccess: succeeded, 80 | content: copiedContent, 81 | successMessage: this.cbSuccessMsg, 82 | event 83 | }; 84 | 85 | if (succeeded) { 86 | if (this.cbOnSuccess.observed) { 87 | this.ngZone.run(() => { 88 | this.cbOnSuccess.emit(response); 89 | }); 90 | } 91 | } else { 92 | if (this.cbOnError.observed) { 93 | this.ngZone.run(() => { 94 | this.cbOnError.emit(response); 95 | }); 96 | } 97 | } 98 | 99 | this.clipboardSrv.pushCopyResponse(response); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { ClipboardIfSupportedDirective } from './ngx-clipboard-if-supported.directive'; 5 | import { ClipboardDirective } from './ngx-clipboard.directive'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule], 9 | declarations: [ClipboardDirective, ClipboardIfSupportedDirective], 10 | exports: [ClipboardDirective, ClipboardIfSupportedDirective] 11 | }) 12 | export class ClipboardModule {} 13 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { ClipboardService } from './ngx-clipboard.service'; 5 | 6 | describe('Service: Clipboard', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | imports: [BrowserModule] 10 | }); 11 | }); 12 | 13 | it('should service work', inject([ClipboardService], (service: ClipboardService) => { 14 | expect(service).toBeTruthy(); 15 | })); 16 | 17 | it('it is supported', inject([ClipboardService], (service: ClipboardService) => { 18 | expect(service.isSupported).toBeTruthy(); 19 | })); 20 | 21 | describe('check if input is valid', () => { 22 | let input: HTMLInputElement; 23 | beforeEach(() => { 24 | input = document.createElement('input'); 25 | }); 26 | 27 | it('input is a valid target', inject([ClipboardService], (service: ClipboardService) => { 28 | expect(service.isTargetValid(input)).toBeTruthy(); 29 | })); 30 | 31 | it('input[disabled] is NOT a valid target', inject([ClipboardService], (service: ClipboardService) => { 32 | input.setAttribute('disabled', ''); 33 | expect(() => service.isTargetValid(input)).toThrowError( 34 | 'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute' 35 | ); 36 | })); 37 | }); 38 | 39 | describe('check if textarea is valid', () => { 40 | let ta: HTMLTextAreaElement; 41 | beforeEach(() => { 42 | ta = document.createElement('textarea'); 43 | }); 44 | 45 | it('textarea is a valid target', inject([ClipboardService], (service: ClipboardService) => { 46 | expect(service.isTargetValid(ta)).toBeTruthy(); 47 | })); 48 | 49 | it('ta[disabled] is NOT a valid target', inject([ClipboardService], (service: ClipboardService) => { 50 | ta.setAttribute('disabled', ''); 51 | expect(() => service.isTargetValid(ta)).toThrowError( 52 | 'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute' 53 | ); 54 | })); 55 | }); 56 | 57 | describe('check if other html element is valid', () => { 58 | let div: any; 59 | it('undefined is NOT a valid target', inject([ClipboardService], (service: ClipboardService) => { 60 | expect(() => service.isTargetValid(div)).toThrowError('Target should be input or textarea'); 61 | })); 62 | it('div is NOT a valid target', inject([ClipboardService], (service: ClipboardService) => { 63 | div = document.createElement('div'); 64 | expect(() => service.isTargetValid(div)).toThrowError('Target should be input or textarea'); 65 | })); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/lib/ngx-clipboard.service.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Inject, Injectable, NgZone, Optional } from '@angular/core'; 3 | import { WINDOW } from 'ngx-window-token'; 4 | import { Observable, Subject } from 'rxjs'; 5 | 6 | import { ClipboardParams, IClipboardResponse } from './interface'; 7 | 8 | /** 9 | * The following code is heavily copied from https://github.com/zenorocha/clipboard.js 10 | */ 11 | @Injectable({ providedIn: 'root' }) 12 | export class ClipboardService { 13 | private copySubject = new Subject(); 14 | public copyResponse$: Observable = this.copySubject.asObservable(); 15 | private tempTextArea: HTMLTextAreaElement | undefined; 16 | private config: ClipboardParams = {}; 17 | 18 | constructor( 19 | private ngZone: NgZone, 20 | @Inject(DOCUMENT) public document: any, 21 | @Optional() @Inject(WINDOW) private window: any 22 | ) {} 23 | 24 | public configure(config: ClipboardParams) { 25 | this.config = config; 26 | } 27 | 28 | public copy(content: string): void { 29 | if (!this.isSupported || !content) { 30 | return this.pushCopyResponse({ isSuccess: false, content }); 31 | } 32 | const copyResult = this.copyFromContent(content); 33 | if (copyResult) { 34 | return this.pushCopyResponse({ content, isSuccess: copyResult }); 35 | } 36 | return this.pushCopyResponse({ isSuccess: false, content }); 37 | } 38 | 39 | public get isSupported(): boolean { 40 | return !!this.document.queryCommandSupported && !!this.document.queryCommandSupported('copy') && !!this.window; 41 | } 42 | 43 | public isTargetValid(element: HTMLInputElement | HTMLTextAreaElement): boolean { 44 | if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { 45 | if (element.hasAttribute('disabled')) { 46 | throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'); 47 | } 48 | return true; 49 | } 50 | throw new Error('Target should be input or textarea'); 51 | } 52 | 53 | /** 54 | * Attempts to copy from an input `targetElm` 55 | */ 56 | public copyFromInputElement(targetElm: HTMLInputElement | HTMLTextAreaElement, isFocus = true): boolean { 57 | try { 58 | this.selectTarget(targetElm); 59 | const re = this.copyText(); 60 | this.clearSelection(isFocus ? targetElm : undefined, this.window); 61 | return re && this.isCopySuccessInIE11(); 62 | } catch (error) { 63 | return false; 64 | } 65 | } 66 | 67 | /** 68 | * This is a hack for IE11 to return `true` even if copy fails. 69 | */ 70 | public isCopySuccessInIE11(): boolean { 71 | const clipboardData = this.window['clipboardData']; 72 | if (clipboardData && clipboardData.getData) { 73 | if (!clipboardData.getData('Text')) { 74 | return false; 75 | } 76 | } 77 | return true; 78 | } 79 | 80 | /** 81 | * Creates a fake textarea element, sets its value from `text` property, 82 | * and makes a selection on it. 83 | */ 84 | public copyFromContent(content: string, container: HTMLElement = this.document.body): boolean { 85 | // check if the temp textarea still belongs to the current container. 86 | // In case we have multiple places using ngx-clipboard, one is in a modal using container but the other one is not. 87 | if (this.tempTextArea && !container.contains(this.tempTextArea)) { 88 | this.destroy(this.tempTextArea.parentElement || undefined); 89 | } 90 | 91 | if (!this.tempTextArea) { 92 | this.tempTextArea = this.createTempTextArea(this.document, this.window); 93 | try { 94 | container.appendChild(this.tempTextArea); 95 | } catch (error) { 96 | throw new Error('Container should be a Dom element'); 97 | } 98 | } 99 | this.tempTextArea.value = content; 100 | 101 | const toReturn = this.copyFromInputElement(this.tempTextArea, false); 102 | if (this.config.cleanUpAfterCopy) { 103 | this.destroy(this.tempTextArea.parentElement || undefined); 104 | } 105 | return toReturn; 106 | } 107 | 108 | /** 109 | * Remove temporary textarea if any exists. 110 | */ 111 | public destroy(container: HTMLElement = this.document.body): void { 112 | if (this.tempTextArea) { 113 | container.removeChild(this.tempTextArea); 114 | // removeChild doesn't remove the reference from memory 115 | this.tempTextArea = undefined; 116 | } 117 | } 118 | 119 | /** 120 | * Select the target html input element. 121 | */ 122 | private selectTarget(inputElement: HTMLInputElement | HTMLTextAreaElement): number | undefined { 123 | inputElement.select(); 124 | inputElement.setSelectionRange(0, inputElement.value.length); 125 | return inputElement.value.length; 126 | } 127 | 128 | private copyText(): boolean { 129 | return this.document.execCommand('copy'); 130 | } 131 | 132 | /** 133 | * Moves focus away from `target` and back to the trigger, removes current selection. 134 | */ 135 | private clearSelection(inputElement: HTMLInputElement | HTMLTextAreaElement | undefined, window: Window): void { 136 | inputElement && inputElement.focus(); 137 | window.getSelection()?.removeAllRanges(); 138 | } 139 | 140 | /** 141 | * Creates a fake textarea for copy command. 142 | */ 143 | private createTempTextArea(doc: Document, window: Window): HTMLTextAreaElement { 144 | const isRTL = doc.documentElement.getAttribute('dir') === 'rtl'; 145 | let ta: HTMLTextAreaElement; 146 | ta = doc.createElement('textarea'); 147 | // Prevent zooming on iOS 148 | ta.style.fontSize = '12pt'; 149 | // Reset box model 150 | ta.style.height = '1px'; 151 | ta.style.width = '1px'; 152 | ta.style.overflow = 'hidden'; 153 | ta.style.clip = 'rect(0 0 0 0);'; 154 | ta.style.border = '0'; 155 | ta.style.padding = '0'; 156 | ta.style.margin = '0'; 157 | // Move element out of screen horizontally 158 | ta.style.position = 'absolute'; 159 | ta.style[isRTL ? 'right' : 'left'] = '-9999px'; 160 | // Move element to the same position vertically 161 | const yPosition = window.pageYOffset || doc.documentElement.scrollTop; 162 | ta.style.top = yPosition + 'px'; 163 | ta.setAttribute('readonly', ''); 164 | ta.setAttribute('aria-hidden', 'true'); 165 | 166 | return ta; 167 | } 168 | 169 | /** 170 | * Pushes copy operation response to copySubject, to provide global access 171 | * to the response. 172 | */ 173 | public pushCopyResponse(response: IClipboardResponse): void { 174 | if (this.copySubject.observers.length > 0) { 175 | this.ngZone.run(() => { 176 | this.copySubject.next(response); 177 | }); 178 | } 179 | } 180 | 181 | /** 182 | * @deprecated use pushCopyResponse instead. 183 | */ 184 | public pushCopyReponse(response: IClipboardResponse): void { 185 | this.pushCopyResponse(response); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-clipboard 3 | */ 4 | 5 | export * from './lib/ngx-clipboard.service'; 6 | export * from './lib/ngx-clipboard.directive'; 7 | export * from './lib/ngx-clipboard.module'; 8 | export * from './lib/ngx-clipboard-if-supported.directive'; 9 | export * from './lib/interface'; 10 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | // The order of import matters 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 10 | teardown: { destroyAfterEach: false } 11 | }); 12 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": ["dom", "es2018"] 10 | }, 11 | "angularCompilerOptions": { 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "enableResourceInlining": true 15 | }, 16 | "exclude": ["src/test.ts", "**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-clipboard/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ngx-window-token@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/ngx-window-token/-/ngx-window-token-1.0.0.tgz#12acb174fbbcffa5c60b3fea5a6ea78cc3304793" 8 | dependencies: 9 | tslib "^1.9.0" 10 | 11 | tslib@^1.9.0: 12 | version "1.9.1" 13 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.1.tgz#a5d1f0532a49221c87755cfcc89ca37197242ba7" 14 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | font: 400 13.3333px Arial; 3 | } 4 | .row { 5 | margin: 1rem; 6 | } 7 | .input-group { 8 | position: relative; 9 | display: -ms-flexbox; 10 | display: flex; 11 | -ms-flex-wrap: wrap; 12 | flex-wrap: wrap; 13 | -ms-flex-align: stretch; 14 | align-items: stretch; 15 | width: 100%; 16 | } 17 | .form-control { 18 | position: relative; 19 | -ms-flex: 1 1 auto; 20 | flex: 1 1 auto; 21 | width: 1%; 22 | margin-bottom: 0; 23 | display: block; 24 | height: calc(2.25rem + 2px); 25 | padding: 0.375rem 0.75rem; 26 | font-size: 1rem; 27 | line-height: 1.5; 28 | color: #495057; 29 | background-color: #fff; 30 | background-clip: padding-box; 31 | border: 1px solid #ced4da; 32 | border-radius: 0.25rem; 33 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 34 | } 35 | .btn { 36 | display: inline-block; 37 | font-weight: 400; 38 | text-align: center; 39 | white-space: nowrap; 40 | vertical-align: middle; 41 | -webkit-user-select: none; 42 | -moz-user-select: none; 43 | -ms-user-select: none; 44 | user-select: none; 45 | border: 1px solid transparent; 46 | padding: 0.375rem 0.75rem; 47 | font-size: 1rem; 48 | line-height: 1.5; 49 | border-radius: 0.25rem; 50 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, 51 | box-shadow 0.15s ease-in-out; 52 | height: auto; 53 | background-color: #6c757d; 54 | color: #fff; 55 | margin: 0; 56 | } 57 | .btn-success { 58 | background-color: #266900; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Copy from text -- text

5 | Click this button, it will copy the text from the input by referring to the 6 | text content 7 |
8 | 14 | 15 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |

Copy from text -- target

33 | Click this button, it will copy the text from the input by referring to the 34 | input element 35 |
36 | 43 | 44 | 54 | 55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach( 5 | waitForAsync(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [AppComponent] 8 | }).compileComponents(); 9 | }) 10 | ); 11 | it( 12 | 'should create the app', 13 | waitForAsync(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }) 18 | ); 19 | it( 20 | `should have as title 'app'`, 21 | waitForAsync(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app.title).toEqual('app'); 25 | }) 26 | ); 27 | it( 28 | 'should render title in a h1 tag', 29 | waitForAsync(() => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 34 | }) 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ClipboardService } from 'ngx-clipboard'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | text1: string; 11 | text2: string; 12 | textModal: string; 13 | isCopied1: boolean; 14 | isCopied2: boolean; 15 | isCopied3: boolean; 16 | basic = false; 17 | constructor(private _clipboardService: ClipboardService) {} 18 | 19 | ngOnInit() { 20 | // Handle copy response globally https://github.com/maxisam/ngx-clipboard#handle-copy-response-globally 21 | this._clipboardService.copyResponse$.subscribe(re => { 22 | if (re.isSuccess) { 23 | alert('copy success!'); 24 | } 25 | }); 26 | } 27 | callServiceToCopy() { 28 | this._clipboardService.copy('This is copy thru service copyFromContent directly'); 29 | } 30 | 31 | onCopyFailure() { 32 | alert('copy fail!'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { ClipboardModule } from 'ngx-clipboard'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [BrowserAnimationsModule, FormsModule, ClipboardModule], 10 | providers: [], 11 | bootstrap: [AppComponent] 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisam/ngx-clipboard/a50d7440797677fcb25a583eb76d564f0210b8e4/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxisam/ngx-clipboard/a50d7440797677fcb25a583eb76d564f0210b8e4/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngxClipboard - AngularCli 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | Loading... 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeHeadlessCI: { 31 | base: 'ChromeHeadless', 32 | flags: ['--no-sandbox', '--disable-gpu'] 33 | } 34 | }, 35 | singleRun: false 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | import 'zone.js'; 17 | 18 | /*************************************************************************************************** 19 | * BROWSER POLYFILLS 20 | */ 21 | 22 | /*************************************************************************************************** 23 | * BROWSER POLYFILLS 24 | */ 25 | 26 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 27 | 28 | /** IE10 and IE11 requires the following for the Reflect API. */ 29 | /** Evergreen browsers require these. **/ 30 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 31 | 32 | /** 33 | * By default, zone.js will patch all possible macroTask and DomEvents 34 | * user can disable parts of macroTask/DomEvents patch by setting following flags 35 | */ 36 | 37 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 38 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 39 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 40 | 41 | /* 42 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 43 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 44 | */ 45 | // (window as any).__Zone_enable_cross_context_check = true; 46 | 47 | /*************************************************************************************************** 48 | * Zone JS is required by default for Angular itself. 49 | */ 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | // The order of import matters 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 6 | 7 | // First, initialize the Angular testing environment. 8 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 9 | teardown: { destroyAfterEach: false } 10 | }); 11 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "app", "camelCase"], 5 | "component-selector": [true, "element", "app", "kebab-case"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "strictNullChecks": true, 14 | "target": "ES2022", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["es2018", "dom"], 17 | "paths": {}, 18 | "useDefineForClassFields": false 19 | }, 20 | "angularCompilerOptions": { 21 | "fullTemplateTypeCheck": true, 22 | "strictInjectionParameters": true, 23 | "strictTemplates": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | --------------------------------------------------------------------------------