├── CHANGELOG.md ├── LICENSE ├── README.md ├── data └── city-data.json ├── img ├── dependent.gif └── repeatcss.gif ├── modify-datetime-entry-file └── README.md ├── package.json ├── picker-column └── picker-column.tsx ├── src ├── ionic4-city-picker.model.ts ├── ionic4-city-picker.module.ts ├── ionic4-city-picker.scss └── ionic4-city-picker.ts └── tsconfig.json /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwlccc/ionic4-city-picker/cd40ee48c1da1c79e6cd4ec4bfb8016b3d16ef66/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-present zwlccc 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic4CityPicker 2 | 3 | Ionic4 City Picker--An Ionic4 Custom Picker Component 4 | 5 | **For ionic 2.x or 3.x, please use ionic2-city-picker 6 | 7 | Github: [ionic2-city-picker](https://github.com/hsuanxyz/ionic2-city-picker) 8 | 9 | ## Preview 10 | ### Picker with Independent/ Dependent Columns 11 | 12 | ![Picker with Dependent Columns](https://github.com/zwlccc/ionic4-city-picker/blob/master/img/dependent.gif?raw=true) 13 | 14 | ### PickerController doesn't show correctly 15 | 16 | ![Picker with Dependent Columns](https://github.com/zwlccc/ionic4-city-picker/blob/master/img/repeatcss.gif?raw=true) 17 | 18 | ### If you project have this happens,you need: 19 | a)ionic version <= 4.4.2 20 | 21 | 1.download ionic core([ionic](https://github.com/ionic-team/ionic)); 22 | 23 | 2.modify picker-column.tsx file([just here](https://github.com/zwlccc/ionic4-city-picker/blob/master/picker-column/picker-column.tsx)); 24 | 25 | 3.```npm run build``` ionic/core; 26 | 27 | 4.copy ```dist``` folder file to ```node_modules\@ionic\core```,overlay folder. 28 | 29 | b)4.4.2 < ionic version <= 4.11.5 30 | 31 | 1.modify ```ion-datetime_3-ios.entry.js``` and ```ion-datetime_3-md.entry.js```([just here](https://github.com/zwlccc/ionic4-city-picker/blob/master/modify-datetime-entry-file)) 32 | 33 | 2.js file path ```node_modules\@ionic\core\dist\esm```; 34 | 35 | ## Installation 36 | ``` 37 | npm install ionic4-city-picker --save 38 | ``` 39 | 40 | ## Json Data 41 | https://github.com/zwlccc/ionic4-city-picker/blob/master/data/city-data.json 42 | 43 | ## Usage 44 | 45 | ### Basic 46 | 1.Import IonCityPickerModule And Provide DataService to your app/module. 47 | ```Typescript 48 | import { IonCityPickerModule } from 'ionic4-city-picker'; 49 | import { DataService } from './services/data.service'; 50 | 51 | @NgModule({ 52 | declarations: [AppComponent], 53 | entryComponents: [], 54 | imports: [ 55 | BrowserModule, 56 | IonicModule.forRoot(), 57 | HttpClientModule, 58 | AppRoutingModule, 59 | IonCityPickerModule //Import IonCityPickerModule 60 | ], 61 | providers: [ 62 | StatusBar, 63 | SplashScreen, 64 | DataService, //Provide the DataService 65 | { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } 66 | ], 67 | bootstrap: [AppComponent] 68 | }) 69 | export class AppModule {} 70 | ``` 71 | 2.Create the Services. 72 | ```typescript 73 | import { Injectable } from '@angular/core'; 74 | import { HttpClient} from '@angular/common/http'; 75 | import { Observable } from 'rxjs'; 76 | import { catchError, tap } from 'rxjs/operators'; 77 | 78 | @Injectable() 79 | export class DataService { 80 | constructor(public http: HttpClient) { 81 | } 82 | 83 | geCityData(): Observable { 84 | return this.http.get('./assets/data/city-data.json', {withCredentials: false}) 85 | .pipe( 86 | tap( data => data), 87 | catchError(this.handleError) 88 | ); 89 | } 90 | 91 | private handleError (error: Response | any) { 92 | let errMsg: string; 93 | if (!error.ok) { 94 | errMsg = "Can't connect to server. Please try again later."; 95 | } else { 96 | errMsg = error.message ? error.message : error.toString(); 97 | } 98 | return Promise.reject(errMsg); 99 | } 100 | 101 | } 102 | ``` 103 | 3.Initialize View Controller. 104 | ```typescript 105 | import { Component } from '@angular/core'; 106 | import { DataService } from '../services/data.service'; 107 | 108 | @Component({ 109 | selector: 'app-home', 110 | templateUrl: 'home.page.html', 111 | styleUrls: ['home.page.scss'], 112 | }) 113 | export class HomePage { 114 | cityData: any[] = []; 115 | 116 | constructor( 117 | private ds: DataService 118 | ) { 119 | 120 | } 121 | 122 | ngOnInit() { 123 | this.ds.geCityData().subscribe(res => { 124 | this.cityData = res; 125 | }, 126 | error => { 127 | console.log(error); 128 | }); 129 | } 130 | 131 | onCityChange(e){ 132 | const cityvalue = e.getValue(); //get city value 133 | } 134 | } 135 | ``` 136 | 4.Add ion-multi-picker to your html template. 137 | 138 | ```html 139 | 140 | 141 | 142 | ``` 143 | 144 | Set `disabled` to `true` to prevent interaction. 145 | 146 | ```html 147 | 148 | 149 | 150 | ``` 151 | ## Attributes 152 | | Attribute | Description | Type | Options | Default| 153 | |-----------|-------------|------|---------|--------| 154 | |`cancelText`|The text to display on the picker's cancel button.| `string` | - | `'Cancel'` | 155 | |`disabled`|If true, the user cannot interact with the city.| `boolean \| undefined` | - | `false` | 156 | |`doneText`|The text to display on the picker's "Done" button.| `string` | - | `'Done'` | 157 | |`mode`|The mode determines which platform styles to use.| `"ios" \| "md"` | - | `undefined` | 158 | |`name`|The name of the control, which is submitted with the form data.| `String` | - | `this.inputId`| 159 | |`placeholder`|The text to display when there's no city selected yet. Using lowercase to match the input attribute| `string` | - | `null \| string \| undefined` | 160 | |`readonly`|If `true`, the city appears normal but is not interactive.| `boolean` | - | `false` 161 | |`value`|The value of the city string.| `null \| string \| undefined` | - | `string` | 162 | |`citiesData`|**Required**,configure multi picker columns | `CityPickerColumn`| - | `[]` | 163 | |`separator`|Optional, charactor to separate value from each column| `string` | - | `'-'` | 164 | 165 | ## Events 166 | 167 | | Event | Description | Type | 168 | | ----------- | --------------------------------------------------- | ---------------------------------------- | 169 | | `ionCancel` | Emitted when the city selection was cancelled. | `CustomEvent` | 170 | | `ionChange` | Emitted when the value (selected city) has changed. | `CustomEvent` | 171 | 172 | ## Contribution 173 | 174 | Welcome issue report, PR and contributors. Help me improve it. 175 | 176 | ## License 177 | MIT 178 | 179 | ## Change Log 180 | [Change log is here](https://github.com/zwlccc/ionic4-city-picker/blob/master/CHANGELOG.md) 181 | 182 | -------------------------------------------------------------------------------- /img/dependent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwlccc/ionic4-city-picker/cd40ee48c1da1c79e6cd4ec4bfb8016b3d16ef66/img/dependent.gif -------------------------------------------------------------------------------- /img/repeatcss.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zwlccc/ionic4-city-picker/cd40ee48c1da1c79e6cd4ec4bfb8016b3d16ef66/img/repeatcss.gif -------------------------------------------------------------------------------- /modify-datetime-entry-file/README.md: -------------------------------------------------------------------------------- 1 | 1.open ```ion-datetime_3-ios.entry.js``` and ```ion-datetime_3-md.entry.js``` 2 | 2.modify update function: 3 | ``` 4 | update(y, duration, saveY) { 5 | if (!this.optsEl) { 6 | return; 7 | } 8 | // ensure we've got a good round number :) 9 | let translateY = 0; 10 | let translateZ = 0; 11 | const { col, rotateFactor } = this; 12 | const selectedIndex = col.selectedIndex = this.indexForY(-y); 13 | const durationStr = (duration === 0) ? '' : duration + 'ms'; 14 | const scaleStr = `scale(${this.scaleFactor})`; 15 | const children = this.optsEl.children; 16 | const children_length = this.optsEl.children.length; 17 | const options_length = col.options.length; 18 | const length = children_length < options_length ? options_length : children_length; 19 | for (let i = 0; i < length; i++) { 20 | const button = children[i]; 21 | const opt = col.options[i]; 22 | const optOffset = (i * this.optHeight) + y; 23 | let transform = ''; 24 | if (rotateFactor !== 0) { 25 | const rotateX = optOffset * rotateFactor; 26 | if (Math.abs(rotateX) <= 90) { 27 | translateY = 0; 28 | translateZ = 90; 29 | transform = `rotateX(${rotateX}deg) `; 30 | } 31 | else { 32 | translateY = -9999; 33 | } 34 | } 35 | else { 36 | translateZ = 0; 37 | translateY = optOffset; 38 | } 39 | const selected = selectedIndex === i; 40 | transform += `translate3d(0px,${translateY}px,${translateZ}px) `; 41 | if (this.scaleFactor !== 1 && !selected) { 42 | transform += scaleStr; 43 | } 44 | // Update transition duration 45 | if (this.noAnimate) { 46 | if (opt) { 47 | opt.duration = 0; 48 | } 49 | if (button) { 50 | button.style.transitionDuration = ''; 51 | } 52 | } 53 | else if (opt) { 54 | if (duration !== opt.duration) { 55 | opt.duration = duration; 56 | if (button) { 57 | button.style.transitionDuration = durationStr; 58 | } 59 | } 60 | } 61 | // Update transform 62 | if (opt) { 63 | if (transform !== opt.transform) { 64 | opt.transform = transform; 65 | if (button) { 66 | button.style.transform = transform; 67 | } 68 | } 69 | } 70 | // Update selected item 71 | if (opt) { 72 | if (selected !== opt.selected) { 73 | opt.selected = selected; 74 | if (selected && button) { 75 | button.classList.add(PICKER_OPT_SELECTED); 76 | } 77 | else if (button) { 78 | button.classList.remove(PICKER_OPT_SELECTED); 79 | } 80 | } 81 | } 82 | } 83 | this.col.prevSelected = selectedIndex; 84 | if (saveY) { 85 | this.y = y; 86 | } 87 | if (this.lastIndex !== selectedIndex) { 88 | // have not set a last index yet 89 | hapticSelectionChanged(); 90 | this.lastIndex = selectedIndex; 91 | } 92 | } 93 | ``` 94 | 3.modify render function: 95 | ``` 96 | render() { 97 | const col = this.col; 98 | const Button = 'button'; 99 | const mode = getIonMode(this); 100 | return (h(Host, { class: { 101 | [mode]: true, 102 | 'picker-col': true, 103 | 'picker-opts-left': this.col.align === 'left', 104 | 'picker-opts-right': this.col.align === 'right' 105 | }, style: { 106 | 'max-width': this.col.columnWidth 107 | } }, col.prefix && (h("div", { class: "picker-prefix", style: { width: col.prefixWidth } }, col.prefix)), h("div", { class: "picker-opts", style: { maxWidth: col.optionsWidth }, ref: el => this.optsEl = el }, col.options.map((o, index) => h(Button, { type: "button", class: { 'picker-opt': true, 'picker-opt-disabled': !!o.disabled, 'picker-opt-selected': o.selected }, style: { transform: o.transform ? o.transform : 'translate3d(0px, -9999px, 90px)', 'transition-duration': o.duration ? o.duration : TRANSITION_DURATION + 'ms' }, "opt-index": index }, o.text))), col.suffix && (h("div", { class: "picker-suffix", style: { width: col.suffixWidth } }, col.suffix)))); 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic4-city-picker", 3 | "version": "0.0.3", 4 | "description": "city picker for ionic4", 5 | "peerDependencies": { 6 | "@angular/common": "^7.2.0", 7 | "@angular/core": "^7.2.0" 8 | }, 9 | "main": "bundles/ionic4-city-picker.umd.js", 10 | "module": "fesm5/ionic4-city-picker.js", 11 | "es2015": "fesm2015/ionic4-city-picker.js", 12 | "esm5": "esm5/ionic4-city-picker.js", 13 | "esm2015": "esm2015/ionic4-city-picker.js", 14 | "fesm5": "fesm5/ionic4-city-picker.js", 15 | "fesm2015": "fesm2015/ionic4-city-picker.js", 16 | "typings": "ionic4-city-picker.d.ts", 17 | "metadata": "ionic4-city-picker.metadata.json", 18 | "sideEffects": false, 19 | "dependencies": { 20 | "tslib": "^1.9.0" 21 | }, 22 | "devDependencies": { 23 | "@angular-devkit/architect": "~0.13.8", 24 | "@angular-devkit/build-angular": "~0.13.8", 25 | "@angular-devkit/core": "~7.3.8", 26 | "@angular-devkit/schematics": "~7.3.8", 27 | "@angular/cli": "~7.3.8", 28 | "@angular/compiler": "~7.2.2", 29 | "@angular/compiler-cli": "~7.2.2", 30 | "@angular/language-service": "~7.2.2", 31 | "@ionic/angular-toolkit": "~1.5.1", 32 | "@types/node": "~12.0.0", 33 | "@types/jasmine": "~2.8.8", 34 | "@types/jasminewd2": "~2.0.3", 35 | "codelyzer": "~4.5.0", 36 | "jasmine-core": "~2.99.1", 37 | "jasmine-spec-reporter": "~4.2.1", 38 | "karma": "~4.1.0", 39 | "karma-chrome-launcher": "~2.2.0", 40 | "karma-coverage-istanbul-reporter": "~2.0.1", 41 | "karma-jasmine": "~1.1.2", 42 | "karma-jasmine-html-reporter": "^0.2.2", 43 | "protractor": "~5.4.0", 44 | "ts-node": "~8.1.0", 45 | "tslint": "~5.16.0", 46 | "typescript": "~3.1.6" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/zwlccc/ionic4-city-picker.git" 51 | }, 52 | "keywords": [ 53 | "ionic4", 54 | "city", 55 | "picker", 56 | "city-picker" 57 | ], 58 | "author": "zwlccc", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/zwlccc/ionic4-city-picker/issues" 62 | }, 63 | "homepage": "https://github.com/zwlccc/ionic4-city-picker#readme" 64 | } 65 | -------------------------------------------------------------------------------- /picker-column/picker-column.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, Watch, h } from '@stencil/core'; 2 | 3 | import { getIonMode } from '../../global/ionic-global'; 4 | import { Gesture, GestureDetail, PickerColumn } from '../../interface'; 5 | import { hapticSelectionChanged } from '../../utils/haptic'; 6 | import { clamp } from '../../utils/helpers'; 7 | 8 | /** 9 | * @internal 10 | */ 11 | @Component({ 12 | tag: 'ion-picker-column', 13 | styleUrls: { 14 | ios: 'picker-column.ios.scss', 15 | md: 'picker-column.md.scss' 16 | } 17 | }) 18 | export class PickerColumnCmp implements ComponentInterface { 19 | 20 | private bounceFrom!: number; 21 | private lastIndex?: number; 22 | private minY!: number; 23 | private maxY!: number; 24 | private optHeight = 0; 25 | private rotateFactor = 0; 26 | private scaleFactor = 1; 27 | private velocity = 0; 28 | private y = 0; 29 | private optsEl?: HTMLElement; 30 | private gesture?: Gesture; 31 | private rafId: any; 32 | private tmrId: any; 33 | private noAnimate = true; 34 | 35 | @Element() el!: HTMLElement; 36 | 37 | /** 38 | * Emitted when the selected value has changed 39 | * @internal 40 | */ 41 | @Event() ionPickerColChange!: EventEmitter; 42 | 43 | /** Picker column data */ 44 | @Prop() col!: PickerColumn; 45 | @Watch('col') 46 | protected colChanged() { 47 | this.refresh(); 48 | } 49 | 50 | async connectedCallback() { 51 | let pickerRotateFactor = 0; 52 | let pickerScaleFactor = 0.81; 53 | 54 | const mode = getIonMode(this); 55 | 56 | if (mode === 'ios') { 57 | pickerRotateFactor = -0.46; 58 | pickerScaleFactor = 1; 59 | } 60 | 61 | this.rotateFactor = pickerRotateFactor; 62 | this.scaleFactor = pickerScaleFactor; 63 | 64 | this.gesture = (await import('../../utils/gesture')).createGesture({ 65 | el: this.el, 66 | gestureName: 'picker-swipe', 67 | gesturePriority: 100, 68 | threshold: 0, 69 | onStart: ev => this.onStart(ev), 70 | onMove: ev => this.onMove(ev), 71 | onEnd: ev => this.onEnd(ev), 72 | }); 73 | this.gesture.enable(); 74 | this.tmrId = setTimeout(() => { 75 | this.noAnimate = false; 76 | this.refresh(true); 77 | }, 250); 78 | } 79 | 80 | componentDidLoad() { 81 | const colEl = this.optsEl; 82 | if (colEl) { 83 | // DOM READ 84 | // We perfom a DOM read over a rendered item, this needs to happen after the first render 85 | this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0); 86 | } 87 | 88 | this.refresh(); 89 | } 90 | 91 | disconnectedCallback() { 92 | cancelAnimationFrame(this.rafId); 93 | clearTimeout(this.tmrId); 94 | if (this.gesture) { 95 | this.gesture.destroy(); 96 | this.gesture = undefined; 97 | } 98 | } 99 | 100 | private emitColChange() { 101 | this.ionPickerColChange.emit(this.col); 102 | } 103 | 104 | private setSelected(selectedIndex: number, duration: number) { 105 | // if there is a selected index, then figure out it's y position 106 | // if there isn't a selected index, then just use the top y position 107 | const y = (selectedIndex > -1) ? -(selectedIndex * this.optHeight) : 0; 108 | 109 | this.velocity = 0; 110 | 111 | // set what y position we're at 112 | cancelAnimationFrame(this.rafId); 113 | this.update(y, duration, true); 114 | 115 | this.emitColChange(); 116 | } 117 | 118 | private update(y: number, duration: number, saveY: boolean) { 119 | if (!this.optsEl) { 120 | return; 121 | } 122 | 123 | // ensure we've got a good round number :) 124 | let translateY = 0; 125 | let translateZ = 0; 126 | const { col, rotateFactor } = this; 127 | const selectedIndex = col.selectedIndex = this.indexForY(-y); 128 | const durationStr = (duration === 0) ? '' : duration + 'ms'; 129 | const scaleStr = `scale(${this.scaleFactor})`; 130 | 131 | const children = this.optsEl.children; 132 | const children_length = this.optsEl.children.length; 133 | const options_length = col.options.length; 134 | const length = children_length < options_length ? options_length : children_length; 135 | for (let i = 0; i < length; i++) { 136 | const button = children[i] as HTMLElement; 137 | const opt = col.options[i]; 138 | const optOffset = (i * this.optHeight) + y; 139 | let transform = ''; 140 | 141 | if (rotateFactor !== 0) { 142 | const rotateX = optOffset * rotateFactor; 143 | if (Math.abs(rotateX) <= 90) { 144 | translateY = 0; 145 | translateZ = 90; 146 | transform = `rotateX(${rotateX}deg) `; 147 | } else { 148 | translateY = -9999; 149 | } 150 | 151 | } else { 152 | translateZ = 0; 153 | translateY = optOffset; 154 | } 155 | 156 | const selected = selectedIndex === i; 157 | transform += `translate3d(0px,${translateY}px,${translateZ}px) `; 158 | if (this.scaleFactor !== 1 && !selected) { 159 | transform += scaleStr; 160 | } 161 | 162 | // Update transition duration 163 | if (this.noAnimate) { 164 | if(opt){ 165 | opt.duration = 0; 166 | } 167 | if(button){ 168 | button.style.transitionDuration = ''; 169 | } 170 | 171 | } else if (opt){ 172 | if (duration !== opt.duration) { 173 | opt.duration = duration; 174 | if(button){ 175 | button.style.transitionDuration = durationStr; 176 | } 177 | } 178 | } 179 | 180 | // Update transform 181 | if(opt){ 182 | if (transform !== opt.transform) { 183 | opt.transform = transform; 184 | if(button){ 185 | button.style.transform = transform; 186 | } 187 | } 188 | } 189 | // Update selected item 190 | if(opt){ 191 | if (selected !== opt.selected) { 192 | opt.selected = selected; 193 | if (selected && button) { 194 | button.classList.add(PICKER_OPT_SELECTED); 195 | } else if (button){ 196 | button.classList.remove(PICKER_OPT_SELECTED); 197 | } 198 | } 199 | } 200 | } 201 | this.col.prevSelected = selectedIndex; 202 | 203 | if (saveY) { 204 | this.y = y; 205 | } 206 | 207 | if (this.lastIndex !== selectedIndex) { 208 | // have not set a last index yet 209 | hapticSelectionChanged(); 210 | this.lastIndex = selectedIndex; 211 | } 212 | } 213 | 214 | private decelerate() { 215 | if (this.velocity !== 0) { 216 | // still decelerating 217 | this.velocity *= DECELERATION_FRICTION; 218 | 219 | // do not let it go slower than a velocity of 1 220 | this.velocity = (this.velocity > 0) 221 | ? Math.max(this.velocity, 1) 222 | : Math.min(this.velocity, -1); 223 | 224 | let y = this.y + this.velocity; 225 | 226 | if (y > this.minY) { 227 | // whoops, it's trying to scroll up farther than the options we have! 228 | y = this.minY; 229 | this.velocity = 0; 230 | 231 | } else if (y < this.maxY) { 232 | // gahh, it's trying to scroll down farther than we can! 233 | y = this.maxY; 234 | this.velocity = 0; 235 | } 236 | 237 | this.update(y, 0, true); 238 | const notLockedIn = (Math.round(y) % this.optHeight !== 0) || (Math.abs(this.velocity) > 1); 239 | if (notLockedIn) { 240 | // isn't locked in yet, keep decelerating until it is 241 | this.rafId = requestAnimationFrame(() => this.decelerate()); 242 | } else { 243 | this.velocity = 0; 244 | this.emitColChange(); 245 | } 246 | 247 | } else if (this.y % this.optHeight !== 0) { 248 | // needs to still get locked into a position so options line up 249 | const currentPos = Math.abs(this.y % this.optHeight); 250 | 251 | // create a velocity in the direction it needs to scroll 252 | this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1); 253 | 254 | this.decelerate(); 255 | } 256 | } 257 | 258 | private indexForY(y: number) { 259 | return Math.min(Math.max(Math.abs(Math.round(y / this.optHeight)), 0), this.col.options.length - 1); 260 | } 261 | 262 | // TODO should this check disabled? 263 | 264 | private onStart(detail: GestureDetail) { 265 | // We have to prevent default in order to block scrolling under the picker 266 | // but we DO NOT have to stop propagation, since we still want 267 | // some "click" events to capture 268 | detail.event.preventDefault(); 269 | detail.event.stopPropagation(); 270 | 271 | // reset everything 272 | cancelAnimationFrame(this.rafId); 273 | const options = this.col.options; 274 | let minY = (options.length - 1); 275 | let maxY = 0; 276 | for (let i = 0; i < options.length; i++) { 277 | if (!options[i].disabled) { 278 | minY = Math.min(minY, i); 279 | maxY = Math.max(maxY, i); 280 | } 281 | } 282 | 283 | this.minY = -(minY * this.optHeight); 284 | this.maxY = -(maxY * this.optHeight); 285 | } 286 | 287 | private onMove(detail: GestureDetail) { 288 | detail.event.preventDefault(); 289 | detail.event.stopPropagation(); 290 | 291 | // update the scroll position relative to pointer start position 292 | let y = this.y + detail.deltaY; 293 | 294 | if (y > this.minY) { 295 | // scrolling up higher than scroll area 296 | y = Math.pow(y, 0.8); 297 | this.bounceFrom = y; 298 | 299 | } else if (y < this.maxY) { 300 | // scrolling down below scroll area 301 | y += Math.pow(this.maxY - y, 0.9); 302 | this.bounceFrom = y; 303 | 304 | } else { 305 | this.bounceFrom = 0; 306 | } 307 | 308 | this.update(y, 0, false); 309 | } 310 | 311 | private onEnd(detail: GestureDetail) { 312 | if (this.bounceFrom > 0) { 313 | // bounce back up 314 | this.update(this.minY, 100, true); 315 | this.emitColChange(); 316 | return; 317 | } else if (this.bounceFrom < 0) { 318 | // bounce back down 319 | this.update(this.maxY, 100, true); 320 | this.emitColChange(); 321 | return; 322 | } 323 | 324 | this.velocity = clamp(-MAX_PICKER_SPEED, detail.velocityY * 23, MAX_PICKER_SPEED); 325 | if (this.velocity === 0 && detail.deltaY === 0) { 326 | const opt = (detail.event.target as Element).closest('.picker-opt'); 327 | if (opt && opt.hasAttribute('opt-index')) { 328 | this.setSelected(parseInt(opt.getAttribute('opt-index')!, 10), TRANSITION_DURATION); 329 | } 330 | 331 | } else { 332 | this.y += detail.deltaY; 333 | this.decelerate(); 334 | } 335 | } 336 | 337 | private refresh(forceRefresh?: boolean) { 338 | let min = this.col.options.length - 1; 339 | let max = 0; 340 | const options = this.col.options; 341 | for (let i = 0; i < options.length; i++) { 342 | if (!options[i].disabled) { 343 | min = Math.min(min, i); 344 | max = Math.max(max, i); 345 | } 346 | } 347 | 348 | /** 349 | * Only update selected value if column has a 350 | * velocity of 0. If it does not, then the 351 | * column is animating might land on 352 | * a value different than the value at 353 | * selectedIndex 354 | */ 355 | if (this.velocity !== 0) { return; } 356 | 357 | const selectedIndex = clamp(min, this.col.selectedIndex || 0, max); 358 | if (this.col.prevSelected !== selectedIndex || forceRefresh) { 359 | const y = (selectedIndex * this.optHeight) * -1; 360 | this.velocity = 0; 361 | this.update(y, TRANSITION_DURATION, true); 362 | } 363 | } 364 | 365 | render() { 366 | const col = this.col; 367 | const Button = 'button' as any; 368 | const mode = getIonMode(this); 369 | return ( 370 | 381 | {col.prefix && ( 382 |
383 | {col.prefix} 384 |
385 | )} 386 |
this.optsEl = el} 390 | > 391 | { col.options.map((o, index) => 392 | 400 | )} 401 |
402 | {col.suffix && ( 403 |
404 | {col.suffix} 405 |
406 | )} 407 |
408 | ); 409 | } 410 | } 411 | 412 | const PICKER_OPT_SELECTED = 'picker-opt-selected'; 413 | const DECELERATION_FRICTION = 0.97; 414 | const MAX_PICKER_SPEED = 90; 415 | const TRANSITION_DURATION = 150; 416 | -------------------------------------------------------------------------------- /src/ionic4-city-picker.model.ts: -------------------------------------------------------------------------------- 1 | export interface CityPickerColumn { 2 | name?: string; 3 | code?: string; 4 | children?: CityPickerColumn[]; 5 | } 6 | 7 | export interface CityData { 8 | province?: string; 9 | city?: string; 10 | region?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/ionic4-city-picker.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonCityPicker } from './ionic4-city-picker'; 3 | 4 | @NgModule({ 5 | declarations: [IonCityPicker], 6 | imports: [ 7 | ], 8 | exports: [IonCityPicker] 9 | }) 10 | export class IonCityPickerModule { } 11 | -------------------------------------------------------------------------------- /src/ionic4-city-picker.scss: -------------------------------------------------------------------------------- 1 | ion-city-picker { 2 | padding-top: 10px; 3 | padding-bottom: 10px; 4 | display: flex; 5 | overflow: hidden; 6 | } 7 | 8 | .city-picker-button{ 9 | left: 0; 10 | top: 0; 11 | margin-left: 0; 12 | margin-right: 0; 13 | margin-top: 0; 14 | margin-bottom: 0; 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | border: 0; 19 | background: transparent; 20 | cursor: pointer; 21 | -webkit-appearance: none; 22 | -moz-appearance: none; 23 | appearance: none; 24 | outline: none; 25 | } 26 | 27 | .city-picker-text { 28 | margin-right: 8px; 29 | font-family: inherit; 30 | font-size: inherit; 31 | font-style: inherit; 32 | font-weight: inherit; 33 | letter-spacing: inherit; 34 | text-decoration: inherit; 35 | text-overflow: inherit; 36 | text-transform: inherit; 37 | text-align: inherit; 38 | white-space: inherit; 39 | color: inherit; 40 | -ms-flex: 1; 41 | flex: 1; 42 | min-height: inherit; 43 | direction: ltr; 44 | overflow: inherit; 45 | } 46 | 47 | .item-label-stacked city-picker, 48 | .item-label-floating city-picker { 49 | padding-left: 0; 50 | 51 | width: 100%; 52 | } 53 | 54 | //set picker column width 55 | .picker-opt{ 56 | margin-left: 4px; 57 | font-size: 15px; 58 | } 59 | 60 | .picker-opts-left{ 61 | .picker-opts{ 62 | .picker-opt{ 63 | margin-left: -8px; 64 | } 65 | } 66 | } 67 | 68 | .picker-opts-right{ 69 | .picker-opts{ 70 | .picker-opt{ 71 | margin-left: 15px; 72 | } 73 | } 74 | } 75 | 76 | .in-item { 77 | position: static; 78 | } 79 | 80 | 81 | .city-picker-placeholder { 82 | color: #999; 83 | } 84 | 85 | .city-picker-disabled, 86 | .item-city-picker-disabled ion-label { 87 | opacity: .3; 88 | pointer-events: none; 89 | } 90 | 91 | .city-picker-readonly { 92 | pointer-events: none; 93 | } -------------------------------------------------------------------------------- /src/ionic4-city-picker.ts: -------------------------------------------------------------------------------- 1 | import { Component, Directive, AfterContentInit, OnDestroy, HostListener, ChangeDetectorRef, Input, Output , EventEmitter, forwardRef, ViewEncapsulation } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | import { PickerController } from '@ionic/angular'; 4 | import { PickerButton, PickerColumn, PickerOptions, PickerColumnOption, Mode, SelectPopoverOption, StyleEventDetail } from '@ionic/core'; 5 | import { CityPickerColumn, CityData } from './ionic4-city-picker.model'; 6 | 7 | export const CITY_PICKER_VALUE_ACCESSOR: any = { 8 | provide: NG_VALUE_ACCESSOR, 9 | useExisting: forwardRef(() => IonCityPicker), 10 | multi: true 11 | }; 12 | 13 | @Component({ 14 | selector: 'ionic4-city-picker', 15 | template: 16 | '
{{cityText}}
' + 17 | '', 25 | styleUrls: ['ionic4-city-picker.scss'], 26 | host: { 27 | '[class.city-picker-disabled]': 'disabled', 28 | '[class.city-picker-readonly]': 'readonly', 29 | '[class.city-picker-placeholder]': 'addPlaceholderClass' 30 | }, 31 | providers: [CITY_PICKER_VALUE_ACCESSOR], 32 | encapsulation: ViewEncapsulation.None, 33 | }) 34 | export class IonCityPicker implements AfterContentInit, ControlValueAccessor { 35 | 36 | private cityIds = 0; 37 | private inputId = `ion-city-${this.cityIds++}`; 38 | public labelId = this.inputId + `-lbl`; 39 | private cityValue: CityData = {}; 40 | public cityText = ''; 41 | public addPlaceholderClass = false; 42 | private buttonEl?: HTMLButtonElement; 43 | public isExpanded = false; 44 | 45 | private isLoadData = false; 46 | 47 | private provinceCol = 0; 48 | private cityCol = 0; 49 | private regionCol = 0; 50 | 51 | private onChange: Function = (randomNumber: number) => {}; 52 | 53 | private onTouch: Function = () => {}; 54 | 55 | /** 56 | * The value of the city string. 57 | */ 58 | @Input() value = ''; 59 | 60 | /** 61 | * The mode determines which platform styles to use. 62 | */ 63 | @Input() mode!: Mode; 64 | 65 | /** 66 | * The name of the control, which is submitted with the form data. 67 | */ 68 | @Input() name: string = this.inputId; 69 | 70 | /** 71 | * If `true`, the user cannot interact with the city. 72 | */ 73 | @Input() disabled = false; 74 | 75 | /** 76 | * If `true`, the city appears normal but is not interactive. 77 | */ 78 | @Input() readonly = false; 79 | 80 | /** 81 | * Any additional options that the picker interface can accept. 82 | * See the [Picker API docs](https://ionicframework.com/docs/api/picker) for the picker options. 83 | */ 84 | @Input() pickerOptions?: SelectPopoverOption; 85 | 86 | /** 87 | * @input {string} The text to display on the picker's cancel button. Default: `Cancel`. 88 | */ 89 | @Input() cancelText = 'Cancel'; 90 | 91 | /** 92 | * @input {string} The text to display on the picker's "Done" button. Default: `Done`. 93 | */ 94 | @Input() doneText = 'Done'; 95 | 96 | /** 97 | * @input {CityPickerColumn} city data 98 | */ 99 | @Input() citiesData: CityPickerColumn[] = []; 100 | 101 | /** 102 | * @input {string} separate 103 | */ 104 | @Input() separator = '-'; 105 | 106 | /** 107 | * The text to display when there's no city selected yet. 108 | * Using lowercase to match the input attribute 109 | */ 110 | @Input() placeholder?: string | null; 111 | 112 | /** 113 | * @output {any} Emitted when the city selection has changed. 114 | */ 115 | @Output() ionChange: EventEmitter = new EventEmitter(); 116 | 117 | /** 118 | * @output {any} Emitted when the city selection was cancelled. 119 | */ 120 | @Output() ionCancel: EventEmitter = new EventEmitter(); 121 | 122 | /** 123 | * Emitted when the styles change. 124 | * @internal 125 | */ 126 | @Output() ionStyle: EventEmitter= new EventEmitter(); 127 | 128 | 129 | constructor( 130 | private pickerCtrl: PickerController, 131 | private changeDetectorRef: ChangeDetectorRef 132 | ) { 133 | } 134 | 135 | ngAfterContentInit() { 136 | this.cityText = this.value; 137 | if (this.cityText === '') { 138 | this.cityText = this.placeholder != null ? this.placeholder : ''; 139 | } 140 | this.hasReadonly(); 141 | this.hasDisable(); 142 | this.updateCityValue(this.value); 143 | this.emitStyle(); 144 | } 145 | 146 | 147 | ngAfterContentChecked() { 148 | if(this.isLoadData || this.citiesData.length ===0){ 149 | return; 150 | } 151 | this.hasPlaceholder(); 152 | this.updateCityValue(this.value); 153 | this.isLoadData = true; 154 | 155 | } 156 | 157 | writeValue(val: any) { 158 | this.setValue(val); 159 | } 160 | 161 | registerOnChange(fn: Function): void { 162 | this.onChange = fn; 163 | } 164 | 165 | registerOnTouched(fn: Function) { 166 | this.onTouch = fn; 167 | } 168 | 169 | @HostListener('click', ['$event.target']) 170 | onClick(ev: UIEvent) { 171 | if (ev.detail === 0) { 172 | // do not continue if the click event came from a form submit 173 | return; 174 | } 175 | this.setFocus(); 176 | this.open(); 177 | } 178 | 179 | @HostListener('keyup', ['$event.target']) 180 | onKeyup(ev: UIEvent) { 181 | if (!this.isExpanded) { 182 | this.open(); 183 | } 184 | } 185 | 186 | private async open() { 187 | if (this.disabled || this.isExpanded) { 188 | return; 189 | } 190 | const pickerOptions = this.generatePickerOptions(); 191 | const picker = await this.pickerCtrl.create(pickerOptions); 192 | 193 | this.isExpanded = true; 194 | 195 | picker.onDidDismiss().then(() => { 196 | this.updateCityValue(this.value); 197 | this.isExpanded = false; 198 | this.setFocus(); 199 | }); 200 | 201 | picker.addEventListener('ionPickerColChange', async (event: any) => { 202 | const data = event.detail; 203 | 204 | if (data.name !== 'province' && data.name !== 'city' && data.name !== 'region') { return; } 205 | 206 | const colSelectedIndex = data.selectedIndex; 207 | const colOptions = data.options; 208 | 209 | const changeData: any = {}; 210 | changeData[data.name] = { 211 | value: colOptions[colSelectedIndex].value 212 | }; 213 | 214 | this.updateCityValue(changeData); 215 | const columns = this.generateColumns(); 216 | 217 | picker.columns = columns; 218 | 219 | await this.valiCity(picker); 220 | }); 221 | await this.valiCity(picker); 222 | 223 | await picker.present(); 224 | } 225 | 226 | private emitStyle() { 227 | this.hasPlaceholder(); 228 | this.ionStyle.emit({ 229 | 'interactive': true, 230 | 'city': true, 231 | 'has-placeholder': this.placeholder != null, 232 | 'has-value': this.hasValue(), 233 | 'interactive-disabled': this.disabled, 234 | }); 235 | } 236 | 237 | private async updateCityValue(value: any) { 238 | this.updateValue(this.cityValue, value, this.separator); 239 | } 240 | 241 | 242 | private generatePickerOptions(): PickerOptions { 243 | const pickerOptions: PickerOptions = { 244 | mode: this.mode, 245 | ...this.pickerOptions, 246 | columns: this.generateColumns() 247 | }; 248 | 249 | // If the user has not passed in picker buttons, 250 | // add a cancel and ok button to the picker 251 | const buttons = pickerOptions.buttons; 252 | if (!buttons || buttons.length === 0) { 253 | pickerOptions.buttons = [ 254 | { 255 | text: this.cancelText, 256 | handler: (data) => { 257 | this.ionCancel.emit(); 258 | } 259 | }, { 260 | text: this.doneText, 261 | handler: (data) => { 262 | this.value = this.convertDuration(data, 'value'); 263 | this.cityText = this.convertDuration(data, 'text'); 264 | this.updateCityValue(data); 265 | this.emitStyle(); 266 | this.ionChange.emit(this); 267 | this.changeDetectorRef.detectChanges(); 268 | } 269 | } 270 | ]; 271 | } 272 | return pickerOptions; 273 | } 274 | 275 | private convertDuration(durationObject, type) { 276 | const province = durationObject.province[type].toString(); 277 | const city = durationObject.city[type].toString(); 278 | const region = durationObject.region[type].toString(); 279 | 280 | return province + this.separator + city + this.separator + region; 281 | } 282 | 283 | 284 | private generateColumns(): PickerColumn[] { 285 | const columns =[]; 286 | // const values = this.value.toString().split(this.separator); 287 | // Add province data to picker 288 | const provinceCol: PickerColumn = { 289 | name: 'province', 290 | options: this.citiesData.map( province => ({text: province.name, value: province.code, disabled: false, duration: 100})) 291 | }; 292 | let provinceIndex = this.citiesData.findIndex( option => option.code === this.cityValue.province); 293 | provinceCol.selectedIndex = provinceIndex === -1 ? 0 : provinceIndex; 294 | columns.push(provinceCol); 295 | 296 | // Add city data to picker 297 | const cityColData = this.citiesData[provinceCol.selectedIndex].children; 298 | const cityCol: PickerColumn = { 299 | name: 'city', 300 | options: cityColData.map( city => ({text: city.name, value: city.code, disabled: false, duration: 100})) 301 | }; 302 | let cityIndex = cityColData.findIndex( option => option.code === this.cityValue.city); 303 | cityCol.selectedIndex = cityIndex === -1 ? 0 : cityIndex; 304 | columns.push(cityCol); 305 | 306 | // Add region data to picker 307 | const regionData = this.citiesData[provinceCol.selectedIndex].children[cityCol.selectedIndex].children; 308 | const regionColCol: PickerColumn = { 309 | name: 'region', 310 | options: regionData.map( city => ({text: city.name, value: city.code, disabled: false, duration: 100})) 311 | }; 312 | let regionIndex = regionData.findIndex( option => option.code === this.cityValue.region); 313 | regionColCol.selectedIndex = regionIndex === -1 ? 0 : regionIndex; 314 | 315 | columns.push(regionColCol); 316 | 317 | this.provinceCol = provinceCol.selectedIndex; 318 | this.cityCol = cityCol.selectedIndex; 319 | this.regionCol = regionColCol.selectedIndex; 320 | return this.divyColumns(columns); 321 | } 322 | 323 | private async valiCity(picker: HTMLIonPickerElement) { 324 | 325 | let columns = picker.columns; 326 | 327 | let provinceCol = columns[0]; 328 | let cityCol = columns[1]; 329 | let regionCol = columns[2]; 330 | 331 | if(cityCol && this.provinceCol != provinceCol.selectedIndex){ 332 | cityCol.selectedIndex = 0; 333 | let cityColData = this.citiesData[provinceCol.selectedIndex].children; 334 | cityCol.options = cityColData.map( city => { return {text: city.name, value: city.code, disabled: false} }); 335 | } 336 | 337 | if(regionCol && (this.cityCol != cityCol.selectedIndex || this.provinceCol != provinceCol.selectedIndex)){ 338 | let regionData = this.citiesData[provinceCol.selectedIndex].children[cityCol.selectedIndex].children; 339 | regionCol.selectedIndex = 0; 340 | regionCol.options = regionData.map( city => {return { text: city.name, value: city.code, disabled: false }} ); 341 | } 342 | 343 | this.provinceCol = provinceCol.selectedIndex; 344 | this.cityCol = cityCol.selectedIndex; 345 | this.regionCol = regionCol.selectedIndex; 346 | } 347 | 348 | setValue(newData: any) { 349 | if (newData === null || newData === undefined) { 350 | this.value = ''; 351 | } else { 352 | this.value = newData; 353 | } 354 | this.updateCityValue(this.value); 355 | this.getText(); 356 | } 357 | 358 | getValue(): string { 359 | return this.value; 360 | } 361 | 362 | getText() { 363 | if ( 364 | this.value === undefined || 365 | this.value === null || 366 | this.value.length === 0 367 | ) { return; } 368 | 369 | this.cityText = this.renderValue(this.value); 370 | return this.cityText; 371 | } 372 | 373 | private hasValue(): boolean { 374 | const val = this.cityValue; 375 | return Object.keys(val).length > 0; 376 | } 377 | 378 | private async setFocus() { 379 | if (this.buttonEl) { 380 | this.buttonEl.focus(); 381 | } 382 | } 383 | 384 | private divyColumns(columns: PickerColumn[]): PickerColumn[] { 385 | const columnsWidth: number[] = []; 386 | let col: PickerColumn; 387 | let width: number; 388 | for (let i = 0; i < columns.length; i++) { 389 | col = columns[i]; 390 | columnsWidth.push(0); 391 | 392 | for (const option of col.options) { 393 | width = option.text!.length; 394 | if (width > columnsWidth[i]) { 395 | columnsWidth[i] = width; 396 | } 397 | } 398 | } 399 | 400 | if (columnsWidth.length === 2) { 401 | width = Math.max(columnsWidth[0], columnsWidth[1]); 402 | columns[0].align = 'right'; 403 | columns[1].align = 'left'; 404 | columns[0].optionsWidth = columns[1].optionsWidth = `${width * 17}px`; 405 | 406 | } else if (columnsWidth.length === 3) { 407 | width = Math.max(columnsWidth[0], columnsWidth[2]); 408 | columns[0].align = 'right'; 409 | columns[2].align = 'left'; 410 | } 411 | return columns; 412 | } 413 | 414 | private updateValue(existingData: CityData, newData: any, separator: string): boolean { 415 | 416 | if (newData && newData !== '') { 417 | 418 | if (typeof newData === 'string') { 419 | // new value is a string 420 | // convert it to our CityData if a valid 421 | newData = this.parseValue(newData); 422 | if (newData) { 423 | // successfully parsed string to our CityData 424 | Object.assign(existingData, newData); 425 | return true; 426 | } 427 | 428 | } else if ((newData.province || newData.city || newData.region)) { 429 | // merge new values from the picker's selection 430 | // to the existing CityData values 431 | for (const key of Object.keys(newData)) { 432 | (existingData as any)[key] = newData[key].value; 433 | } 434 | return true; 435 | } 436 | 437 | } else { 438 | // blank data, clear everything out 439 | for (const k in existingData) { 440 | if (existingData.hasOwnProperty(k)) { 441 | delete (existingData as any)[k]; 442 | } 443 | } 444 | } 445 | return false; 446 | } 447 | 448 | private parseValue(val: string | undefined | null): CityData | undefined { 449 | let parse: any[] | null = null; 450 | 451 | if (val != null && val !== '') { 452 | // try parsing for just value first 453 | parse = val.split(this.separator); 454 | } 455 | 456 | if (parse === null) { 457 | // wasn't able to parse the value 458 | return undefined; 459 | } 460 | 461 | return { 462 | province: parse[0], 463 | city: parse[1], 464 | region: parse[2] 465 | }; 466 | } 467 | 468 | private renderValue(val: string | undefined): string | undefined { 469 | let parse: any[] | null = null; 470 | 471 | if (val != null && val !== '') { 472 | // try parsing for just value first 473 | parse = val.split(this.separator); 474 | } 475 | 476 | if (parse === null) { 477 | // wasn't able to parse the value 478 | return undefined; 479 | } 480 | 481 | let textValue; 482 | this.citiesData.forEach(pro =>{ 483 | if (pro.code !== parse[0]){ 484 | return; 485 | } 486 | textValue = pro.name; 487 | pro.children.forEach(city =>{ 488 | if (city.code !== parse[1]){ 489 | return; 490 | } 491 | textValue = textValue + this.separator + city.name; 492 | city.children.forEach(reg =>{ 493 | if (reg.code !== parse[2]){ 494 | return; 495 | } 496 | textValue = textValue + this.separator + reg.name; 497 | }); 498 | }); 499 | }); 500 | 501 | return textValue; 502 | } 503 | 504 | private async hasPlaceholder(){ 505 | this.addPlaceholderClass = 506 | (this.getText() === undefined && this.placeholder != null) ? true : false; 507 | } 508 | 509 | private async hasReadonly(){ 510 | this.readonly = 511 | (this.readonly.toString() === '' || this.readonly.toString() === 'true' || this.readonly === true) ? true : false; 512 | } 513 | 514 | private async hasDisable(){ 515 | this.disabled = 516 | (this.disabled.toString() === '' || this.disabled.toString() === 'true' || this.disabled === true) ? true : false; 517 | } 518 | 519 | } 520 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "experimentalDecorators": true, 5 | /* Basic Options */ 6 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "incremental": true, /* Enable incremental compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | } 64 | } 65 | --------------------------------------------------------------------------------