├── angular-library-starter.ts ├── src ├── ng2-swipe-cards.ts ├── ng2-swipe-cards.module.ts ├── components │ └── card.ts └── directives │ └── tinder-card.ts ├── license-banner.txt ├── ng2-swipe-cards-0.0.1.tgz ├── public_api.ts ├── rollup.es.config.js ├── tsconfig.json ├── tests └── services │ └── sum.service.spec.ts ├── .gitignore ├── spec.bundle.js ├── LICENSE ├── tsconfig-build.json ├── rollup.config.js ├── tslint.json ├── README.md ├── package.json └── karma.conf.js /angular-library-starter.ts: -------------------------------------------------------------------------------- 1 | export * from './public_api'; 2 | -------------------------------------------------------------------------------- /src/ng2-swipe-cards.ts: -------------------------------------------------------------------------------- 1 | export { SwipeCardsModule } from './ng2-swipe-cards.module'; 2 | -------------------------------------------------------------------------------- /license-banner.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license angular-library-starter 3 | * MIT license 4 | */ 5 | -------------------------------------------------------------------------------- /ng2-swipe-cards-0.0.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viczei/ng2-swipe-cards/HEAD/ng2-swipe-cards-0.0.1.tgz -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Angular library starter 3 | * Build an Angular library compatible with AoT compilation & Tree shaking 4 | * Copyright Roberto Simonetti 5 | * MIT license 6 | * https://github.com/robisim74/angular-library-starter 7 | */ 8 | 9 | /** 10 | * Entry point for all public APIs of the package. 11 | */ 12 | export * from './src/ng2-swipe-cards'; 13 | -------------------------------------------------------------------------------- /rollup.es.config.js: -------------------------------------------------------------------------------- 1 | import sourcemaps from 'rollup-plugin-sourcemaps'; 2 | import license from 'rollup-plugin-license'; 3 | 4 | const path = require('path'); 5 | 6 | export default { 7 | output: { 8 | format: 'es', 9 | sourcemap: true 10 | }, 11 | plugins: [ 12 | sourcemaps(), 13 | license({ 14 | sourceMap: true, 15 | 16 | banner: { 17 | file: path.join(__dirname, 'license-banner.txt'), 18 | encoding: 'utf-8', 19 | } 20 | }) 21 | ], 22 | onwarn: () => { return } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "strict": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "rootDir": ".", 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "target": "es5", 13 | "skipLibCheck": true, 14 | "lib": [ 15 | "es2015", 16 | "dom" 17 | ], 18 | "typeRoots": [ 19 | "./node_modules/@types/" 20 | ] 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /tests/services/sum.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | 3 | import { SumService } from './../../angular-library-starter'; 4 | 5 | describe('SumService', () => { 6 | 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ 10 | SumService 11 | ] 12 | }); 13 | }); 14 | 15 | it('should be calculate the sum', 16 | inject([SumService], 17 | (sumService: SumService) => { 18 | sumService.calculate(45, 78, 90, 674); 19 | expect(sumService.sum).toEqual(887); 20 | }) 21 | ); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | tmp 27 | .tmp 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | /dist 33 | .idea 34 | *.ngfactory.ts 35 | 36 | src/**/*.js 37 | src/**/*.js.map 38 | src/**/*.json 39 | 40 | package-lock.json 41 | 42 | -------------------------------------------------------------------------------- /spec.bundle.js: -------------------------------------------------------------------------------- 1 | import 'core-js'; 2 | import 'zone.js/dist/zone'; 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | 10 | import { getTestBed } from '@angular/core/testing'; 11 | import { 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting 14 | } from '@angular/platform-browser-dynamic/testing'; 15 | 16 | import 'rxjs'; 17 | 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | 23 | const testContext = require.context('./tests', true, /\.spec\.ts/); 24 | 25 | function requireAll(requireContext) { 26 | return requireContext.keys().map(requireContext); 27 | } 28 | 29 | const modules = requireAll(testContext); 30 | -------------------------------------------------------------------------------- /src/ng2-swipe-cards.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { CardComponent } from './components/card'; 5 | import { TinderCardDirective } from './directives/tinder-card'; 6 | 7 | import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; 8 | 9 | export class HammerConfig extends HammerGestureConfig { 10 | overrides = { 11 | 'pan': { enable: true } 12 | } 13 | } 14 | 15 | @NgModule({ 16 | imports: [ 17 | CommonModule 18 | ], 19 | declarations: [ 20 | CardComponent, 21 | TinderCardDirective 22 | ], 23 | exports: [ 24 | CardComponent, 25 | TinderCardDirective 26 | ], 27 | providers: [{ 28 | provide: HAMMER_GESTURE_CONFIG, 29 | useClass: HammerConfig 30 | }] 31 | }) 32 | export class SwipeCardsModule { } 33 | 34 | export { CardComponent } from './components/card'; 35 | export { TinderCardDirective } from './directives/tinder-card'; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ViCode 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 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "baseUrl": ".", 5 | "paths": { 6 | "@angular/*": [ 7 | "node_modules/@angular/*" 8 | ] 9 | }, 10 | "outDir": "dist", 11 | "declaration": true, 12 | "strict": true, 13 | "moduleResolution": "node", 14 | "module": "es2015", 15 | "target": "es2015", 16 | "lib": [ 17 | "es2015", 18 | "dom" 19 | ], 20 | "skipLibCheck": true, 21 | "types": [], 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | "sourceMap": true, 25 | "inlineSources": true 26 | }, 27 | "files": [ 28 | "public_api.ts", 29 | "node_modules/zone.js/dist/zone.js.d.ts" 30 | ], 31 | "angularCompilerOptions": { 32 | "skipTemplateCodegen": true, 33 | "annotateForClosureCompiler": true, 34 | "strictMetadataEmit": true, 35 | "flatModuleOutFile": "ng2-swipe-cards.js", 36 | "flatModuleId": "ng2-swipe-cards" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import sourcemaps from 'rollup-plugin-sourcemaps'; 3 | 4 | /** 5 | * Add here external dependencies that actually you use. 6 | * 7 | * About RxJS 8 | * Each RxJS functionality that you use in the library must be added as external dependency. 9 | * - For main classes use 'Rx': 10 | * e.g. import { Observable } from 'rxjs/Observable'; => 'rxjs/Observable': 'Rx' 11 | * - For observable methods use 'Rx.Observable': 12 | * e.g. import 'rxjs/add/observable/merge'; => 'rxjs/add/observable/merge': 'Rx.Observable' 13 | * or for lettable operators: 14 | * e.g. import { merge } from 'rxjs/observable/merge'; => 'rxjs/observable/merge': 'Rx.Observable' 15 | * - For operators use 'Rx.Observable.prototype': 16 | * e.g. import 'rxjs/add/operator/map'; => 'rxjs/add/operator/map': 'Rx.Observable.prototype' 17 | * or for lettable operators: 18 | * e.g. import { map } from 'rxjs/operators'; => 'rxjs/operators': 'Rx.Observable.prototype' 19 | */ 20 | const globals = { 21 | '@angular/core': 'ng.core', 22 | '@angular/common': 'ng.common', 23 | 'rxjs/Observable': 'Rx', 24 | 'rxjs/Observer': 'Rx' 25 | }; 26 | 27 | export default { 28 | external: Object.keys(globals), 29 | plugins: [resolve(), sourcemaps()], 30 | onwarn: () => { return }, 31 | output: { 32 | format: 'umd', 33 | name: 'ng.angularLibraryStarter', 34 | globals: globals, 35 | sourcemap: true, 36 | exports: 'named' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "node_modules/codelyzer" 5 | ], 6 | "rules": { 7 | "angular-whitespace": [ 8 | true, 9 | "check-interpolation", 10 | "check-pipe" 11 | ], 12 | "banana-in-box": true, 13 | "templates-no-negated-async": true, 14 | "directive-selector": [ 15 | true, 16 | "attribute", 17 | [ 18 | "dir-prefix1", 19 | "dir-prefix2" 20 | ], 21 | "camelCase" 22 | ], 23 | "component-selector": [ 24 | true, 25 | "element", 26 | [ 27 | "cmp-prefix1", 28 | "cmp-prefix2" 29 | ], 30 | "kebab-case" 31 | ], 32 | "use-input-property-decorator": true, 33 | "use-output-property-decorator": true, 34 | "use-host-property-decorator": true, 35 | "no-attribute-parameter-decorator": true, 36 | "no-input-rename": true, 37 | "no-output-rename": true, 38 | "no-forward-ref": true, 39 | "use-view-encapsulation": false, 40 | "use-life-cycle-interface": true, 41 | "use-pipe-transform-interface": true, 42 | "pipe-naming": [ 43 | true, 44 | "camelCase", 45 | "Pipe" 46 | ], 47 | "component-class-suffix": [ 48 | true, 49 | "Component" 50 | ], 51 | "directive-class-suffix": [ 52 | true, 53 | "Directive" 54 | ], 55 | "ordered-imports": [ 56 | false 57 | ], 58 | "quotemark": [ 59 | false 60 | ], 61 | "trailing-comma": [ 62 | false 63 | ] 64 | } 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng2-swipe-cards 2 | A kit of cards (including tinder-card) for angular2 3 | 4 | # Demo 5 | https://embed.plnkr.co/ebTZz51SsYUs7wzLemuo/ 6 | 7 | # Installation 8 | ```bash 9 | $ npm install ng2-swipe-cards --save 10 | ``` 11 | 12 | # Usage 13 | 14 | For webpack consumers, first, import SwipeableCardModule to your entry AppModule file, 15 | 16 | ```bash 17 | // Root app module file 18 | import { NgModule } from '@angular/core'; 19 | import { BrowserModule } from '@angular/platform-browser'; 20 | import { SwipeCardsModule } from 'ng2-swipe-cards'; 21 | 22 | import { AppComponent } from './app/'; 23 | 24 | @NgModule({ 25 | imports: [ 26 | BrowserModule, 27 | SwipeCardsModule 28 | ... 29 | ], 30 | declarations: [AppComponent], 31 | bootstrap: [AppComponent] 32 | }); 33 | ``` 34 | 35 | Then, import ng2-swipe-cards and hammerjs in your vendor.ts file, 36 | ```bash 37 | // vendor.ts file 38 | import '@angular/common'; 39 | import '@angular/core'; 40 | ... 41 | 42 | import 'hammerjs'; 43 | import 'ng2-swipe-cards'; 44 | ``` 45 | 46 | # Documentation 47 | 48 | The package contains one component called: sc-card. it basically display a card that you can drag and drop. 49 | 50 | It's inputs are: 51 | fixed: Boolean => set condition to forbid any drag and drop 52 | orientation: String => set 'x' to only allow horizontal drags, set 'y' to only allow vertical drags and 'xy' for both 53 | callDestroy: EventEmitter => emit to destroy the card 54 | 55 | It's outputs are: 56 | onRelease: EventEmitter => called when the card is dropped outside it's release radius 57 | onSwipe: EventEmitter => called when the card is dragged 58 | onAbort: EventEmitter => called when the card is dropped inside it's release radius. 59 | 60 | 61 | The package contains a directive called: tinder-card. It reproduce the like/dislike functions from the tinder card application. 62 | 63 | It's inputs are: 64 | tinder-card: object => set an object with this pattern: 65 | ```bash 66 | { 67 | 'like': 68 | { 69 | 'backgroundColor': 'green', 70 | 'img': 'anyLikeImage.png' 71 | }, 72 | 'dislike': 73 | { 74 | 'backgroundColor': 'red', 75 | 'img': 'anyDislikeImage.png' 76 | } 77 | } 78 | ``` 79 | It will automatically generate an overlay above your card which display the 'backgroundColor' and 'img' depending on whether the card is beeing liked or disliked. (This is optional) 80 | callLike: EventEmitter => emit to force a like action on the card heap. You can set as argument a boolean as liking. 81 | 82 | It's ouputs are: 83 | onLike: EventEmitter => called when the card is liked or disliked from the release action. 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-swipe-cards", 3 | "version": "1.1.1", 4 | "description": "Build an Angular library compatible with AoT compilation and Tree shaking", 5 | "main": "./bundles/ng2-swipe-cards.umd.js", 6 | "module": "./esm5/ng2-swipe-cards.js", 7 | "es2015": "./esm2015/ng2-swipe-cards.js", 8 | "scripts": { 9 | "build": "node build.js", 10 | "test": "karma start", 11 | "pack-lib": "npm pack ./dist", 12 | "publish-lib": "npm publish ./dist", 13 | "publish-lib:next": "npm publish --tag next ./dist", 14 | "compodoc": "compodoc -p tsconfig.json", 15 | "compodoc-serve": "compodoc -s" 16 | }, 17 | "typings": "./ng2-swipe-cards.d.ts", 18 | "author": "", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/robisim74/ng2-swipe-cards.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/robisim74/ng2-swipe-cards/issues" 25 | }, 26 | "homepage": "https://github.com/robisim74/ng2-swipe-cards", 27 | "keywords": [ 28 | "angular", 29 | "angular2", 30 | "card", 31 | "tinder" 32 | ], 33 | "license": "MIT", 34 | "dependencies": { 35 | "@angular/common": "^2.0.1", 36 | "@angular/core": "^2.0.1", 37 | "@angular/forms": "^2.0.1", 38 | "@angular/platform-browser": "^2.0.1", 39 | "@angular/platform-browser-dynamic": "^2.0.1", 40 | "@angular/platform-server": "^2.0.1", 41 | "@angular/router": "^3.0.1", 42 | "hammerjs": "^2.0.8", 43 | "ng-bootstrap": "^1.1.16-1", 44 | "@types/hammerjs": "2.0.33" 45 | }, 46 | "peerDependencies": { 47 | "@angular/common": ">= 5.0.0", 48 | "@angular/core": ">= 5.0.0" 49 | }, 50 | "devDependencies": { 51 | "@angular/animations": "5.0.0", 52 | "@angular/common": "5.0.0", 53 | "@angular/compiler": "5.0.0", 54 | "@angular/compiler-cli": "5.0.0", 55 | "@angular/core": "5.0.0", 56 | "@angular/platform-browser": "5.0.0", 57 | "@angular/platform-browser-dynamic": "5.0.0", 58 | "@angular/platform-server": "5.0.0", 59 | "@compodoc/compodoc": "1.0.3", 60 | "@types/jasmine": "2.6.2", 61 | "@types/node": "8.0.47", 62 | "@types/hammerjs": "^2.0.33", 63 | "chalk": "2.3.0", 64 | "codelyzer": "4.0.0", 65 | "core-js": "2.5.1", 66 | "jasmine-core": "2.8.0", 67 | "karma": "1.7.1", 68 | "karma-chrome-launcher": "2.2.0", 69 | "karma-jasmine": "1.1.0", 70 | "karma-sourcemap-loader": "0.3.7", 71 | "karma-spec-reporter": "0.0.31", 72 | "karma-webpack": "2.0.5", 73 | "karma-coverage-istanbul-reporter": "1.3.0", 74 | "istanbul-instrumenter-loader": "3.0.0", 75 | "reflect-metadata": "0.1.10", 76 | "rollup": "0.50.0", 77 | "rollup-plugin-node-resolve": "3.0.0", 78 | "rollup-plugin-sourcemaps": "0.4.2", 79 | "rollup-plugin-license": "0.5.0", 80 | "rxjs": "5.5.2", 81 | "shelljs": "0.7.8", 82 | "source-map-loader": "0.2.3", 83 | "ts-loader": "3.1.1", 84 | "tslint": "5.8.0", 85 | "typescript": "2.4.2", 86 | "uglify-js": "3.1.6", 87 | "webpack": "3.8.1", 88 | "zone.js": "0.8.18" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for Unit testing 2 | 3 | const path = require('path'); 4 | 5 | module.exports = function (config) { 6 | 7 | const configuration = { 8 | 9 | // base path that will be used to resolve all patterns (eg. files, exclude) 10 | basePath: '', 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | plugins: [ 17 | require('karma-jasmine'), 18 | require('karma-chrome-launcher'), 19 | require('karma-webpack'), 20 | require('karma-sourcemap-loader'), 21 | require('karma-spec-reporter'), 22 | require('karma-coverage-istanbul-reporter'), 23 | require("istanbul-instrumenter-loader") 24 | ], 25 | 26 | // list of files / patterns to load in the browser 27 | files: [ 28 | { pattern: 'spec.bundle.js', watched: false } 29 | ], 30 | 31 | // list of files to exclude 32 | exclude: [ 33 | ], 34 | 35 | // preprocess matching files before serving them to the browser 36 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 37 | preprocessors: { 38 | 'spec.bundle.js': ['webpack', 'sourcemap'] 39 | }, 40 | 41 | // webpack 42 | webpack: { 43 | resolve: { 44 | extensions: ['.ts', '.js'] 45 | }, 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.ts/, 50 | use: [ 51 | { loader: 'ts-loader' }, 52 | { loader: 'source-map-loader' } 53 | ], 54 | exclude: /node_modules/ 55 | }, 56 | { 57 | enforce: 'post', 58 | test: /\.ts/, 59 | use: [ 60 | { 61 | loader: 'istanbul-instrumenter-loader', 62 | options: { esModules: true } 63 | } 64 | ], 65 | exclude: [ 66 | /\.spec.ts/, 67 | /node_modules/ 68 | ] 69 | } 70 | ], 71 | exprContextCritical: false 72 | }, 73 | devtool: 'inline-source-map', 74 | performance: { hints: false } 75 | }, 76 | 77 | webpackServer: { 78 | noInfo: true 79 | }, 80 | 81 | 82 | // test results reporter to use 83 | // possible values: 'dots', 'progress' 84 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 85 | reporters: ['spec', 'coverage-istanbul'], 86 | 87 | coverageIstanbulReporter: { 88 | reports: ['html', 'lcovonly'], 89 | dir: path.join(__dirname, 'coverage'), 90 | fixWebpackSourcePaths: true 91 | }, 92 | 93 | 94 | // web server port 95 | port: 9876, 96 | 97 | 98 | // enable / disable colors in the output (reporters and logs) 99 | colors: true, 100 | 101 | 102 | // level of logging 103 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 104 | logLevel: config.LOG_INFO, 105 | 106 | 107 | // enable / disable watching file and executing tests whenever any file changes 108 | autoWatch: true, 109 | 110 | 111 | // start these browsers 112 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 113 | browsers: ['Chrome'], 114 | 115 | 116 | // Continuous Integration mode 117 | // if true, Karma captures browsers, runs the tests and exits 118 | singleRun: true 119 | 120 | }; 121 | 122 | config.set(configuration); 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/components/card.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | HostListener, 6 | Renderer, 7 | Input, 8 | Output, 9 | OnInit, 10 | OnDestroy 11 | } from '@angular/core'; 12 | 13 | @Component({ 14 | template: '', 15 | selector: 'sc-card', 16 | styles: [`:host { 17 | transition: transform 1s ease; 18 | position: absolute; 19 | border-radius: 2px; 20 | border: 1px solid white; 21 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; 22 | transition: transform 0.2s ease; 23 | background-color: white; 24 | touch-action: none !important; 25 | } 26 | :host(.card-heap) { 27 | transition: transform 1s ease; 28 | display: block; 29 | position: absolute; 30 | background-color: white; 31 | box-shadow: 0 0 0 rgba(0, 0, 0, 0) !important; 32 | transform: perspective(400px) translate3d(0, 30px, -30px); 33 | visibility: hidden; 34 | } 35 | 36 | :host(.card-heap):nth-child(1) { 37 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; 38 | z-index:3; 39 | visibility: visible; 40 | transform: perspective(400px) translate3d(0, 0px, 0px); 41 | } 42 | :host(.card-heap):nth-child(2) { 43 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; 44 | z-index:2; 45 | visibility: visible; 46 | transform: perspective(400px) translate3d(0, 30px, -30px); 47 | } 48 | :host(.card-heap):nth-child(3) { 49 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; 50 | z-index:1; 51 | visibility: visible; 52 | transform: perspective(400px) translate3d(0, 60px, -60px); 53 | } 54 | 55 | :host .card-overlay { 56 | transform: translateZ(0); 57 | opacity: 0; 58 | border-radius: 2px; 59 | position: absolute; 60 | width: 100%; 61 | height: 10px; 62 | top: 0; 63 | left: 0; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | overflow: hidden; 68 | color: white; 69 | } 70 | `] 71 | }) 72 | export class CardComponent implements OnInit, OnDestroy { 73 | @Input() fixed: Boolean = false; 74 | _orientation: string = 'xy'; 75 | @Input('orientation') 76 | set orientation(value: string) { 77 | this._orientation = value || "xy"; 78 | } 79 | _callDestroy: EventEmitter; 80 | @Input('callDestroy') 81 | set callDestroy(value: EventEmitter) { 82 | this._callDestroy = value || new EventEmitter(); 83 | this.initCallDestroy(); 84 | } 85 | releaseRadius: any; 86 | released: Boolean = false; 87 | 88 | @Output() onRelease: EventEmitter = new EventEmitter(); 89 | @Output() onSwipe: EventEmitter = new EventEmitter(); 90 | @Output() onAbort: EventEmitter = new EventEmitter(); 91 | 92 | element: HTMLElement; 93 | 94 | direction: any = { x: 0, y: 0 }; 95 | 96 | constructor(protected el: ElementRef, public renderer: Renderer) { 97 | this.element = el.nativeElement; 98 | } 99 | 100 | translate(params: any) { 101 | if (!this.fixed && !this.released) { 102 | this.renderer.setElementStyle(this.element, "transition", "transform " + (params.time || 0) + "s ease"); 103 | this.renderer.setElementStyle(this.element, "webkitTransform", "translate3d(" + 104 | (params.x && (!this._orientation || this._orientation.indexOf("x") != -1) ? (params.x) : 0) + 105 | "px, " + 106 | (params.y && (!this._orientation || this._orientation.indexOf("y") != -1) ? (params.y) : 0) + 107 | "px, 0) rotate(" + (params.rotate || 0) + "deg)"); 108 | } 109 | } 110 | 111 | onSwipeCb(event: any) { 112 | let rotate = ((event.deltaX * 20) / this.element.clientWidth); 113 | this.direction.x = event.deltaX > 0 ? 1 : -1; 114 | this.direction.y = event.deltaY > 0 ? 1 : -1; 115 | this.translate({ 116 | x: event.deltaX, 117 | y: event.deltaY 118 | }); 119 | } 120 | 121 | onAbortCb(event: any) { 122 | this.translate({ 123 | x: 0, 124 | y: 0, 125 | rotate: 0, 126 | time: 0.2 127 | }); 128 | } 129 | 130 | ngOnInit() { 131 | this._callDestroy = this._callDestroy || new EventEmitter(); 132 | this.initCallDestroy(); 133 | } 134 | 135 | initCallDestroy() { 136 | this._callDestroy.subscribe((delay: number) => { 137 | this.destroy(delay); 138 | }); 139 | } 140 | 141 | destroy(delay: number = 0) { 142 | setTimeout(() => { 143 | this.element.remove(); 144 | }, 200); 145 | } 146 | 147 | ngAfterViewChecked() { 148 | if (this.element.parentElement) { 149 | let height = this.element.parentElement.clientHeight; 150 | let width = this.element.parentElement.clientWidth; 151 | this.renderer.setElementStyle(this.element, "height", height + 'px'); 152 | this.renderer.setElementStyle(this.element, "width", width + 'px'); 153 | this.releaseRadius = { 154 | x: width / 4, 155 | y: height / 4 156 | }; 157 | } 158 | } 159 | 160 | @HostListener('pan', ['$event']) 161 | onPan(event: any) { 162 | if (!this.fixed && !this.released) { 163 | this.onSwipeCb(event); 164 | if (this.onSwipe) { 165 | this.onSwipe.emit(event); 166 | } 167 | } 168 | } 169 | 170 | @HostListener('panend', ['$event']) 171 | onPanEnd(event: any) { 172 | if (!this.fixed && !this.released) { 173 | if ( 174 | (this._orientation == "x" && (this.releaseRadius.x < event.deltaX || this.releaseRadius.x * -1 > event.deltaX)) || 175 | (this._orientation == "y" && (this.releaseRadius.y < event.deltaY || this.releaseRadius.y * -1 > event.deltaY)) || 176 | ((this.releaseRadius.x < event.deltaX || this.releaseRadius.x * -1 > event.deltaX) || 177 | (this.releaseRadius.y < event.deltaY || this.releaseRadius.y * -1 > event.deltaY)) 178 | ) { 179 | if (this.onRelease) { 180 | this.released = true; 181 | this.onRelease.emit(event); 182 | } 183 | } else { 184 | this.onAbortCb(event); 185 | if (this.onAbort) { 186 | this.onAbort.emit(event); 187 | } 188 | } 189 | } 190 | } 191 | 192 | ngOnDestroy() { 193 | if (this._callDestroy) { 194 | this._callDestroy.unsubscribe(); 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/directives/tinder-card.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Directive, 4 | ElementRef, 5 | EventEmitter, 6 | HostListener, 7 | Renderer, 8 | Input, 9 | Output, 10 | OnInit, 11 | OnDestroy 12 | } 13 | from '@angular/core'; 14 | 15 | 16 | @Directive({ 17 | selector: '[tinder-card]', 18 | host: { 19 | class: 'card-heap' 20 | } 21 | }) 22 | export class TinderCardDirective implements OnInit, OnDestroy { 23 | _overlay: any; 24 | @Input('tinder-card') 25 | set overlay(value: any) { 26 | this._overlay = value || {}; 27 | } 28 | _callLike: EventEmitter; 29 | @Input('callLike') 30 | set callLike(value: EventEmitter) { 31 | this._callLike = value || new EventEmitter(); 32 | this.initCallLike(); 33 | } 34 | 35 | @Input() fixed: boolean; 36 | @Input() orientation: string = 'xy'; 37 | 38 | @Output() onLike: EventEmitter = new EventEmitter(); 39 | 40 | like: boolean; 41 | 42 | element: HTMLElement; 43 | renderer: Renderer; 44 | overlayElement: HTMLElement; 45 | 46 | constructor(el: ElementRef, renderer: Renderer) { 47 | this.renderer = renderer; 48 | this.element = el.nativeElement; 49 | } 50 | 51 | onReleaseLikeCb(event: any) { 52 | this.like = event.like; 53 | let el = this.element; 54 | let x = (el.offsetWidth + el.clientWidth) * ((!!event.like ? 1 : -1) || 0); 55 | let rotate = (x * 20) / el.clientWidth; 56 | 57 | if (this._overlay) { 58 | let overlayElm = this.element.querySelector('.tinder-overlay'); 59 | this.renderer.setElementStyle(overlayElm, "transition", "transform 0.6s ease"); 60 | this.renderer.setElementStyle(overlayElm, "opacity", "0.5"); 61 | } 62 | } 63 | 64 | onSwipeLikeCb(event: any) { 65 | if (this._overlay) { 66 | let overlayElm = this.element.querySelector('.tinder-overlay'); 67 | this.renderer.setElementStyle(overlayElm, "transition", "opacity 0s ease"); 68 | let opacity = (event.distance < 0 ? event.distance * -1 : event.distance) * 0.5 / this.element.offsetWidth; 69 | this.renderer.setElementStyle(overlayElm, "opacity", opacity.toString()); 70 | } 71 | } 72 | 73 | destroy(delay: number = 0) { 74 | setTimeout(() => { 75 | this.element.remove(); 76 | }, 200); 77 | } 78 | 79 | @HostListener('onSwipe', ['$event']) 80 | onSwipe(event: any) { 81 | let like = (this.orientation === "y" && event.deltaY < 0) || 82 | (this.orientation !== "y" && event.deltaX > 0); 83 | let opacity = (event.distance < 0 ? event.distance * -1 : event.distance) * 0.5 / this.element.offsetWidth; 84 | if (!!this._overlay) { 85 | this.renderer.setElementStyle(this.overlayElement, "transition", "opacity 0s ease"); 86 | this.renderer.setElementStyle(this.overlayElement, "opacity", opacity.toString()); 87 | this.renderer.setElementStyle(this.overlayElement, "background-color", this._overlay[like ? "like" : "dislike"].backgroundColor); 88 | } 89 | this.translate({ 90 | x: event.deltaX, 91 | y: event.deltaY, 92 | rotate: ((event.deltaX * 20) / this.element.clientWidth) 93 | }); 94 | } 95 | 96 | @HostListener('onAbort', ['$event']) 97 | onAbort(event: any) { 98 | if (!!this._overlay) { 99 | this.renderer.setElementStyle(this.overlayElement, "transition", "opacity 0.2s ease"); 100 | this.renderer.setElementStyle(this.overlayElement, "opacity", "0"); 101 | } 102 | } 103 | 104 | @HostListener('onRelease', ['$event']) 105 | onRelease(event: any) { 106 | let like = (this.orientation === "y" && event.deltaY < 0) || 107 | (this.orientation !== "y" && event.deltaX > 0); 108 | if (this._callLike) { 109 | this._callLike.emit({ like }); 110 | } 111 | if (this.onLike) { 112 | this.onLike.emit({ like }); 113 | } 114 | } 115 | 116 | translate(params: any) { 117 | if (!this.fixed) { 118 | this.renderer.setElementStyle(this.element, "transition", "transform " + (params.time || 0) + "s ease"); 119 | this.renderer.setElementStyle(this.element, "webkitTransform", "translate3d(" + 120 | (params.x && (!this.orientation || this.orientation.indexOf("x") != -1) ? (params.x) : 0) + 121 | "px, " + 122 | (params.y && (!this.orientation || this.orientation.indexOf("y") != -1) ? (params.y) : 0) + 123 | "px, 0) rotate(" + 124 | params.rotate + 125 | "deg)"); 126 | } 127 | } 128 | 129 | initOverlay() { 130 | if (!!this._overlay) { 131 | this.overlayElement = document.createElement("div"); 132 | this.overlayElement.className += " card-overlay"; 133 | this.element.appendChild(this.overlayElement); 134 | this.renderer.setElementStyle(this.overlayElement, "transform", "translateZ(0)"); 135 | this.renderer.setElementStyle(this.overlayElement, "opacity", "0"); 136 | this.renderer.setElementStyle(this.overlayElement, "border-radius", "2px"); 137 | this.renderer.setElementStyle(this.overlayElement, "position", "absolute"); 138 | this.renderer.setElementStyle(this.overlayElement, "width", "100%"); 139 | this.renderer.setElementStyle(this.overlayElement, "height", "100%"); 140 | this.renderer.setElementStyle(this.overlayElement, "top", "0"); 141 | this.renderer.setElementStyle(this.overlayElement, "left", "0"); 142 | this.renderer.setElementStyle(this.overlayElement, "display", "flex"); 143 | this.renderer.setElementStyle(this.overlayElement, "align-items", "center"); 144 | this.renderer.setElementStyle(this.overlayElement, "justify-content", "center"); 145 | this.renderer.setElementStyle(this.overlayElement, "overflow", "hidden"); 146 | this.renderer.setElementStyle(this.overlayElement, "color", "white"); 147 | } 148 | } 149 | 150 | ngOnInit() { 151 | this.initOverlay(); 152 | 153 | this._overlay = this._overlay || {}; 154 | this.orientation = this.orientation || "xy"; 155 | this._callLike = this._callLike || new EventEmitter(); 156 | this.initCallLike(); 157 | } 158 | 159 | initCallLike() { 160 | this._callLike.subscribe((params: any) => { 161 | let el = this.element; 162 | let x = (el.offsetWidth + el.clientWidth) * (params.like ? 1 : -1); 163 | let y = (el.offsetHeight + el.clientHeight) * (params.like ? -1 : 1); 164 | this.translate({ 165 | x: x, 166 | y: y, 167 | rotate: (x * 20) / el.clientWidth, 168 | time: 0.8 169 | }); 170 | this.renderer.setElementStyle(this.overlayElement, "transition", "opacity 0.4s ease"); 171 | this.renderer.setElementStyle(this.overlayElement, "opacity", "0.5"); 172 | this.renderer.setElementStyle(this.overlayElement, "background-color", this._overlay[params.like ? "like" : "dislike"].backgroundColor); 173 | this.destroy(200); 174 | }); 175 | } 176 | 177 | ngOnDestroy() { 178 | if (this._callLike) { 179 | this._callLike.unsubscribe(); 180 | } 181 | } 182 | } 183 | --------------------------------------------------------------------------------