├── 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 |
--------------------------------------------------------------------------------