├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── bs-config.json ├── gulpfile.js ├── package.json ├── src ├── components │ └── select │ │ ├── select.component.html │ │ ├── select.component.scss │ │ └── select.component.ts ├── enums │ └── key-code.enum.ts ├── index.ts ├── package.json ├── tsconfig.es5.json └── tsconfig.spec.json ├── tools └── gulp │ └── inline-resources.js ├── tsconfig.json └── tslint.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Jaspero] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # TypeScript 6 | src/*.js 7 | src/*.map 8 | src/*.d.ts 9 | 10 | # JetBrains 11 | .idea 12 | .project 13 | .settings 14 | .idea/* 15 | *.iml 16 | 17 | # VS Code 18 | .vscode/* 19 | 20 | # Windows 21 | Thumbs.db 22 | Desktop.ini 23 | 24 | # Mac 25 | .DS_Store 26 | **/.DS_Store 27 | 28 | # Ngc generated files 29 | **/*.ngfactory.ts 30 | 31 | # Build files 32 | dist/* 33 | 34 | # Playground tmp files 35 | .playground 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | docs/* 5 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 6 | # TypeScript 7 | # *.js 8 | # *.map 9 | # *.d.ts 10 | 11 | # JetBrains 12 | .idea 13 | .project 14 | .settings 15 | .idea/* 16 | *.iml 17 | 18 | # VS Code 19 | .vscode/* 20 | 21 | # Windows 22 | Thumbs.db 23 | Desktop.ini 24 | 25 | # Mac 26 | .DS_Store 27 | **/.DS_Store 28 | 29 | # Ngc generated files 30 | **/*.ngfactory.ts 31 | 32 | # Library files 33 | src/* 34 | build/* 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | os: 4 | - linux 5 | node_js: 6 | - '8.9.1' 7 | install: 8 | - yarn install 9 | script: 10 | - npm run build 11 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angular2-library": { 3 | "promptValues": { 4 | "gitRepositoryUrl": "https://github.com/Jaspero/ng2-confirmations" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jaspero co. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Jaspero/ng-select.svg?branch=master)](https://travis-ci.org/jaspero/ng-select) 2 | [![NPM Version](https://img.shields.io/npm/v/@jaspero/ng-select.svg)](https://www.npmjs.com/package/@jaspero/ng-select) 3 | # NG2 Select 4 | A select library for Angular, with single and multiple functionality. 5 | ``` 6 | npm install --save @jaspero/ng-select 7 | ``` 8 | A demo can be found [here](https://jaspero.co/resources/projects/ng-select) 9 | 10 | ## Setup 11 | Import `JasperoSelectModule` in your `@NgModule`: 12 | 13 | ```ts 14 | @NgModule({ 15 | imports: [ 16 | JasperoSelectModule 17 | ], 18 | declarations: [AppComponent], 19 | bootstrap: [AppComponent] 20 | }) 21 | export class AppModule {} 22 | ``` 23 | 24 | ## How To Use 25 | To use the library simply add the component in your templates: 26 | ```typescript 27 | 28 | ``` 29 | 30 | ## Options 31 | 32 | You can pass the following inputs to the component: 33 | 34 | |Name|Type|Description|Default| 35 | |---|---|---|---| 36 | |options|An array of options for the select dropdown|any[]|[]| 37 | |key|The key from the options object to be used as the name of the option|string|name| 38 | |multi|Should more options be selectable at once|boolean|false| 39 | 40 | This component also support template driven and reactive forms. 41 | 42 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "baseDir": "src", 4 | "routes": { 5 | "/": "playground", 6 | "/node_modules/": "node_modules", 7 | "/dist/": "dist", 8 | "/.playground": ".playground" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var gulp = require('gulp'), 3 | path = require('path'), 4 | ngc = require('@angular/compiler-cli/src/main').main, 5 | rollup = require('gulp-rollup'), 6 | rename = require('gulp-rename'), 7 | del = require('del'), 8 | runSequence = require('run-sequence'), 9 | inlineResources = require('./tools/gulp/inline-resources'); 10 | 11 | const rootFolder = path.join(__dirname); 12 | const srcFolder = path.join(rootFolder, 'src'); 13 | const tmpFolder = path.join(rootFolder, '.tmp'); 14 | const buildFolder = path.join(rootFolder, 'build'); 15 | const distFolder = path.join(rootFolder, 'dist'); 16 | 17 | /** 18 | * 1. Delete /dist folder 19 | */ 20 | gulp.task('clean:dist', function () { 21 | 22 | // Delete contents but not dist folder to avoid broken npm links 23 | // when dist directory is removed while npm link references it. 24 | return deleteFolders([distFolder + '/**', '!' + distFolder]); 25 | }); 26 | 27 | /** 28 | * 2. Clone the /src folder into /.tmp. If an npm link inside /src has been made, 29 | * then it's likely that a node_modules folder exists. Ignore this folder 30 | * when copying to /.tmp. 31 | */ 32 | gulp.task('copy:source', function () { 33 | return gulp.src([`${srcFolder}/**/*`, `!${srcFolder}/node_modules`]) 34 | .pipe(gulp.dest(tmpFolder)); 35 | }); 36 | 37 | /** 38 | * 3. Inline template (.html) and style (.css) files into the the component .ts files. 39 | * We do this on the /.tmp folder to avoid editing the original /src files 40 | */ 41 | gulp.task('inline-resources', function () { 42 | return Promise.resolve() 43 | .then(() => inlineResources(tmpFolder)); 44 | }); 45 | 46 | 47 | /** 48 | * 4. Run the Angular compiler, ngc, on the /.tmp folder. This will output all 49 | * compiled modules to the /build folder. 50 | */ 51 | gulp.task('ngc', function () { 52 | ngc([ '--project', `${tmpFolder}/tsconfig.es5.json` ]); 53 | return Promise.resolve() 54 | }); 55 | 56 | /** 57 | * 5. Run rollup inside the /build folder to generate our Flat ES module and place the 58 | * generated file into the /dist folder 59 | */ 60 | gulp.task('rollup:fesm', function () { 61 | return gulp.src(`${buildFolder}/**/*.js`) 62 | // transform the files here. 63 | .pipe(rollup({ 64 | 65 | // Bundle's entry point 66 | // See "input" in https://rollupjs.org/#core-functionality 67 | input: `${buildFolder}/index.js`, 68 | 69 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 70 | // accessed by Rollup or produced by plugins further down the chain. 71 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 72 | // when subdirectories are used in the `src` directory. 73 | allowRealFiles: true, 74 | 75 | // A list of IDs of modules that should remain external to the bundle 76 | // See "external" in https://rollupjs.org/#core-functionality 77 | external: [ 78 | '@angular/core', 79 | '@angular/common' 80 | ], 81 | 82 | // Format of generated bundle 83 | // See "format" in https://rollupjs.org/#core-functionality 84 | format: 'es' 85 | })) 86 | .pipe(gulp.dest(distFolder)); 87 | }); 88 | 89 | /** 90 | * 6. Run rollup inside the /build folder to generate our UMD module and place the 91 | * generated file into the /dist folder 92 | */ 93 | gulp.task('rollup:umd', function () { 94 | return gulp.src(`${buildFolder}/**/*.js`) 95 | // transform the files here. 96 | .pipe(rollup({ 97 | 98 | // Bundle's entry point 99 | // See "input" in https://rollupjs.org/#core-functionality 100 | input: `${buildFolder}/index.js`, 101 | 102 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 103 | // accessed by Rollup or produced by plugins further down the chain. 104 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 105 | // when subdirectories are used in the `src` directory. 106 | allowRealFiles: true, 107 | 108 | // A list of IDs of modules that should remain external to the bundle 109 | // See "external" in https://rollupjs.org/#core-functionality 110 | external: [ 111 | '@angular/core', 112 | '@angular/common' 113 | ], 114 | 115 | // Format of generated bundle 116 | // See "format" in https://rollupjs.org/#core-functionality 117 | format: 'umd', 118 | 119 | // Export mode to use 120 | // See "exports" in https://rollupjs.org/#danger-zone 121 | exports: 'named', 122 | 123 | // The name to use for the module for UMD/IIFE bundles 124 | // (required for bundles with exports) 125 | // See "name" in https://rollupjs.org/#core-functionality 126 | name: 'ng-select', 127 | 128 | // See "globals" in https://rollupjs.org/#core-functionality 129 | globals: { 130 | typescript: 'ts' 131 | } 132 | 133 | })) 134 | .pipe(rename('ng-select.umd.js')) 135 | .pipe(gulp.dest(distFolder)); 136 | }); 137 | 138 | /** 139 | * 7. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build 140 | * because with don't need individual modules anymore, just the Flat ES module generated 141 | * on step 5. 142 | */ 143 | gulp.task('copy:build', function () { 144 | return gulp.src([`${buildFolder}/**/*`, `!${buildFolder}/**/*.js`]) 145 | .pipe(gulp.dest(distFolder)); 146 | }); 147 | 148 | /** 149 | * 8. Copy package.json from /src to /dist 150 | */ 151 | gulp.task('copy:manifest', function () { 152 | return gulp.src([`${srcFolder}/package.json`]) 153 | .pipe(gulp.dest(distFolder)); 154 | }); 155 | 156 | /** 157 | * 9. Copy README.md from / to /dist 158 | */ 159 | gulp.task('copy:readme', function () { 160 | return gulp.src([path.join(rootFolder, 'README.MD')]) 161 | .pipe(gulp.dest(distFolder)); 162 | }); 163 | 164 | /** 165 | * 10. Delete /.tmp folder 166 | */ 167 | gulp.task('clean:tmp', function () { 168 | return deleteFolders([tmpFolder]); 169 | }); 170 | 171 | /** 172 | * 11. Delete /build folder 173 | */ 174 | gulp.task('clean:build', function () { 175 | return deleteFolders([buildFolder]); 176 | }); 177 | 178 | gulp.task('compile', function () { 179 | runSequence( 180 | 'clean:dist', 181 | 'copy:source', 182 | 'inline-resources', 183 | 'ngc', 184 | 'rollup:fesm', 185 | 'rollup:umd', 186 | 'copy:build', 187 | 'copy:manifest', 188 | 'copy:readme', 189 | 'clean:build', 190 | 'clean:tmp', 191 | function (err) { 192 | if (err) { 193 | console.log('ERROR:', err.message); 194 | deleteFolders([distFolder, tmpFolder, buildFolder]); 195 | } else { 196 | console.log('Compilation finished succesfully'); 197 | } 198 | }); 199 | }); 200 | 201 | /** 202 | * Watch for any change in the /src folder and compile files 203 | */ 204 | gulp.task('watch', function () { 205 | gulp.watch(`${srcFolder}/**/*`, ['compile']); 206 | }); 207 | 208 | gulp.task('clean', ['clean:dist', 'clean:tmp', 'clean:build']); 209 | 210 | gulp.task('build', ['clean', 'compile']); 211 | gulp.task('build:watch', ['build', 'watch']); 212 | gulp.task('default', ['build:watch']); 213 | 214 | /** 215 | * Deletes the specified folder 216 | */ 217 | function deleteFolders(folders) { 218 | return del(folders); 219 | } 220 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jaspero/ng-select", 3 | "version": "0.2.0", 4 | "description": "A select library for Angular, with single and multiple select functionality.", 5 | "scripts": { 6 | "build": "gulp build", 7 | "build:watch": "gulp", 8 | "docs": "npm run docs:build", 9 | "docs:build": "compodoc -p tsconfig.json -n ng-select -d docs --hideGenerator", 10 | "docs:serve": "npm run docs:build -- -s", 11 | "docs:watch": "npm run docs:build -- -s -w", 12 | "lint": "tslint --type-check --project tsconfig.json src/**/*.ts", 13 | "publish": "npm run build && npm publish dist" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Jaspero/ng-select" 18 | }, 19 | "author": { 20 | "name": "Jaspero co.", 21 | "email": "info@jaspero.co" 22 | }, 23 | "keywords": [ 24 | "angular", 25 | "select" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Jaspero/ng-select/issues" 30 | }, 31 | "devDependencies": { 32 | "@angular/common": "5.0.0", 33 | "@angular/forms": "5.0.0", 34 | "@angular/compiler": "5.0.0", 35 | "@angular/compiler-cli": "5.0.0", 36 | "@angular/core": "5.0.0", 37 | "@angular/platform-browser": "5.0.0", 38 | "@angular/platform-browser-dynamic": "5.0.0", 39 | "@compodoc/compodoc": "1.0.4", 40 | "@types/jasmine": "2.5.38", 41 | "@types/node": "6.0.60", 42 | "angular-in-memory-web-api": "0.5.1", 43 | "codelyzer": "2.0.0", 44 | "concurrently": "3.4.0", 45 | "core-js": "2.4.1", 46 | "del": "2.2.2", 47 | "gulp": "3.9.1", 48 | "gulp-rename": "1.2.2", 49 | "gulp-rollup": "2.15.0", 50 | "jasmine-core": "2.5.2", 51 | "jasmine-spec-reporter": "3.2.0", 52 | "karma": "1.4.1", 53 | "karma-chrome-launcher": "2.0.0", 54 | "karma-cli": "1.0.1", 55 | "karma-coverage-istanbul-reporter": "0.2.0", 56 | "karma-jasmine": "1.1.0", 57 | "karma-jasmine-html-reporter": "0.2.2", 58 | "lite-server": "2.3.0", 59 | "node-sass": "4.5.2", 60 | "node-sass-tilde-importer": "1.0.0", 61 | "node-watch": "0.5.2", 62 | "protractor": "5.1.0", 63 | "rollup": "0.49.3", 64 | "run-sequence": "1.2.2", 65 | "rxjs": "5.5.2", 66 | "systemjs": "0.20.12", 67 | "ts-node": "2.0.0", 68 | "tslint": "4.5.0", 69 | "typescript": "2.4.2", 70 | "zone.js": "0.8.14" 71 | }, 72 | "engines": { 73 | "node": ">=6.0.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/select/select.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 |
6 | 7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 | {{item[key]}} 15 | 16 | 17 | 18 | {{selected[key]}} 19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 | 41 | -------------------------------------------------------------------------------- /src/components/select/select.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .items { 3 | font-size: 14px; 4 | font-family:inherit; 5 | box-sizing: border-box; 6 | border: none; 7 | outline: none; 8 | background: transparent; 9 | width: 100%; 10 | padding: 0 10px; 11 | line-height: 40px; 12 | height: 40px; 13 | position: relative; 14 | box-shadow: 0 1px 3px 0 rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 2px 1px -1px rgba(0,0,0,.12); 15 | 16 | &:after { 17 | position: absolute; 18 | top: 50%; 19 | right: 15px; 20 | display: block; 21 | width: 0; 22 | height: 0; 23 | border-color: #808080 transparent transparent transparent; 24 | border-style: solid; 25 | border-width: 5px 5px 0 5px; 26 | content: ''; 27 | -webkit-transform: translateY(-50%); 28 | -moz-transform: translateY(-50%); 29 | -ms-transform: translateY(-50%); 30 | -o-transform: translateY(-50%); 31 | transform: translateY(-50%); 32 | } 33 | 34 | .item { 35 | float: left; 36 | margin: 0 3px 3px 0; 37 | cursor: pointer; 38 | display: inline-block; 39 | vertical-align: baseline; 40 | zoom: 1; 41 | } 42 | 43 | &.multi { 44 | .item { 45 | cursor: default; 46 | border-radius: 16px; 47 | display: block; 48 | height: 30px; 49 | line-height: 30px; 50 | margin: 5px 8px 0 0; 51 | padding: 0 12px; 52 | float: left; 53 | box-sizing: border-box; 54 | max-width: 100%; 55 | position: relative; 56 | background: rgb(224,224,224); 57 | color: rgb(66,66,66); 58 | } 59 | } 60 | } 61 | 62 | input[type="text"] { 63 | background: none; 64 | border: none; 65 | outline: none; 66 | float: left; 67 | min-height: 20px; 68 | vertical-align: middle; 69 | position: relative; 70 | top: 50%; 71 | font-family:inherit; 72 | -webkit-transform: translateY(-50%); 73 | -moz-transform: translateY(-50%); 74 | -ms-transform: translateY(-50%); 75 | -o-transform: translateY(-50%); 76 | transform: translateY(-50%); 77 | box-shadow: none; 78 | padding: 0; 79 | display: inline-block; 80 | width: 100%; 81 | } 82 | 83 | .dropdown { 84 | position: absolute; 85 | z-index: 10; 86 | margin: -1px 0 0 0; 87 | background: #ffffff; 88 | border-top: 0 none; 89 | display: none; 90 | width: 100%; 91 | box-shadow: 0 1px 3px 0 rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 2px 1px -1px rgba(0,0,0,.12); 92 | 93 | &.active { 94 | display: block; 95 | } 96 | 97 | .dropdown-content { 98 | max-height: 200px; 99 | overflow-x: hidden; 100 | overflow-y: auto; 101 | width: 100%; 102 | 103 | .option { 104 | padding: 8px; 105 | cursor: pointer; 106 | 107 | &.selected { 108 | background-color: #f9f9f9; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/select/select.component.ts: -------------------------------------------------------------------------------- 1 | import {DOCUMENT} from '@angular/common'; 2 | import { 3 | AfterViewInit, 4 | ChangeDetectionStrategy, ChangeDetectorRef, 5 | Component, 6 | ElementRef, 7 | EventEmitter, 8 | forwardRef, 9 | Inject, 10 | Input, OnChanges, 11 | OnDestroy, 12 | OnInit, 13 | Output, 14 | Renderer2, TemplateRef, 15 | ViewChild 16 | } from '@angular/core'; 17 | import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; 18 | import {fromEvent} from 'rxjs/observable/fromEvent'; 19 | import {filter, skip, takeUntil, withLatestFrom} from 'rxjs/operators'; 20 | import {Subject} from 'rxjs/Subject'; 21 | import {KeyCode} from '../../enums/key-code.enum'; 22 | import {BehaviorSubject} from 'rxjs/BehaviorSubject'; 23 | 24 | @Component({ 25 | selector: 'jaspero-select', 26 | templateUrl: './select.component.html', 27 | styleUrls: ['./select.component.scss'], 28 | changeDetection: ChangeDetectionStrategy.OnPush, 29 | providers: [{ 30 | provide: NG_VALUE_ACCESSOR, 31 | useExisting: forwardRef(() => SelectComponent), 32 | multi: true 33 | }] 34 | }) 35 | export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges, OnDestroy { 36 | constructor( 37 | private _renderer: Renderer2, 38 | private _cdr: ChangeDetectorRef, 39 | @Inject(DOCUMENT) private _document: any 40 | ) { } 41 | 42 | @ViewChild('input') inputEl: ElementRef; 43 | 44 | @Input() key = 'name'; 45 | @Input() options: any[]; 46 | @Input() multiSelect: boolean; 47 | @Input() theTabIndex: number; 48 | @Input() matchFromStart = false; 49 | @Input() returnOnlyKey = false; 50 | @Input() selected: any; 51 | @Input() selectTemplate: TemplateRef; 52 | @Input() dropDownTemplate: TemplateRef; 53 | 54 | @Output() toggled = new EventEmitter(); 55 | 56 | propagateChange; 57 | onTouched; 58 | 59 | open$ = new Subject(); 60 | destroyed$ = new Subject(); 61 | 62 | isMulti$ = new BehaviorSubject(false); 63 | activeIndex$ = new BehaviorSubject(null); 64 | selection$ = new BehaviorSubject(null); 65 | 66 | selectToUse: Function = this.select; 67 | outputToUse: Function = this.output; 68 | removeToUse: Function = this.remove; 69 | mapMethodToUse: Function = this.output; 70 | 71 | blockEmit = false; 72 | 73 | ngOnInit() { 74 | this.open$.pipe( 75 | takeUntil(this.destroyed$) 76 | ) 77 | .subscribe(value => { 78 | this.toggled.emit(value); 79 | }); 80 | 81 | this.selection$.pipe( 82 | takeUntil(this.destroyed$), 83 | skip(1), 84 | filter(() => !this.blockEmit) 85 | ) 86 | .subscribe(value => { 87 | this.propagateChange(this.outputToUse(value)); 88 | }); 89 | } 90 | 91 | ngAfterViewInit() { 92 | fromEvent(this._document.documentElement, 'click') 93 | .pipe( 94 | takeUntil(this.destroyed$) 95 | ) 96 | .subscribe(() => { 97 | this.open$.next(false); 98 | }); 99 | 100 | fromEvent(this.inputEl.nativeElement, 'blur') 101 | .pipe( 102 | takeUntil(this.destroyed$) 103 | ) 104 | .subscribe(() => { 105 | this.onTouched(); 106 | }); 107 | 108 | fromEvent(this.inputEl.nativeElement, 'keyup') 109 | .pipe( 110 | takeUntil(this.destroyed$), 111 | withLatestFrom(this.open$, this.activeIndex$) 112 | ) 113 | .subscribe(data => { 114 | data[0].stopPropagation(); 115 | 116 | switch (data[0].keyCode) { 117 | case KeyCode.UpArrow: 118 | if (!data[1]) { 119 | this.activeIndex$.next(this.options.length - 1); 120 | this.open$.next(true); 121 | } else { 122 | this.activeIndex$.next(data[2] > 0 ? data[2] - 1 : this.options.length - 1); 123 | } 124 | 125 | break; 126 | 127 | case KeyCode.DownArrow: 128 | if (!data[1]) { 129 | this.activeIndex$.next(0); 130 | this.open$.next(true); 131 | } else { 132 | this.activeIndex$.next(data[2] < this.options.length - 1 ? data[2] + 1 : 0); 133 | } 134 | 135 | break; 136 | 137 | case KeyCode.Enter: 138 | if (data[2] === null || !this.options.length) { 139 | return; 140 | } 141 | 142 | this.open$.next(false); 143 | this.selectToUse(data[2]); 144 | this.activeIndex$.next(null); 145 | 146 | break; 147 | 148 | case KeyCode.Escape: 149 | if (data[1]) { 150 | this.activeIndex$.next(null); 151 | this.open$.next(false); 152 | } 153 | break; 154 | } 155 | }); 156 | } 157 | 158 | ngOnChanges(change) { 159 | 160 | if (change.multiSelect) { 161 | this.isMulti$.next(change.multiSelect.currentValue); 162 | 163 | if (change.multiSelect.currentValue) { 164 | this.outputToUse = this.returnOnlyKey ? this.outputOnlyKeyMulti : this.output; 165 | this.removeToUse = this.removeMulti; 166 | this.selectToUse = this.selectMulti; 167 | } else { 168 | this.outputToUse = this.returnOnlyKey ? this.outputOnlyKey : this.output; 169 | this.removeToUse = this.remove; 170 | this.selectToUse = this.select; 171 | } 172 | } 173 | 174 | if (change.returnOnlyKey) { 175 | const multiCurrent = this.isMulti$.getValue(); 176 | 177 | if (change.returnOnlyKey.currentValue) { 178 | 179 | if (multiCurrent) { 180 | this.outputToUse = this.outputOnlyKeyMulti; 181 | this.mapMethodToUse = this.mapOnlyKeyMulti; 182 | } else { 183 | this.outputToUse = this.outputOnlyKey; 184 | this.mapMethodToUse = this.mapOnlyKey; 185 | } 186 | 187 | } else { 188 | this.outputToUse = this.output; 189 | this.mapMethodToUse = this.output; 190 | } 191 | } 192 | 193 | if (change.options) { 194 | 195 | } 196 | 197 | console.log('the change', change); 198 | } 199 | 200 | ngOnDestroy() { 201 | this.destroyed$.next(); 202 | } 203 | 204 | setObservable(name: string, value: any) { 205 | this[name].next(value); 206 | } 207 | 208 | open(event: KeyboardEvent) { 209 | event.stopPropagation(); 210 | this.inputEl.nativeElement.focus(); 211 | this.open$.next(true); 212 | } 213 | 214 | output(value: any) { 215 | return value; 216 | } 217 | 218 | outputOnlyKey(value: any) { 219 | return value[this.key]; 220 | } 221 | 222 | outputOnlyKeyMulti(value: any[]) { 223 | return value.map(val => val[this.key]); 224 | } 225 | 226 | mapOnlyKey(value: any) { 227 | return this.options.find(option => option[this.key] === value); 228 | } 229 | 230 | mapOnlyKeyMulti(value: any) { 231 | return value.map(val => { 232 | return this.options.find(option => option[this.key] === val) 233 | }); 234 | } 235 | 236 | select(index: number) { 237 | this.selection$.next(this.options[index]); 238 | } 239 | 240 | selectMulti(value: any) {} 241 | 242 | remove() { 243 | this.selection$.next(null); 244 | } 245 | 246 | removeMulti(value: any) {} 247 | 248 | writeValue(value: any) { 249 | this.blockEmit = true; 250 | this.selection$.next(this.mapMethodToUse(value)); 251 | this.blockEmit = false; 252 | } 253 | 254 | registerOnChange(fn) { 255 | this.propagateChange = fn; 256 | } 257 | 258 | setDisabledState(isDisabled) { 259 | this._renderer.setProperty(this.inputEl.nativeElement, 'disabled', isDisabled); 260 | this.open$.next(false); 261 | } 262 | 263 | registerOnTouched(fn) { 264 | this.onTouched = fn; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/enums/key-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum KeyCode { 2 | Backspace = 8, 3 | Tab = 9, 4 | Enter = 13, 5 | Shift = 16, 6 | Ctrl = 17, 7 | Alt = 18, 8 | PauseBreak = 19, 9 | CapsLock = 20, 10 | Escape = 27, 11 | Space = 32, 12 | PageUp = 33, 13 | PageDown = 34, 14 | End = 35, 15 | Home = 36, 16 | 17 | LeftArrow = 37, 18 | UpArrow = 38, 19 | RightArrow = 39, 20 | DownArrow = 40, 21 | 22 | Insert = 45, 23 | Delete = 46, 24 | 25 | Zero = 48, 26 | ClosedParen = Zero, 27 | One = 49, 28 | ExclamationMark = One, 29 | Two = 50, 30 | AtSign = Two, 31 | Three = 51, 32 | PoundSign = Three, 33 | Hash = PoundSign, 34 | Four = 52, 35 | DollarSign = Four, 36 | Five = 53, 37 | PercentSign = Five, 38 | Six = 54, 39 | Caret = Six, 40 | Hat = Caret, 41 | Seven = 55, 42 | Ampersand = Seven, 43 | Eight = 56, 44 | Star = Eight, 45 | Asterik = Star, 46 | Nine = 57, 47 | OpenParen = Nine, 48 | 49 | A = 65, 50 | B = 66, 51 | C = 67, 52 | D = 68, 53 | E = 69, 54 | F = 70, 55 | G = 71, 56 | H = 72, 57 | I = 73, 58 | J = 74, 59 | K = 75, 60 | L = 76, 61 | M = 77, 62 | N = 78, 63 | O = 79, 64 | P = 80, 65 | Q = 81, 66 | R = 82, 67 | S = 83, 68 | T = 84, 69 | U = 85, 70 | V = 86, 71 | W = 87, 72 | X = 88, 73 | Y = 89, 74 | Z = 90, 75 | 76 | LeftWindowKey = 91, 77 | RightWindowKey = 92, 78 | SelectKey = 93, 79 | 80 | Numpad0 = 96, 81 | Numpad1 = 97, 82 | Numpad2 = 98, 83 | Numpad3 = 99, 84 | Numpad4 = 100, 85 | Numpad5 = 101, 86 | Numpad6 = 102, 87 | Numpad7 = 103, 88 | Numpad8 = 104, 89 | Numpad9 = 105, 90 | 91 | Multiply = 106, 92 | Add = 107, 93 | Subtract = 109, 94 | DecimalPoint = 110, 95 | Divide = 111, 96 | 97 | F1 = 112, 98 | F2 = 113, 99 | F3 = 114, 100 | F4 = 115, 101 | F5 = 116, 102 | F6 = 117, 103 | F7 = 118, 104 | F8 = 119, 105 | F9 = 120, 106 | F10 = 121, 107 | F11 = 122, 108 | F12 = 123, 109 | 110 | NumLock = 144, 111 | ScrollLock = 145, 112 | 113 | SemiColon = 186, 114 | Equals = 187, 115 | Comma = 188, 116 | Dash = 189, 117 | Period = 190, 118 | UnderScore = Dash, 119 | PlusSign = Equals, 120 | ForwardSlash = 191, 121 | Tilde = 192, 122 | GraveAccent = Tilde, 123 | 124 | OpenBracket = 219, 125 | ClosedBracket = 221, 126 | Quote = 222 127 | } 128 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {FormsModule} from '@angular/forms'; 4 | import {SelectComponent} from './components/select/select.component'; 5 | 6 | export * from './components/select/select.component'; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, FormsModule], 10 | declarations: [SelectComponent], 11 | exports: [SelectComponent] 12 | }) 13 | export class JasperoSelectModule {} 14 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jaspero/ng-select", 3 | "version": "0.2.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/Jaspero/ng-select" 7 | }, 8 | "author": { 9 | "name": "Jaspero co.", 10 | "email": "info@jaspero.co" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "select" 15 | ], 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/Jaspero/ng-select/issues" 19 | }, 20 | "main": "ng-select.umd.js", 21 | "module": "ng-select.js", 22 | "jsnext:main": "ng-select.js", 23 | "typings": "ng-select.d.ts", 24 | "peerDependencies": { 25 | "@angular/common": "^5.0.0", 26 | "@angular/forms": "^5.0.0", 27 | "@angular/core": "^5.0.0", 28 | "rxjs": "^5.1.0", 29 | "zone.js": "^0.8.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "baseUrl": ".", 7 | "stripInternal": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "outDir": "../build", 12 | "rootDir": ".", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "skipLibCheck": true, 18 | "types": [] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "strictMetadataEmit": true, 23 | "skipTemplateCodegen": true, 24 | "flatModuleOutFile": "ng-select.js", 25 | "flatModuleId": "@jaspero/ng-select" 26 | }, 27 | "files": [ 28 | "./index.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.es5.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "outDir": "../out-tsc/spec", 7 | "module": "commonjs", 8 | "target": "es6", 9 | "baseUrl": "", 10 | "types": [ 11 | "jest", 12 | "node" 13 | ] 14 | }, 15 | "files": [ 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tools/gulp/inline-resources.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/filipesilva/angular-quickstart-lib/blob/master/inline-resources.js 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | const sass = require('node-sass'); 9 | const tildeImporter = require('node-sass-tilde-importer'); 10 | 11 | /** 12 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 13 | * We use promises instead of synchronized functions to make the process less I/O bound and 14 | * faster. It also simplifies the code. 15 | */ 16 | function promiseify(fn) { 17 | return function () { 18 | const args = [].slice.call(arguments, 0); 19 | return new Promise((resolve, reject) => { 20 | fn.apply(this, args.concat([function (err, value) { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | resolve(value); 25 | } 26 | }])); 27 | }); 28 | }; 29 | } 30 | 31 | const readFile = promiseify(fs.readFile); 32 | const writeFile = promiseify(fs.writeFile); 33 | 34 | /** 35 | * Inline resources in a tsc/ngc compilation. 36 | * @param projectPath {string} Path to the project. 37 | */ 38 | function inlineResources(projectPath) { 39 | 40 | // Match only TypeScript files in projectPath. 41 | const files = glob.sync('**/*.ts', {cwd: projectPath}); 42 | 43 | // For each file, inline the templates and styles under it and write the new file. 44 | return Promise.all(files.map(filePath => { 45 | const fullFilePath = path.join(projectPath, filePath); 46 | return readFile(fullFilePath, 'utf-8') 47 | .then(content => inlineResourcesFromString(content, url => { 48 | // Resolve the template url. 49 | return path.join(path.dirname(fullFilePath), url); 50 | })) 51 | .then(content => writeFile(fullFilePath, content)) 52 | .catch(err => { 53 | console.error('An error occured: ', err); 54 | }); 55 | })); 56 | } 57 | 58 | /** 59 | * Inline resources from a string content. 60 | * @param content {string} The source file's content. 61 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 62 | * @returns {string} The content with resources inlined. 63 | */ 64 | function inlineResourcesFromString(content, urlResolver) { 65 | // Curry through the inlining functions. 66 | return [ 67 | inlineTemplate, 68 | inlineStyle, 69 | removeModuleId 70 | ].reduce((content, fn) => fn(content, urlResolver), content); 71 | } 72 | 73 | /** 74 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 75 | * replace with `template: ...` (with the content of the file included). 76 | * @param content {string} The source file's content. 77 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 78 | * @return {string} The content with all templates inlined. 79 | */ 80 | function inlineTemplate(content, urlResolver) { 81 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) { 82 | const templateFile = urlResolver(templateUrl); 83 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 84 | const shortenedTemplate = templateContent 85 | .replace(/([\n\r]\s*)+/gm, ' ') 86 | .replace(/"/g, '\\"'); 87 | return `template: "${shortenedTemplate}"`; 88 | }); 89 | } 90 | 91 | 92 | /** 93 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 94 | * replace with `styles: [...]` (with the content of the file included). 95 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 96 | * @param content {string} The source file's content. 97 | * @return {string} The content with all styles inlined. 98 | */ 99 | function inlineStyle(content, urlResolver) { 100 | return content.replace(/styleUrls\s*:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) { 101 | const urls = eval(styleUrls); 102 | return 'styles: [' 103 | + urls.map(styleUrl => { 104 | const styleFile = urlResolver(styleUrl); 105 | const originContent = fs.readFileSync(styleFile, 'utf-8'); 106 | const styleContent = styleFile.endsWith('.scss') ? buildSass(originContent, styleFile) : originContent; 107 | const shortenedStyle = styleContent 108 | .replace(/([\n\r]\s*)+/gm, ' ') 109 | .replace(/"/g, '\\"'); 110 | return `"${shortenedStyle}"`; 111 | }) 112 | .join(',\n') 113 | + ']'; 114 | }); 115 | } 116 | 117 | /** 118 | * build sass content to css 119 | * @param content {string} the css content 120 | * @param sourceFile {string} the scss file sourceFile 121 | * @return {string} the generated css, empty string if error occured 122 | */ 123 | function buildSass(content, sourceFile) { 124 | try { 125 | const result = sass.renderSync({ 126 | data: content, 127 | file: sourceFile, 128 | importer: tildeImporter 129 | }); 130 | return result.css.toString() 131 | } catch (e) { 132 | console.error('\x1b[41m'); 133 | console.error('at ' + sourceFile + ':' + e.line + ":" + e.column); 134 | console.error(e.formatted); 135 | console.error('\x1b[0m'); 136 | return ""; 137 | } 138 | } 139 | 140 | /** 141 | * Remove every mention of `moduleId: module.id`. 142 | * @param content {string} The source file's content. 143 | * @returns {string} The content with all moduleId: mentions removed. 144 | */ 145 | function removeModuleId(content) { 146 | return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 147 | } 148 | 149 | module.exports = inlineResources; 150 | module.exports.inlineResourcesFromString = inlineResourcesFromString; 151 | 152 | // Run inlineResources if module is being called directly from the CLI with arguments. 153 | if (require.main === module && process.argv.length > 2) { 154 | console.log('Inlining resources from project:', process.argv[2]); 155 | return inlineResources(process.argv[2]); 156 | } 157 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "experimentalDecorators": true, 5 | "moduleResolution": "node", 6 | "rootDir": "./src", 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ], 11 | "skipLibCheck": true, 12 | "types": [] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "rulesDirectory": [ 4 | "node_modules/codelyzer" 5 | ], 6 | "rules": { 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "indent": [ 16 | true, 17 | "spaces" 18 | ], 19 | "label-position": true, 20 | "max-line-length": false, 21 | "member-access": false, 22 | "member-ordering": false, 23 | "no-arg": true, 24 | "no-bitwise": true, 25 | "no-console": [ 26 | true, 27 | "debug", 28 | "info", 29 | "time", 30 | "timeEnd", 31 | "trace" 32 | ], 33 | "no-construct": true, 34 | "no-debugger": true, 35 | "no-duplicate-variable": true, 36 | "no-empty": false, 37 | "no-eval": true, 38 | "no-inferrable-types": true, 39 | "no-shadowed-variable": true, 40 | "no-string-literal": false, 41 | "no-switch-case-fall-through": true, 42 | "no-trailing-whitespace": true, 43 | "no-unused-expression": true, 44 | "no-unused-variable": true, 45 | "no-use-before-declare": true, 46 | "no-var-keyword": true, 47 | "object-literal-sort-keys": false, 48 | "one-line": [ 49 | true, 50 | "check-open-brace", 51 | "check-catch", 52 | "check-else", 53 | "check-whitespace" 54 | ], 55 | "quotemark": [ 56 | true, 57 | "single" 58 | ], 59 | "radix": true, 60 | "semicolon": [ 61 | "always" 62 | ], 63 | "triple-equals": [ 64 | true, 65 | "allow-null-check" 66 | ], 67 | "typedef-whitespace": [ 68 | true, 69 | { 70 | "call-signature": "nospace", 71 | "index-signature": "nospace", 72 | "parameter": "nospace", 73 | "property-declaration": "nospace", 74 | "variable-declaration": "nospace" 75 | } 76 | ], 77 | "variable-name": false, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type" 85 | ], 86 | "directive-selector": [true, "attribute", "", "camelCase"], 87 | "component-selector": [true, "element", "", "kebab-case"], 88 | "use-input-property-decorator": true, 89 | "use-output-property-decorator": true, 90 | "use-host-property-decorator": true, 91 | "no-input-rename": true, 92 | "no-output-rename": true, 93 | "use-life-cycle-interface": true, 94 | "use-pipe-transform-interface": true, 95 | "component-class-suffix": true, 96 | "directive-class-suffix": true 97 | } 98 | } 99 | --------------------------------------------------------------------------------