├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── typings.d.ts ├── app │ └── modules │ │ └── ionic-swing │ │ ├── swing │ │ ├── direction.ts │ │ ├── utilities.ts │ │ ├── stack.ts │ │ └── card.ts │ │ ├── directives │ │ ├── swing-card.directive.ts │ │ └── swing-stack.directive.ts │ │ ├── ionic-swing.module.ts │ │ └── interfaces │ │ └── swing.ts ├── tsconfig.app.json ├── index.html ├── main.ts ├── tsconfig.spec.json ├── test.ts └── polyfills.ts ├── index.ts ├── ng-package.json ├── .editorconfig ├── tsconfig.json ├── .npmignore ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── package.json ├── README.md ├── tslint.json ├── angular.json └── CHANGELOG.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/app/modules/ionic-swing/ionic-swing.module'; 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluster-app/ionic-swing/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "index.ts" 5 | }, 6 | "whitelistedNonPeerDependencies": ["."] 7 | } 8 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/swing/direction.ts: -------------------------------------------------------------------------------- 1 | export const Direction = { 2 | DOWN: Symbol('DOWN'), 3 | INVALID: Symbol('INVALID'), 4 | LEFT: Symbol('LEFT'), 5 | RIGHT: Symbol('RIGHT'), 6 | UP: Symbol('UP') 7 | }; 8 | 9 | export default Direction; 10 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IonicSwing 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts", 15 | "polyfills.ts" 16 | ], 17 | "include": [ 18 | "**/*.spec.ts", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/swing/utilities.ts: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | /** 4 | * Return direct children elements. 5 | * 6 | * @see http://stackoverflow.com/a/27102446/368691 7 | * param {HTMLElement} element 8 | * returns {Array} 9 | */ 10 | const elementChildren = (element) => { 11 | return _.filter(element.childNodes, (elem) => { 12 | return elem.nodeType === 1; 13 | }); 14 | }; 15 | 16 | /** 17 | * @see http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886 18 | * returns {boolean} 19 | */ 20 | const isTouchDevice = () => { 21 | return 'ontouchstart' in window || navigator.msMaxTouchPoints; 22 | }; 23 | 24 | export { 25 | elementChildren, 26 | isTouchDevice 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/directives/swing-card.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, OnInit} from '@angular/core'; 2 | 3 | import {SwingStackDirective} from './swing-stack.directive'; 4 | import {Card} from '../interfaces/swing'; 5 | 6 | @Directive({ 7 | selector: '[swingCard]' 8 | }) 9 | export class SwingCardDirective implements OnInit { 10 | private card: Card; 11 | 12 | constructor( 13 | private elementRef: ElementRef, 14 | private swingStack: SwingStackDirective) { 15 | } 16 | 17 | ngOnInit() { 18 | this.card = this.swingStack.addCard(this); 19 | } 20 | 21 | getElementRef() { 22 | return this.elementRef; 23 | } 24 | 25 | getNativeElement() { 26 | return this.elementRef.nativeElement; 27 | } 28 | 29 | getCard(): Card { 30 | return this.card; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/ionic-swing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {SwingCardDirective} from './directives/swing-card.directive'; 5 | import {SwingStackDirective} from './directives/swing-stack.directive'; 6 | 7 | export * from './directives/swing-card.directive'; 8 | export * from './directives/swing-stack.directive'; 9 | 10 | export * from './interfaces/swing'; 11 | 12 | export * from './swing/card'; 13 | export * from './swing/direction'; 14 | export * from './swing/stack'; 15 | export * from './swing/utilities'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule 20 | ], 21 | declarations: [ 22 | SwingCardDirective, 23 | SwingStackDirective 24 | ], 25 | exports: [ 26 | SwingCardDirective, 27 | SwingStackDirective 28 | ] 29 | }) 30 | export class IonicSwingModule { 31 | } 32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 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 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-swing", 3 | "version": "4.1.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "packagr": "ng-packagr -p ng-package.json" 13 | }, 14 | "private": false, 15 | "dependencies": { 16 | "@angular/animations": "^7.1.3", 17 | "@angular/common": "^7.1.3", 18 | "@angular/compiler": "^7.1.3", 19 | "@angular/core": "^7.1.3", 20 | "@angular/forms": "^7.1.3", 21 | "@angular/http": "^7.1.3", 22 | "@angular/platform-browser": "^7.1.3", 23 | "@angular/platform-browser-dynamic": "^7.1.3", 24 | "@angular/router": "^7.1.3", 25 | "core-js": "^2.6.1", 26 | "raf": "^3.4.1", 27 | "rebound": "^0.1.0", 28 | "sister": "^3.0.1", 29 | "underscore": "^1.9.1", 30 | "vendor-prefix": "^0.1.0", 31 | "zone.js": "^0.8.26" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "0.11.3", 35 | "@angular/cli": "^7.0.6", 36 | "@angular/compiler-cli": "^7.1.3", 37 | "@angular/language-service": "^7.1.3", 38 | "@types/jasmine": "~3.3.2", 39 | "@types/jasminewd2": "~2.0.6", 40 | "@types/node": "~10.12.15", 41 | "codelyzer": "~4.5.0", 42 | "jasmine-core": "~3.3.0", 43 | "jasmine-spec-reporter": "~4.2.1", 44 | "karma": "~3.1.4", 45 | "karma-chrome-launcher": "~2.2.0", 46 | "karma-cli": "~2.0.0", 47 | "karma-coverage-istanbul-reporter": "^2.0.4", 48 | "karma-jasmine": "~2.0.1", 49 | "karma-jasmine-html-reporter": "^1.4.0", 50 | "ng-packagr": "^4.4.5", 51 | "protractor": "~5.4.1", 52 | "ts-node": "~7.0.1", 53 | "tsickle": "^0.34.0", 54 | "tslint": "~5.12.0", 55 | "typescript": "3.1.6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/directives/swing-stack.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | AfterContentInit, EventEmitter, Output, Directive 4 | } from '@angular/core'; 5 | 6 | import {SwingCardDirective} from './swing-card.directive'; 7 | 8 | import {StackConfig, ThrowEvent, DragEvent} from '../interfaces/swing'; 9 | 10 | import Stack from '../swing/stack'; 11 | 12 | @Directive({ 13 | selector: '[swingStack]' 14 | }) 15 | export class SwingStackDirective implements AfterContentInit { 16 | 17 | @Input() stackConfig: StackConfig; 18 | 19 | @Output() throwout: EventEmitter = new EventEmitter(); 20 | @Output() throwoutend: EventEmitter = new EventEmitter(); 21 | @Output() throwoutleft: EventEmitter = new EventEmitter(); 22 | @Output() throwoutright: EventEmitter = new EventEmitter(); 23 | @Output() throwoutup: EventEmitter = new EventEmitter(); 24 | @Output() throwoutdown: EventEmitter = new EventEmitter(); 25 | @Output() throwin: EventEmitter = new EventEmitter(); 26 | @Output() throwinend: EventEmitter = new EventEmitter(); 27 | 28 | @Output() dragstart: EventEmitter = new EventEmitter(); 29 | @Output() dragmove: EventEmitter = new EventEmitter(); 30 | @Output() dragend: EventEmitter = new EventEmitter(); 31 | 32 | cards: SwingCardDirective[]; 33 | stack: any; 34 | 35 | constructor() { 36 | this.cards = []; 37 | } 38 | 39 | addCard(card: SwingCardDirective) { 40 | this.cards.push(card); 41 | if (this.stack) { 42 | return this.stack.createCard(card.getNativeElement()); 43 | } 44 | } 45 | 46 | ngAfterContentInit() { 47 | this.stack = Stack(this.stackConfig || {}); 48 | this.cards.forEach((c) => this.stack.createCard(c.getNativeElement())); 49 | 50 | // Hook various events 51 | this.stack.on('throwout', $event => this.throwout.emit($event)); 52 | this.stack.on('throwoutend', $event => this.throwoutend.emit($event)); 53 | this.stack.on('throwoutleft', $event => this.throwoutleft.emit($event)); 54 | this.stack.on('throwoutright', $event => this.throwoutright.emit($event)); 55 | this.stack.on('throwin', $event => this.throwin.emit($event)); 56 | this.stack.on('throwinend', $event => this.throwinend.emit($event)); 57 | this.stack.on('dragstart', $event => this.dragstart.emit($event)); 58 | this.stack.on('dragmove', $event => this.dragmove.emit($event)); 59 | this.stack.on('dragend', $event => this.dragend.emit($event)); 60 | this.stack.on('throwoutup', $event => this.throwoutup.emit($event)); 61 | this.stack.on('throwoutdown', $event => this.throwoutdown.emit($event)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[NOT MAINTAINED]** This project is discontinued respectively I don't maintain it anymore. 2 | 3 | # ionic-swing 4 | 5 | ionic-swing is a fork of the following projects intended to add the swipeable cards capatibilies to Ionic (>= 2) 6 | 7 | - [swing.js](https://github.com/gajus/swing) 8 | - [angular2-swing](https://github.com/ksachdeva/angular2-swing) 9 | 10 | ## Installation 11 | 12 | To install this library, run: 13 | 14 | ```bash 15 | $ npm install ionic-swing --save 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### 1. Import the IonicSwingModule 21 | 22 | In your app.module.ts, import the library like following: 23 | 24 | import {IonicSwingModule} from 'ionic-swing'; 25 | 26 | and add it to your imports: 27 | 28 | imports: [ 29 | ... 30 | IonicSwingModule 31 | ... 32 | ] 33 | 34 | ### 2. To implement a card stack 35 | 36 | To implement a card stack, follow the example provided by angular2-swing [https://github.com/ksachdeva/angular2-swing](https://github.com/ksachdeva/angular2-swing) 37 | 38 | Or you could find also another example in my mobile application [Fluster](https://fluster.io), see the following page and module [https://github.com/fluster/fluster-app/tree/master/src/app/pages/browse/items/items](https://github.com/fluster/fluster-app/tree/master/src/app/pages/browse/items/items) 39 | 40 | ### 3. ViewChild and ViewChildren in Ionic v4 41 | 42 | In Ionic v4, in order to access the stack and cards as `ViewChild` and `ViewChildren`, it's mandatory to use the keyword `read` to identify correctly the elements 43 | 44 | Html: 45 | 46 |
47 | 48 | 49 |
50 | 51 | Ts: 52 | 53 | @ViewChild('swingStack', {read: SwingStackDirective}) swingStack: SwingStackDirective; 54 | @ViewChildren('swingCards', {read: SwingCardDirective}) swingCards: QueryList; 55 | 56 | ## Notes regarding hammerjs 57 | 58 | This library need `hammerjs` but isn't shipped with it because some framework, like `Ionic v3`, already include it in their own resources. If it isn't your case, you would need to install `hammerjs` in your project 59 | 60 | ```bash 61 | $ npm install hammerjs --save 62 | ``` 63 | 64 | and add the following line to your `app.component.ts` 65 | 66 | import 'hammerjs'; 67 | 68 | ## Note regarding global 69 | 70 | If you would face the error `ReferenceError: global is not defined at ionic-swing.js` at runtime, this could be fixed by declaring the `window` to the global scope. To do so add you could add the following to your `polyfill.ts`: 71 | 72 | ``` 73 | (window as any).global = window; 74 | ``` 75 | 76 | ## Development 77 | 78 | To generate the library using ng-packagr (https://github.com/dherges/ng-packagr) 79 | 80 | ```bash 81 | $ npm run packagr 82 | ``` 83 | 84 | To test locally 85 | 86 | ```bash 87 | $ cd dist 88 | $ npm pack 89 | $ cd /your-project-path/ 90 | $ npm install /relative-path-to-local-ionic-swing/dist/ionic-swing-0.0.0.tgz 91 | ``` 92 | 93 | ## License 94 | 95 | MIT © [David Dal Busco](mailto:david.dalbusco@outlook.com) 96 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/swing/stack.ts: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | import Sister from 'sister'; 4 | import * as rebound from 'rebound'; 5 | 6 | import Card from './card'; 7 | 8 | /** 9 | * param {Object} config Stack configuration. 10 | * returns {Object} An instance of Stack object. 11 | */ 12 | export const Stack = (config) => { 13 | let eventEmitter; 14 | let index; 15 | let springSystem; 16 | let stack; 17 | 18 | const construct = () => { 19 | stack = {}; 20 | springSystem = new rebound.SpringSystem(); 21 | eventEmitter = Sister(); 22 | index = []; 23 | }; 24 | 25 | construct(); 26 | 27 | /** 28 | * Get the configuration object. 29 | * 30 | * returns {Object} 31 | */ 32 | stack.getConfig = () => { 33 | return config; 34 | }; 35 | 36 | /** 37 | * Get a singleton instance of the SpringSystem physics engine. 38 | * 39 | * returns {Sister} 40 | */ 41 | stack.getSpringSystem = () => { 42 | return springSystem; 43 | }; 44 | 45 | /** 46 | * Proxy to the instance of the event emitter. 47 | * 48 | * param {string} eventName 49 | * param {string} listener 50 | * returns {undefined} 51 | */ 52 | stack.on = (eventName, listener) => { 53 | eventEmitter.on(eventName, listener); 54 | }; 55 | 56 | /** 57 | * Creates an instance of Card and associates it with an element. 58 | * 59 | * param {HTMLElement} element 60 | * returns {Card} 61 | */ 62 | stack.createCard = (element) => { 63 | const card = Card(stack, element); 64 | const events = [ 65 | 'throwout', 66 | 'throwoutend', 67 | 'throwoutleft', 68 | 'throwoutright', 69 | 'throwoutup', 70 | 'throwoutdown', 71 | 'throwin', 72 | 'throwinend', 73 | 'dragstart', 74 | 'dragmove', 75 | 'dragend' 76 | ]; 77 | 78 | // Proxy Card events to the Stack. 79 | events.forEach((eventName) => { 80 | card.on(eventName, (data) => { 81 | eventEmitter.trigger(eventName, data); 82 | }); 83 | }); 84 | 85 | index.push({ 86 | card, 87 | element 88 | }); 89 | 90 | return card; 91 | }; 92 | 93 | /** 94 | * Returns an instance of Card associated with an element. 95 | * 96 | * param {HTMLElement} element 97 | * returns {Card|null} 98 | */ 99 | stack.getCard = (element) => { 100 | const group = _.find(index, (i) => { 101 | if (element.isEqualNode(i.element)) { 102 | return i; 103 | } 104 | }); 105 | 106 | if (group) { 107 | return group.card; 108 | } 109 | 110 | return null; 111 | }; 112 | 113 | /** 114 | * Remove an instance of Card from the stack index. 115 | * 116 | * param {Card} card 117 | * returns {null} 118 | */ 119 | stack.destroyCard = (card) => { 120 | eventEmitter.trigger('destroyCard', card); 121 | 122 | if (index && config.sortCards) { 123 | if (config.prependCards) { 124 | index.shift(); 125 | } else { 126 | index.pop(); 127 | } 128 | } 129 | 130 | return index; 131 | }; 132 | 133 | return stack; 134 | }; 135 | 136 | export default Stack; 137 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs/Rx" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 1400 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-arg": true, 44 | "no-bitwise": true, 45 | "no-console": [ 46 | true, 47 | "debug", 48 | "info", 49 | "time", 50 | "timeEnd", 51 | "trace" 52 | ], 53 | "no-construct": true, 54 | "no-debugger": true, 55 | "no-duplicate-super": true, 56 | "no-empty": false, 57 | "no-empty-interface": true, 58 | "no-eval": true, 59 | "no-inferrable-types": [ 60 | true, 61 | "ignore-params" 62 | ], 63 | "no-misused-new": true, 64 | "no-non-null-assertion": true, 65 | "no-shadowed-variable": true, 66 | "no-string-literal": false, 67 | "no-string-throw": true, 68 | "no-switch-case-fall-through": true, 69 | "no-trailing-whitespace": true, 70 | "no-unnecessary-initializer": true, 71 | "no-unused-expression": true, 72 | "no-use-before-declare": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "typeof-compare": true, 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "directive-selector": [ 118 | true, 119 | "attribute", 120 | "swing", 121 | "camelCase" 122 | ], 123 | "component-selector": [ 124 | true, 125 | "element", 126 | "swing", 127 | "kebab-case" 128 | ], 129 | "use-input-property-decorator": true, 130 | "use-output-property-decorator": true, 131 | "use-host-property-decorator": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-life-cycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true, 138 | "invoke-injectable": true 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ionic-swing": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico" 22 | ], 23 | "styles": [ 24 | "src/styles.scss" 25 | ], 26 | "scripts": [] 27 | }, 28 | "configurations": { 29 | "production": { 30 | "optimization": true, 31 | "outputHashing": "all", 32 | "sourceMap": false, 33 | "extractCss": true, 34 | "namedChunks": false, 35 | "aot": true, 36 | "extractLicenses": true, 37 | "vendorChunk": false, 38 | "buildOptimizer": true, 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ] 45 | } 46 | } 47 | }, 48 | "serve": { 49 | "builder": "@angular-devkit/build-angular:dev-server", 50 | "options": { 51 | "browserTarget": "ionic-swing:build" 52 | }, 53 | "configurations": { 54 | "production": { 55 | "browserTarget": "ionic-swing:build:production" 56 | } 57 | } 58 | }, 59 | "extract-i18n": { 60 | "builder": "@angular-devkit/build-angular:extract-i18n", 61 | "options": { 62 | "browserTarget": "ionic-swing:build" 63 | } 64 | }, 65 | "test": { 66 | "builder": "@angular-devkit/build-angular:karma", 67 | "options": { 68 | "main": "src/test.ts", 69 | "karmaConfig": "./karma.conf.js", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "scripts": [], 73 | "styles": [ 74 | "src/styles.scss" 75 | ], 76 | "assets": [ 77 | "src/assets", 78 | "src/favicon.ico" 79 | ] 80 | } 81 | }, 82 | "lint": { 83 | "builder": "@angular-devkit/build-angular:tslint", 84 | "options": { 85 | "tsConfig": [ 86 | "src/tsconfig.app.json", 87 | "src/tsconfig.spec.json" 88 | ], 89 | "exclude": [ 90 | "**/node_modules/**" 91 | ] 92 | } 93 | } 94 | } 95 | }, 96 | "ionic-swing-e2e": { 97 | "root": "", 98 | "sourceRoot": "e2e", 99 | "projectType": "application", 100 | "architect": { 101 | "e2e": { 102 | "builder": "@angular-devkit/build-angular:protractor", 103 | "options": { 104 | "protractorConfig": "./protractor.conf.js", 105 | "devServerTarget": "ionic-swing:serve" 106 | } 107 | }, 108 | "lint": { 109 | "builder": "@angular-devkit/build-angular:tslint", 110 | "options": { 111 | "tsConfig": [ 112 | "e2e/tsconfig.e2e.json" 113 | ], 114 | "exclude": [ 115 | "**/node_modules/**" 116 | ] 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "defaultProject": "ionic-swing", 123 | "schematics": { 124 | "@schematics/angular:component": { 125 | "prefix": "swing", 126 | "styleext": "scss" 127 | }, 128 | "@schematics/angular:directive": { 129 | "prefix": "swing" 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/interfaces/swing.ts: -------------------------------------------------------------------------------- 1 | // here the ambient definitions for the swing 2 | // module are specified. Normally they should be at DefinitelyTyped 3 | // or better with the repository 4 | 5 | export interface ThrowEvent { 6 | /** 7 | * The element being dragged. 8 | */ 9 | target: HTMLElement; 10 | 11 | /** 12 | * The direction in which the element is being dragged: Card.DIRECTION_LEFT 13 | * or Card.DIRECTION_RIGHT 14 | */ 15 | throwDirection: any; 16 | } 17 | 18 | export interface DragEvent { 19 | /** 20 | * The element being dragged. 21 | */ 22 | target: HTMLElement; 23 | 24 | /** 25 | * Only available when the event is dragmove 26 | */ 27 | throwOutConfidence?: number; 28 | /** 29 | * Only available when the event is dragmove 30 | */ 31 | throwDirection?: any; 32 | /** 33 | * Only available when the event is dragmove 34 | */ 35 | offset?: number; 36 | 37 | } 38 | 39 | export type ThrowEventName = 'throwin' | 'throwinend' | 40 | 'throwout' | 'throwoutend' | 'throwoutleft' | 'throwoutup' | 'throwoutdown' | 'throwoutright'; 41 | 42 | export type DragEventName = 'dragstart' | 'dragmove' | 'dragend'; 43 | 44 | export interface Card { 45 | /** 46 | * Unbinds all Hammer.Manager events. 47 | * Removes the listeners from the physics simulation. 48 | * 49 | * return {undefined} 50 | */ 51 | destroy(): void; 52 | 53 | /** 54 | * Throws a card into the stack from an arbitrary position. 55 | * 56 | * param {Number} fromX 57 | * param {Number} fromY 58 | * return {undefined} 59 | */ 60 | throwIn(x: number, y: number): void; 61 | 62 | /** 63 | * Throws a card out of the stack in the direction away from the original offset. 64 | * 65 | * param {Number} fromX 66 | * param {Number} fromY 67 | * return {undefined} 68 | */ 69 | throwOut(x: number, y: number): void; 70 | 71 | on(eventName: ThrowEventName, callabck: (event: ThrowEvent) => void): void; 72 | on(eventName: DragEventName, callabck: (event: DragEvent) => void): void; 73 | } 74 | 75 | export interface StackConfig { 76 | 77 | minThrowOutDistance?: number; 78 | maxThrowOutDistance?: number; 79 | maxRotation?: number; 80 | allowedDirections?: Array; 81 | 82 | sortCards?: boolean; 83 | prependCards?: boolean; 84 | 85 | /** 86 | * Determines if element is being thrown out of the stack. 87 | * 88 | * Element is considered to be thrown out when throwOutConfidence is equal to 1. 89 | * 90 | * param {Number} offsetX Distance from the dragStart. 91 | * param {Number} offsetY Distance from the dragStart. 92 | * param {HTMLElement} element Element. 93 | * param {Number} throwOutConfidence config.throwOutConfidence 94 | * return {Boolean} 95 | */ 96 | isThrowOut?: (offsetX: number, offsetY: number, element: HTMLElement, throwOutConfidence: number) => boolean; 97 | 98 | /** 99 | * Returns a value between 0 and 1 indicating the completeness of the throw out condition. 100 | * 101 | * Ration of the absolute distance from the original card position and element width. 102 | * 103 | * param {Number} offsetX Distance from the dragStart. 104 | * param {Number} offsetY Distance from the dragStart. 105 | * param {HTMLElement} element Element. 106 | * return {Number} 107 | */ 108 | throwOutConfidence?: (offsetX: number, offsetY: number, element: HTMLElement) => number; 109 | 110 | /** 111 | * Calculates a distances at which the card is thrown out of the stack. 112 | * 113 | * param {Number} min 114 | * param {Number} max 115 | * return {Number} 116 | */ 117 | throwOutDistance?: (min: number, max: number) => number; 118 | 119 | /** 120 | * Calculates rotation based on the element x and y offset, element width and 121 | * maxRotation variables. 122 | * 123 | * param {Number} x Horizontal offset from the startDrag. 124 | * param {Number} y Vertical offset from the startDrag. 125 | * param {HTMLElement} element Element. 126 | * param {Number} maxRotation 127 | * return {Number} Rotation angle expressed in degrees. 128 | */ 129 | rotation?: (x: number, y: number, element: HTMLElement, maxRotation: number) => number; 130 | 131 | /** 132 | * Uses CSS transform to translate element position and rotation. 133 | * 134 | * Invoked in the event of `dragmove` and every time the physics solver is triggered. 135 | * 136 | * param {HTMLElement} element 137 | * param {Number} x Horizontal offset from the startDrag. 138 | * param {Number} y Vertical offset from the startDrag. 139 | * param {Number} r 140 | * return {undefined} 141 | */ 142 | transform?: (element: HTMLElement, x: number, y: number, r: number) => void; 143 | } 144 | 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [4.1.1](https://github.com/peterpeterparker/ionic-swing/compare/v4.1.0...v4.1.1) (2019-06-09) 3 | * **fix**: isThrowOut signature ([#13](https://github.com/fluster/ionic-swing/pull/13)) 4 | 5 | ### Kudos 6 | 7 | Thx [Simon Coope](https://github.com/sjcoope) for the PR 👍 8 | 9 | 10 | ## [4.1.0](https://github.com/peterpeterparker/ionic-swing/compare/v4.0.0...v4.1.0) (2018-12-18) 11 | * **features**: fork last swing.js features ([#10](https://github.com/fluster/ionic-swing/issues/10)) 12 | * **lib**: update dependencies 13 | 14 | 15 | ## [4.0.0](https://github.com/peterpeterparker/ionic-swing/compare/v3.0.0...v4.0.0) (2018-11-17) 16 | * **breaking**: Upgrade to Angular v7 17 | 18 | 19 | ## [3.0.0](https://github.com/peterpeterparker/ionic-swing/compare/v2.3.1...v3.0.0) (2018-08-28) 20 | * **breaking**: Refactor components to directives in order to be compatible with Ionic v4 (>= beta.5) and/or Typescript v2.9.2 21 | * **fix**: `touchmove` set as not passive 22 | 23 | 24 | ## [2.3.1](https://github.com/peterpeterparker/ionic-swing/compare/v2.3.0...v2.3.1) (2018-08-27) 25 | * **revert**: Revert rename `swing-card` 26 | 27 | 28 | ## [2.3.0](https://github.com/peterpeterparker/ionic-swing/compare/v2.2.3...v2.3.0) (2018-08-27) 29 | * **feat**: Rename `swing-card` component to `swing` in order to fix compatibility problem with `Card` from Ionic (Angular error: "More than one component matched on this element.") 30 | * **lib**: Update last Angular dependencies 31 | 32 | 33 | ## [2.2.3](https://github.com/peterpeterparker/ionic-swing/compare/v2.2.2...v2.2.3) (2018-08-18) 34 | * **fix**: Fix Chrome complains "Added non-passive event listener to a scroll-blocking..." (see [Hammerjs commit #987](https://github.com/hammerjs/hammer.js/pull/987/commits/49cd23d30d9618c5e8b14dd4412f94454143e080)) 35 | 36 | 37 | ## [2.2.2](https://github.com/peterpeterparker/ionic-swing/compare/v2.2.1...v2.2.2) (2018-08-18) 38 | * **fix**: `stack.destroyCard` was wrongly implemented 39 | * **feature**: Observe `prepend` option in `stack.destroyCard` 40 | 41 | 42 | ## [2.2.1](https://github.com/peterpeterparker/ionic-swing/compare/v2.2.0...v2.2.1) (2018-08-18) 43 | * **fix**: Remove import of `hammerjs` (see README or CHANGELOG v2.1.0) 44 | 45 | 46 | ## [2.2.0](https://github.com/peterpeterparker/ionic-swing/compare/v2.1.0...v2.2.0) (2018-08-16) 47 | * **feature**: Cards' position in the stack is not modified per default anymore. This is now optional, use `StackConfig.sortCards` if you wish to do so 48 | * **refactor**: The `prepend` option as been moved to `StackConfig.prependCards` 49 | * **lib**: Update all libs dependencies 50 | 51 | 52 | ## [2.1.0](https://github.com/peterpeterparker/ionic-swing/compare/v2.0.1...v2.1.0) (2018-08-04) 53 | * **lib**: Revert, `hammerjs` will not be shipped with `ionic-swing` as it was previously the case 54 | 55 | 56 | ## [2.0.1](https://github.com/peterpeterparker/ionic-swing/compare/v0.2.0...v2.0.1) (2018-07-25) 57 | * **breaking changes**: Update project to Angular v6 58 | * **lib**: From now on, `ionic-swing` is shipped with a reference to `hammerjs` 59 | 60 | p.s.: v2.0.1 instead of v2.0.0 in order to publish correctly to npm 61 | 62 | 63 | ## [0.2.0](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.10...v0.2.0) (2017-12-21) 64 | * **feat:** Avoid potential cpu load problem 65 | 66 | 67 | ## [0.1.0](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.10...v0.1.0) (2017-11-09) 68 | * **project:** Migration of the project structure to use Angular CLI 69 | * **project:** Introduction of ng-packagr to build the module 70 | * **lib:** Update angular 5.0.0 and rjxs 5.5.2 71 | 72 | 73 | ## [0.0.7](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.6...v0.0.10) (2017-06-18) 74 | * **lib:** Update angular, rjxs and zone.js 75 | 76 | 77 | ## [0.0.6](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.5...v0.0.6) (2017-05-11) 78 | * **lib:** Update to zonejs 79 | * fix publish on npm 80 | 81 | 82 | ## [0.0.5](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.4...v0.0.5) (2017-05-11) 83 | * **lib:** Update to angular 4.1.0 84 | 85 | 86 | ## [0.0.4](https://github.com/peterpeterparker/ionic-swing/compare/v0.0.3...v0.0.4) (2017-04-26) 87 | 88 | ### Features 89 | 90 | * **lib:** Lodash replaced by underscore.js ([#1]https://github.com/peterpeterparker/ionic-swing/issues/1) 91 | * **lib:** Angular, rxjs and zone updated to reflect Ionic 3.1.0 dependencies ([#3]https://github.com/peterpeterparker/ionic-swing/issues/3) 92 | 93 | ### Comments 94 | 95 | The main reason behind the replacement of lodash by underscore is the size of the library. In an Ionic app the size of the bundle is a major factor regarding the boot time, therefore, smaller the libs are, faster the app boot will be. 96 | -------------------------------------------------------------------------------- /src/app/modules/ionic-swing/swing/card.ts: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | import Sister from 'sister'; 4 | import * as rebound from 'rebound'; 5 | import vendorPrefix from 'vendor-prefix'; 6 | import raf from 'raf'; 7 | import Direction from './direction'; 8 | 9 | import { 10 | elementChildren, 11 | isTouchDevice 12 | } from './utilities'; 13 | 14 | declare var global: any; 15 | declare var Hammer: any; 16 | 17 | /** 18 | * param {number} fromX 19 | * param {number} fromY 20 | * param {Direction[]} allowedDirections 21 | * returns {Direction[]} computed direction 22 | */ 23 | const computeDirection = (fromX, fromY, allowedDirections) => { 24 | const isHorizontal = Math.abs(fromX) > Math.abs(fromY); 25 | 26 | const isLeftDirection = fromX < 0 ? Direction.LEFT : Direction.RIGHT; 27 | const isUpDirection = fromY < 0 ? Direction.UP : Direction.DOWN; 28 | 29 | const direction = isHorizontal ? isLeftDirection : isUpDirection; 30 | 31 | if (allowedDirections.indexOf(direction) === -1) { 32 | return Direction.INVALID; 33 | } 34 | 35 | return direction; 36 | }; 37 | 38 | /** 39 | * Prepend element to the parentNode. 40 | * 41 | * This makes the element last among the siblings. 42 | * 43 | * Invoked when card is added to the stack (when prepend is true). 44 | * 45 | * param {HTMLElement} element The target element. 46 | * return {undefined} 47 | */ 48 | const prependToParent = (element) => { 49 | const parentNode = element.parentNode; 50 | 51 | parentNode.removeChild(element); 52 | parentNode.insertBefore(element, parentNode.firstChild); 53 | }; 54 | 55 | /** 56 | * Append element to the parentNode. 57 | * 58 | * This makes the element first among the siblings. The reason for using 59 | * this as opposed to zIndex is to allow CSS selector :nth-child. 60 | * 61 | * Invoked in the event of mousedown. 62 | * Invoked when card is added to the stack. 63 | * 64 | * param {HTMLElement} element The target element. 65 | * returns {undefined} 66 | */ 67 | const appendToParent = (element) => { 68 | const parentNode = element.parentNode; 69 | const siblings = elementChildren(parentNode); 70 | const targetIndex = siblings.indexOf(element); 71 | const appended = targetIndex + 1 !== siblings.length; 72 | 73 | if (appended) { 74 | parentNode.removeChild(element); 75 | parentNode.appendChild(element); 76 | } 77 | 78 | return appended; 79 | }; 80 | 81 | /** 82 | * Uses CSS transform to translate element position and rotation. 83 | * 84 | * Invoked in the event of `dragmove` and every time the physics solver is triggered. 85 | * 86 | * param {HTMLElement} element 87 | * param {number} coordinateX Horizontal offset from the startDrag. 88 | * param {number} coordinateY Vertical offset from the startDrag. 89 | * param {number} rotation 90 | * returns {undefined} 91 | */ 92 | const transform = (element, coordinateX, coordinateY, rotation) => { 93 | element.style[vendorPrefix('transform')] = 'translate3d(0, 0, 0) translate(' + coordinateX + 'px, ' + coordinateY + 'px) rotate(' + rotation + 'deg)'; 94 | }; 95 | 96 | /** 97 | * Returns a value between 0 and 1 indicating the completeness of the throw out condition. 98 | * 99 | * Ration of the absolute distance from the original card position and element width. 100 | * 101 | * param {number} xOffset Distance from the dragStart. 102 | * param {number} yOffset Distance from the dragStart. 103 | * param {HTMLElement} element Element. 104 | * returns {number} 105 | */ 106 | const throwOutConfidence = (xOffset, yOffset, element) => { 107 | const xConfidence = Math.min(Math.abs(xOffset) / element.offsetWidth, 1); 108 | const yConfidence = Math.min(Math.abs(yOffset) / element.offsetHeight, 1); 109 | 110 | return Math.max(xConfidence, yConfidence); 111 | }; 112 | 113 | /** 114 | * Determines if element is being thrown out of the stack. 115 | * 116 | * Element is considered to be thrown out when throwOutConfidence is equal to 1. 117 | * 118 | * param {number} xOffset Distance from the dragStart. 119 | * param {number} yOffset Distance from the dragStart. 120 | * param {HTMLElement} element Element. 121 | * param {number} throwOutConfidence config.throwOutConfidence 122 | * returns {boolean} 123 | */ 124 | const isThrowOut = (xOffset, yOffset, element, throwOutConfidence) => { 125 | return throwOutConfidence === 1; 126 | }; 127 | 128 | /** 129 | * Calculates a distances at which the card is thrown out of the stack. 130 | * 131 | * param {number} min 132 | * param {number} max 133 | * returns {number} 134 | */ 135 | const throwOutDistance = (min, max) => { 136 | return _.random(min, max); 137 | }; 138 | 139 | /** 140 | * Calculates rotation based on the element x and y offset, element width and maxRotation variables. 141 | * 142 | * param {number} coordinateX Horizontal offset from the startDrag. 143 | * param {number} coordinateY Vertical offset from the startDrag. 144 | * param {HTMLElement} element Element. 145 | * param {number} maxRotation 146 | * returns {number} Rotation angle expressed in degrees. 147 | */ 148 | const rotation = (coordinateX, coordinateY, element, maxRotation) => { 149 | const horizontalOffset = Math.min(Math.max(coordinateX / element.offsetWidth, -1), 1); 150 | const verticalOffset = (coordinateY > 0 ? 1 : -1) * Math.min(Math.abs(coordinateY) / 100, 1); 151 | const calculatedRotation = horizontalOffset * verticalOffset * maxRotation; 152 | 153 | return calculatedRotation; 154 | }; 155 | 156 | const THROW_IN = 'in'; 157 | const THROW_OUT = 'out'; 158 | 159 | /** 160 | * Creates a configuration object. 161 | * 162 | * param {Object} config 163 | * returns {Object} 164 | */ 165 | const makeConfig = (config = {}) => { 166 | const defaultConfig = { 167 | allowedDirections: [ 168 | Direction.RIGHT, 169 | Direction.LEFT, 170 | Direction.UP 171 | ], 172 | isThrowOut: isThrowOut, 173 | maxRotation: 20, 174 | maxThrowOutDistance: 500, 175 | minThrowOutDistance: 400, 176 | rotation: rotation, 177 | throwOutConfidence: throwOutConfidence, 178 | throwOutDistance: throwOutDistance, 179 | transform: transform, 180 | sortCards: false, 181 | prependCards: false 182 | }; 183 | 184 | return _.assign({}, defaultConfig, config); 185 | }; 186 | 187 | /** 188 | * param {Stack} stack 189 | * param {HTMLElement} targetElement 190 | * returns {Object} An instance of Card. 191 | */ 192 | const Card = (stack, targetElement) => { 193 | let card; 194 | let config; 195 | let currentX; 196 | let currentY; 197 | let doMove; 198 | let eventEmitter; 199 | let isDraging; 200 | let isPanning; 201 | let lastThrow; 202 | let lastTranslate; 203 | let lastX; 204 | let lastY; 205 | let mc; 206 | let onSpringUpdate; 207 | let springSystem; 208 | let springThrowIn; 209 | let springThrowOut; 210 | let throwDirectionToEventName; 211 | let throwOutDistance; 212 | let throwWhere; 213 | let appendedDuringMouseDown; 214 | 215 | const construct = () => { 216 | card = {}; 217 | config = makeConfig(stack.getConfig()); 218 | eventEmitter = Sister(); 219 | springSystem = stack.getSpringSystem(); 220 | springThrowIn = springSystem.createSpring(250, 10); 221 | springThrowOut = springSystem.createSpring(500, 20); 222 | lastThrow = {}; 223 | lastTranslate = { 224 | coordinateX: 0, 225 | coordinateY: 0 226 | }; 227 | 228 | /* Mapping directions to event names */ 229 | throwDirectionToEventName = {}; 230 | throwDirectionToEventName[Direction.LEFT] = 'throwoutleft'; 231 | throwDirectionToEventName[Direction.RIGHT] = 'throwoutright'; 232 | throwDirectionToEventName[Direction.UP] = 'throwoutup'; 233 | throwDirectionToEventName[Direction.DOWN] = 'throwoutdown'; 234 | 235 | springThrowIn.setRestSpeedThreshold(0.05); 236 | springThrowIn.setRestDisplacementThreshold(0.05); 237 | 238 | springThrowOut.setRestSpeedThreshold(0.05); 239 | springThrowOut.setRestDisplacementThreshold(0.05); 240 | 241 | throwOutDistance = config.throwOutDistance(config.minThrowOutDistance, config.maxThrowOutDistance); 242 | 243 | mc = new Hammer.Manager(targetElement, { 244 | recognizers: [ 245 | [ 246 | Hammer.Pan, 247 | { 248 | threshold: 2 249 | } 250 | ] 251 | ] 252 | }); 253 | 254 | if (config.sortCards) { 255 | if (config.prependCards) { 256 | prependToParent(targetElement); 257 | } else { 258 | appendToParent(targetElement); 259 | } 260 | } 261 | 262 | eventEmitter.on('panstart', () => { 263 | if (config.sortCards) { 264 | appendToParent(targetElement); 265 | } 266 | 267 | eventEmitter.trigger('dragstart', { 268 | target: targetElement 269 | }); 270 | 271 | currentX = 0; 272 | currentY = 0; 273 | 274 | isDraging = true; 275 | 276 | (function animation() { 277 | if (isDraging) { 278 | doMove(); 279 | 280 | raf(animation); 281 | } 282 | })(); 283 | }); 284 | 285 | eventEmitter.on('panmove', (event) => { 286 | currentX = event.deltaX; 287 | currentY = event.deltaY; 288 | }); 289 | 290 | eventEmitter.on('panend', (event) => { 291 | isDraging = false; 292 | 293 | const coordinateX = lastTranslate.coordinateX + event.deltaX; 294 | const coordinateY = lastTranslate.coordinateY + event.deltaY; 295 | 296 | const isThrowOut = config.isThrowOut( 297 | coordinateX, 298 | coordinateY, 299 | targetElement, 300 | config.throwOutConfidence(coordinateX, coordinateY, targetElement) 301 | ); 302 | 303 | // Not really sure about computing direction here and filtering on directions here. 304 | // It adds more logic. Any suggestion will be appreciated. 305 | const direction = computeDirection(coordinateX, coordinateY, config.allowedDirections); 306 | 307 | if (isThrowOut && direction !== Direction.INVALID) { 308 | card.throwOut(coordinateX, coordinateY, direction); 309 | } else { 310 | card.throwIn(coordinateX, coordinateY, direction); 311 | } 312 | 313 | eventEmitter.trigger('dragend', { 314 | target: targetElement 315 | }); 316 | }); 317 | 318 | // "mousedown" event fires late on touch enabled devices, thus listening 319 | // to the touchstart event for touch enabled devices and mousedown otherwise. 320 | if (isTouchDevice()) { 321 | targetElement.addEventListener('touchstart', () => { 322 | eventEmitter.trigger('panstart'); 323 | }, { passive: true }); 324 | 325 | targetElement.addEventListener('touchend', () => { 326 | if (isDraging && !isPanning) { 327 | eventEmitter.trigger('dragend', { 328 | target: targetElement 329 | }); 330 | } 331 | }, { passive: true }); 332 | 333 | // Disable scrolling while dragging the element on the touch enabled devices. 334 | // @see http://stackoverflow.com/a/12090055/368691 335 | (() => { 336 | let dragging; 337 | 338 | targetElement.addEventListener('touchstart', () => { 339 | dragging = true; 340 | }, { passive: true }); 341 | 342 | targetElement.addEventListener('touchend', () => { 343 | dragging = false; 344 | }, { passive: true }); 345 | 346 | global.addEventListener('touchmove', (event) => { 347 | if (dragging) { 348 | event.preventDefault(); 349 | } 350 | }, { passive: false }); 351 | })(); 352 | } else { 353 | targetElement.addEventListener('mousedown', () => { 354 | appendedDuringMouseDown = appendToParent(targetElement) || appendedDuringMouseDown; 355 | eventEmitter.trigger('panstart'); 356 | }, { passive: true }); 357 | 358 | targetElement.addEventListener('mouseup', () => { 359 | if (appendedDuringMouseDown) { 360 | targetElement.click(); 361 | appendedDuringMouseDown = false; 362 | } 363 | 364 | if (isDraging && !isPanning) { 365 | eventEmitter.trigger('dragend', { 366 | target: targetElement 367 | }); 368 | } 369 | }, { passive: true }); 370 | } 371 | 372 | mc.on('panstart', (event) => { 373 | isPanning = true; 374 | eventEmitter.trigger('panstart', event); 375 | }); 376 | 377 | mc.on('panmove', (event) => { 378 | eventEmitter.trigger('panmove', event); 379 | }); 380 | 381 | mc.on('panend', (event) => { 382 | isPanning = false; 383 | eventEmitter.trigger('panend', event); 384 | }); 385 | 386 | springThrowIn.addListener({ 387 | onSpringAtRest: () => { 388 | eventEmitter.trigger('throwinend', { 389 | target: targetElement 390 | }); 391 | }, 392 | onSpringUpdate: (spring) => { 393 | const value = spring.getCurrentValue(); 394 | const coordianteX = rebound.util.mapValueInRange(value, 0, 1, lastThrow.fromX, 0); 395 | const coordianteY = rebound.util.mapValueInRange(value, 0, 1, lastThrow.fromY, 0); 396 | 397 | onSpringUpdate(coordianteX, coordianteY); 398 | } 399 | }); 400 | 401 | springThrowOut.addListener({ 402 | onSpringAtRest: () => { 403 | eventEmitter.trigger('throwoutend', { 404 | target: targetElement 405 | }); 406 | }, 407 | onSpringUpdate: (spring) => { 408 | const value = spring.getCurrentValue(); 409 | 410 | let coordianteX; 411 | let coordianteY; 412 | let directionFactor; 413 | 414 | if (lastThrow.direction === Direction.RIGHT || lastThrow.direction === Direction.LEFT) { 415 | directionFactor = lastThrow.direction === Direction.RIGHT ? 1 : -1; 416 | coordianteX = rebound.util.mapValueInRange(value, 0, 1, lastThrow.fromX, throwOutDistance * directionFactor); 417 | coordianteY = lastThrow.fromY; 418 | } else if (lastThrow.direction === Direction.UP || lastThrow.direction === Direction.DOWN) { 419 | directionFactor = lastThrow.direction === Direction.DOWN ? 1 : -1; 420 | coordianteX = lastThrow.fromX; 421 | coordianteY = rebound.util.mapValueInRange(value, 0, 1, lastThrow.fromY, throwOutDistance * directionFactor); 422 | } 423 | 424 | onSpringUpdate(coordianteX, coordianteY); 425 | } 426 | }); 427 | 428 | /** 429 | * Transforms card position based on the current environment variables. 430 | * 431 | * returns {undefined} 432 | */ 433 | doMove = () => { 434 | if (currentX === lastX && currentY === lastY) { 435 | return; 436 | } 437 | 438 | lastX = currentX; 439 | lastY = currentY; 440 | 441 | const coordinateX = lastTranslate.coordinateX + currentX; 442 | const coordianteY = lastTranslate.coordinateY + currentY; 443 | const rotation = config.rotation(coordinateX, coordianteY, targetElement, config.maxRotation); 444 | 445 | config.transform(targetElement, coordinateX, coordianteY, rotation); 446 | 447 | eventEmitter.trigger('dragmove', { 448 | offset: coordinateX, 449 | target: targetElement, 450 | throwDirection: computeDirection(coordinateX, coordianteY, config.allowedDirections), 451 | throwOutConfidence: config.throwOutConfidence(coordinateX, coordianteY, targetElement) 452 | }); 453 | }; 454 | 455 | /** 456 | * Invoked every time the physics solver updates the Spring's value. 457 | * 458 | * param {number} coordinateX 459 | * param {number} coordinateY 460 | * returns {undefined} 461 | */ 462 | onSpringUpdate = (coordinateX, coordinateY) => { 463 | const rotation = config.rotation(coordinateX, coordinateY, targetElement, config.maxRotation); 464 | 465 | lastTranslate.coordinateX = coordinateX || 0; 466 | lastTranslate.coordinateY = coordinateY || 0; 467 | 468 | config.transform(targetElement, coordinateX, coordinateY, rotation); 469 | }; 470 | 471 | /** 472 | * param {THROW_IN|THROW_OUT} where 473 | * param {number} fromX 474 | * param {number} fromY 475 | * param {Direction} [direction] 476 | * returns {undefined} 477 | */ 478 | throwWhere = (where, fromX, fromY, direction) => { 479 | lastThrow.fromX = fromX; 480 | lastThrow.fromY = fromY; 481 | 482 | // If direction argument is not set, compute it from coordinates. 483 | lastThrow.direction = direction || computeDirection(fromX, fromY, config.allowedDirections); 484 | 485 | if (where === THROW_IN) { 486 | appendToParent(targetElement); 487 | springThrowIn.setCurrentValue(0).setAtRest().setEndValue(1); 488 | 489 | eventEmitter.trigger('throwin', { 490 | target: targetElement, 491 | throwDirection: lastThrow.direction 492 | }); 493 | } else if (where === THROW_OUT) { 494 | appendToParent(targetElement); 495 | isDraging = false; 496 | springThrowOut.setCurrentValue(0).setAtRest().setVelocity(100).setEndValue(1); 497 | 498 | eventEmitter.trigger('throwout', { 499 | target: targetElement, 500 | throwDirection: lastThrow.direction 501 | }); 502 | 503 | /* Emits more accurate events about specific directions */ 504 | eventEmitter.trigger(throwDirectionToEventName[lastThrow.direction], { 505 | target: targetElement, 506 | throwDirection: lastThrow.direction 507 | }); 508 | } else { 509 | throw new Error('Invalid throw event.'); 510 | } 511 | }; 512 | }; 513 | 514 | construct(); 515 | 516 | /** 517 | * Alias 518 | */ 519 | card.on = eventEmitter.on; 520 | card.trigger = eventEmitter.trigger; 521 | 522 | /** 523 | * Throws a card into the stack from an arbitrary position. 524 | * 525 | * param {number} coordinateX 526 | * param {number} coordinateY 527 | * param {Direction} [direction] 528 | * returns {undefined} 529 | */ 530 | card.throwIn = (coordinateX, coordinateY, direction) => { 531 | throwWhere(THROW_IN, coordinateX, coordinateY, direction); 532 | }; 533 | 534 | /** 535 | * Throws a card out of the stack in the direction away from the original offset. 536 | * 537 | * param {number} coordinateX 538 | * param {number} coordinateY 539 | * param {Direction} [direction] 540 | * returns {undefined} 541 | */ 542 | card.throwOut = (coordinateX, coordinateY, direction) => { 543 | throwWhere(THROW_OUT, coordinateX, coordinateY, direction); 544 | }; 545 | 546 | /** 547 | * Unbinds all Hammer.Manager events. 548 | * Removes the listeners from the physics simulation. 549 | * 550 | * returns {undefined} 551 | */ 552 | card.destroy = () => { 553 | isDraging = false; 554 | mc.destroy(); 555 | springThrowIn.destroy(); 556 | springThrowOut.destroy(); 557 | 558 | stack.destroyCard(card); 559 | }; 560 | 561 | return card; 562 | }; 563 | 564 | export default Card; 565 | --------------------------------------------------------------------------------