├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── ng-package.json ├── package-lock.json ├── package.json ├── projects └── nestable │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── scss-bundle.config.json │ ├── src │ ├── lib │ │ ├── _nestable-theme.scss │ │ ├── nestable-drag-handle │ │ │ ├── nestable-drag-handle.directive.spec.ts │ │ │ └── nestable-drag-handle.directive.ts │ │ ├── nestable-expand-collapse-handle │ │ │ ├── nestable-expand-collapse.directive.spec.ts │ │ │ └── nestable-expand-collapse.directive.ts │ │ ├── nestable.component.html │ │ ├── nestable.component.scss │ │ ├── nestable.component.spec.ts │ │ ├── nestable.component.ts │ │ ├── nestable.constant.ts │ │ ├── nestable.helper.ts │ │ ├── nestable.models.ts │ │ └── nestable.module.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── protractor.conf.js ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts ├── assets │ ├── .gitkeep │ └── ngx_nestable.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Angular Nestable List

4 |

5 | 6 | # ngx-nestable 7 | Nestable list for Angular4 and beyond. This is a Angular adaptation of Jquery [Nestable](https://dbushell.com/Nestable/) library. 8 | [Demo](https://cybercomet.github.io/ngx-nestable) 9 | 10 | ## Features 11 | * Drag and Drop 12 | * Sorting 13 | * Events (drag, drop, over, out) // TODO 14 | * Nesting 15 | * Touch support // TODO 16 | 17 | ## Instalation 18 | ``` 19 | npm i ngx-nestable --save 20 | ``` 21 | 22 | ## Implementation 23 | 24 | app.module.ts 25 | 26 | ```ts 27 | import { NestableModule } from 'ngx-nestable'; 28 | 29 | @NgModule({ 30 | imports: [NestableModule], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule {} 34 | ``` 35 | 36 | app.component.ts 37 | ```ts 38 | public options = { 39 | fixedDepth: true 40 | } as NestableSettings; 41 | public list = [ 42 | { 'id': 1 }, 43 | { 44 | 'expanded': true, 45 | 'id': 2, 'children': [ 46 | { 'id': 3 }, 47 | { 'id': 4 }, 48 | { 49 | 'expanded': false, 50 | 'id': 5, 'children': [ 51 | { 'id': 6 }, 52 | { 'id': 7 }, 53 | { 'id': 8 } 54 | ] 55 | }, 56 | { 'id': 9 }, 57 | { 'id': 10 } 58 | ] 59 | }, 60 | { 'id': 11 } 61 | ]; 62 | ``` 63 | 64 | app.html 65 | ```html 66 | 67 | 68 | 69 | 70 | 73 | 74 | 78 | 79 |
Item: {{row.item.id}}
80 |
81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | ## API 88 | 89 | ### Inputs 90 | 91 | | Name| Type| Default|Descirption| 92 | | :-------------: |:-------------:| :-----:|:--------------| 93 | | list| Array| | Items which will be displayed in tree 94 | | template| TemplateRef| | HTML template for tree item. Inside this template you can place directives (ngxNestableDragHandle and ngxNestableExpandCollapse). That will describe the custom look and position of these elements. Otherwise, the default functions are applied. 95 | 96 | #### options 97 | 98 | | Name| Type| Default|Descirption| 99 | | :-------------: |:-------------:| :-----:|:--------------| 100 | | fixedDepth | boolean |false | Constaint items to keep their initial depth 101 | | maxDepth | number| 5 | Maximum nested depth 102 | | threshold | number | 20 | Distance in px after which horizontal movement (nesting) is applied. Also gives padding to the tree. 103 | | disableDrag| boolean| false| Disable/enable drag event 104 | | template| TemplateRef| | HTML template for tree item. Inside this template you can place directives (ngxNestableDragHandle and ngxNestableExpandCollapse). That will describe the custom look and position of these elements, otherwise the default functions are applied. 105 | 106 | ### Outputs 107 | 108 | | Name| Type |Descirption| 109 | | :-------------: |:-------------:| :-----| 110 | | drag| void| Emits event when item drag is started 111 | | drop| void| Emits event when item is dropped 112 | |disclosure|void| Emits event when item expand/collapse state is changed 113 | 114 | ## Authors 115 | 116 | * **Petar Markov** - *Initial work* - [Speculees](https://github.com/speculees) 117 | * **Mihajlo Grubjesic** - *Initial work* - [Comi89](https://github.com/comi89) 118 | * **Zlatomir Darabuc** - *Initial work* - [zlaayaa](https://github.com/zlaayaa) 119 | 120 | ## License 121 | 122 | This project is licensed under the MIT License - see the [LICENSE.md](https://opensource.org/licenses/MIT) file for details 123 | 124 | ## Acknowledgments 125 | 126 | * [Nestable](https://github.com/dbushell/Nestable) 127 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-nestable": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "ngx", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "tsConfig": "src/tsconfig.app.json", 24 | "polyfills": "src/polyfills.ts", 25 | "assets": [ 26 | "src/assets", 27 | "src/favicon.ico" 28 | ], 29 | "styles": [ 30 | "src/styles.scss" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "optimization": true, 37 | "outputHashing": "all", 38 | "sourceMap": false, 39 | "extractCss": true, 40 | "namedChunks": false, 41 | "aot": true, 42 | "extractLicenses": true, 43 | "vendorChunk": false, 44 | "buildOptimizer": true, 45 | "fileReplacements": [ 46 | { 47 | "replace": "src/environments/environment.ts", 48 | "with": "src/environments/environment.prod.ts" 49 | } 50 | ], 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | } 65 | }, 66 | "serve": { 67 | "builder": "@angular-devkit/build-angular:dev-server", 68 | "options": { 69 | "browserTarget": "ngx-nestable:build" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "ngx-nestable:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "ngx-nestable:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "main": "src/test.ts", 87 | "karmaConfig": "./karma.conf.js", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "src/tsconfig.spec.json", 90 | "scripts": [], 91 | "styles": [ 92 | "src/styles.scss" 93 | ], 94 | "assets": [ 95 | "src/assets", 96 | "src/favicon.ico" 97 | ] 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-devkit/build-angular:tslint", 102 | "options": { 103 | "tsConfig": [ 104 | "src/tsconfig.app.json", 105 | "src/tsconfig.spec.json", 106 | "e2e/tsconfig.json" 107 | ], 108 | "exclude": [ 109 | "**/node_modules/**" 110 | ] 111 | } 112 | }, 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "ngx-nestable:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "ngx-nestable:serve:production" 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "ngx-nestable-e2e": { 128 | "root": "", 129 | "sourceRoot": "e2e", 130 | "projectType": "application", 131 | "architect": { 132 | "e2e": { 133 | "builder": "@angular-devkit/build-angular:protractor", 134 | "options": { 135 | "protractorConfig": "./protractor.conf.js", 136 | "devServerTarget": "ngx-nestable:serve" 137 | } 138 | }, 139 | "lint": { 140 | "builder": "@angular-devkit/build-angular:tslint", 141 | "options": { 142 | "tsConfig": [ 143 | "e2e/tsconfig.e2e.json" 144 | ], 145 | "exclude": [ 146 | "**/node_modules/**" 147 | ] 148 | } 149 | } 150 | } 151 | }, 152 | "nestable": { 153 | "projectType": "library", 154 | "root": "projects/nestable", 155 | "sourceRoot": "projects/nestable/src", 156 | "prefix": "ngx", 157 | "schematics": { 158 | "@schematics/angular:component": { 159 | "styleext": "scss" 160 | } 161 | }, 162 | "architect": { 163 | "build": { 164 | "builder": "@angular-devkit/build-ng-packagr:build", 165 | "options": { 166 | "tsConfig": "projects/nestable/tsconfig.lib.json", 167 | "project": "projects/nestable/ng-package.json" 168 | } 169 | }, 170 | "test": { 171 | "builder": "@angular-devkit/build-angular:karma", 172 | "options": { 173 | "main": "projects/nestable/src/test.ts", 174 | "tsConfig": "projects/nestable/tsconfig.spec.json", 175 | "karmaConfig": "projects/nestable/karma.conf.js" 176 | } 177 | }, 178 | "lint": { 179 | "builder": "@angular-devkit/build-angular:tslint", 180 | "options": { 181 | "tsConfig": [ 182 | "projects/nestable/tsconfig.lib.json", 183 | "projects/nestable/tsconfig.spec.json" 184 | ], 185 | "exclude": [ 186 | "**/node_modules/**" 187 | ] 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "defaultProject": "ngx-nestable", 194 | "schematics": { 195 | "@schematics/angular:component": { 196 | "prefix": "ngx", 197 | "styleext": "scss" 198 | }, 199 | "@schematics/angular:directive": { 200 | "prefix": "ngx" 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('ngx-nestable App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/package.schema.json", 3 | "lib": { 4 | "entryFile": "lib/index.ts" 5 | }, 6 | "dest": "./dist/ngx-nestable" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-nestable", 3 | "version": "0.9.2", 4 | "license": "MIT", 5 | "contributors": [ 6 | { 7 | "name": "Petar Markov", 8 | "email": "speculees@gmail.com" 9 | }, 10 | { 11 | "name": "Zlatomir Darabuc - Zlaya", 12 | "email": "zlayabre@gmail.com" 13 | }, 14 | { 15 | "name": "Mihajlo Grubjesic", 16 | "email": "mgrubjesic89@gmail.com" 17 | } 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/cybercomet/ngx-nestable.git" 22 | }, 23 | "scripts": { 24 | "ng": "ng", 25 | "start": "ng serve", 26 | "build": "ng build --prod", 27 | "test": "ng test", 28 | "lint": "ng lint", 29 | "e2e": "ng e2e", 30 | "pack": "rimraf dist && ng build nestable && npm run bundle-styles && npm run readme-to-dist", 31 | "bundle-styles": "scss-bundle -c projects/nestable/scss-bundle.config.json", 32 | "readme-to-dist": "copyfiles README.md dist/ngx-nestable", 33 | "publish-package": "npm run pack && cd dist/ngx-nestable/ && ls && npm publish" 34 | }, 35 | "private": false, 36 | "dependencies": { 37 | "@angular/animations": "~8.2.0", 38 | "@angular/cdk": "^8.1.2", 39 | "@angular/common": "~8.2.0", 40 | "@angular/compiler": "~8.2.0", 41 | "@angular/core": "~8.2.0", 42 | "@angular/flex-layout": "^8.0.0-beta.26", 43 | "@angular/forms": "~8.2.0", 44 | "@angular/material": "^8.1.3", 45 | "@angular/platform-browser": "~8.2.0", 46 | "@angular/platform-browser-dynamic": "~8.2.0", 47 | "@angular/router": "~8.2.0", 48 | "rxjs": "~6.4.0", 49 | "tslib": "^1.10.0", 50 | "zone.js": "~0.9.1" 51 | }, 52 | "devDependencies": { 53 | "@angular-devkit/build-angular": "~0.802.1", 54 | "@angular-devkit/build-ng-packagr": "~0.802.1", 55 | "@angular/cli": "~8.2.1", 56 | "@angular/compiler-cli": "~8.2.0", 57 | "@angular/language-service": "~8.2.0", 58 | "@types/node": "~8.9.4", 59 | "@ngtools/webpack": "8.2.1", 60 | "@types/jasmine": "~3.3.8", 61 | "@types/jasminewd2": "~2.0.3", 62 | "codelyzer": "^5.0.0", 63 | "copyfiles": "^2.1.1", 64 | "jasmine-core": "~3.4.0", 65 | "jasmine-spec-reporter": "~4.2.1", 66 | "karma": "~4.1.0", 67 | "karma-chrome-launcher": "~2.2.0", 68 | "karma-cli": "~2.0.0", 69 | "karma-coverage-istanbul-reporter": "~2.0.1", 70 | "karma-jasmine": "~2.0.1", 71 | "karma-jasmine-html-reporter": "^1.4.0", 72 | "live-server": "1.2.0", 73 | "ng-packagr": "^5.3.0", 74 | "protractor": "~5.4.0", 75 | "rimraf": "^3.0.0", 76 | "scss-bundle": "^2.5.1", 77 | "ts-node": "~7.0.0", 78 | "tsickle": "^0.36.0", 79 | "tslint": "~5.15.0", 80 | "typescript": "~3.5.3" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /projects/nestable/README.md: -------------------------------------------------------------------------------- 1 | # Nestable 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.0. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project nestable` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project nestable`. 8 | > Note: Don't forget to add `--project nestable` or else it will be added to the default project in your `angular.json` file. 9 | 10 | ## Build 11 | 12 | Run `ng build nestable` to build the project. The build artifacts will be stored in the `dist/` directory. 13 | 14 | ## Publishing 15 | 16 | After building your library with `ng build nestable`, go to the dist folder `cd dist/nestable` and run `npm publish`. 17 | 18 | ## Running unit tests 19 | 20 | Run `ng test nestable` to execute the unit tests via [Karma](https://karma-runner.github.io). 21 | 22 | ## Further help 23 | 24 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 25 | -------------------------------------------------------------------------------- /projects/nestable/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/nestable'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 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 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/nestable/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-nestable", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/nestable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-nestable", 3 | "description": "Nestable list with drag and drop for Angular", 4 | "version": "0.9.2", 5 | "license": "MIT", 6 | "homepage": "https://github.com/cybercomet/ngx-nestable", 7 | "private": false, 8 | "contributors": [ 9 | { 10 | "name" : "Petar Markov", 11 | "email" : "speculees@gmail.com" 12 | }, 13 | { 14 | "name" : "Zlatomir Darabuc - Zlaya", 15 | "email" : "zlayabre@gmail.com" 16 | }, 17 | { 18 | "name" : "Mihajlo Grubjesic", 19 | "email" : "mgrubjesic89@gmail.com" 20 | } 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/cybercomet/ngx-nestable.git", 25 | "directory": "projects/nestable" 26 | }, 27 | "keywords": [ 28 | "angular", 29 | "nestable" 30 | ], 31 | "peerDependencies": { 32 | "@angular/common": "^8.2.0", 33 | "@angular/core": "^8.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/nestable/scss-bundle.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "./projects/nestable/src/lib/_nestable-theme.scss", 3 | "dest": "./dist/ngx-nestable/_nestable-theme.scss" 4 | } 5 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/_nestable-theme.scss: -------------------------------------------------------------------------------- 1 | @mixin nestable-theme($theme) { 2 | $primary: map-get($theme, primary); 3 | $accent: map-get($theme, accent); 4 | $warn: map-get($theme, warn); 5 | $foreground: map-get($theme, foreground); 6 | $background: map-get($theme, background); 7 | 8 | ngx-nestable { 9 | ul { 10 | .dd-placeholder { 11 | background: mat-color($primary, 200); 12 | } 13 | 14 | li { 15 | .nestable-expand-button { 16 | color: mat-color($foreground, 'text'); 17 | } 18 | 19 | .nestable-item-container { 20 | background: mat-color($background, card); 21 | color: mat-color($foreground, 'text'); 22 | } 23 | 24 | .nestable-item-container:hover { 25 | background: mat-color($background, 'hover'); 26 | color: mat-color($foreground, 'text'); 27 | } 28 | } 29 | } 30 | } 31 | .dd-dragel { 32 | background-color: rgba(mat-color($primary, 100), 0.5); 33 | border: 2px dashed mat-color($primary, 500); 34 | 35 | .ngx-nestable-hidden, button { 36 | visibility: hidden; 37 | } 38 | 39 | .ngx-nestable-none { 40 | display: none; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable-drag-handle/nestable-drag-handle.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { ElementRef } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { NestableDragHandleDirective } from './nestable-drag-handle.directive'; 5 | 6 | describe('NestableDragHandleDirective', () => { 7 | 8 | const elementRef = {} as ElementRef; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ElementRef, NestableDragHandleDirective] 13 | }); 14 | }); 15 | 16 | it('should create an instance', () => { 17 | const directive = new NestableDragHandleDirective(elementRef); 18 | expect(directive).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable-drag-handle/nestable-drag-handle.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core'; 2 | 3 | import { DRAG_START, REGISTER_HANDLE } from '../nestable.constant'; 4 | 5 | @Directive({ 6 | selector: '[ngxNestableDragHandle]' 7 | }) 8 | export class NestableDragHandleDirective implements OnInit { 9 | @Input() public ngxNestableDragHandle; 10 | 11 | @HostListener('mousedown', ['$event']) 12 | public onMouseDown(event) { 13 | const detail = { 14 | param: this.ngxNestableDragHandle, 15 | event: event 16 | }; 17 | this._el.nativeElement.dispatchEvent( 18 | new CustomEvent(DRAG_START, { bubbles: true, detail: detail }) 19 | ); 20 | } 21 | 22 | constructor(private _el: ElementRef) {} 23 | 24 | ngOnInit(): void { 25 | this._el.nativeElement.dispatchEvent( 26 | new CustomEvent(REGISTER_HANDLE, { bubbles: true, detail: this.ngxNestableDragHandle }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable-expand-collapse-handle/nestable-expand-collapse.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { NestableExpandCollapseDirective } from './nestable-expand-collapse.directive'; 2 | import { ElementRef } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { NestableDragHandleDirective } from '../nestable-drag-handle/nestable-drag-handle.directive'; 5 | 6 | describe('NestableExpandCollapseDirective', () => { 7 | 8 | const elementRef = {} as ElementRef; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ElementRef, NestableDragHandleDirective] 13 | }); 14 | }); 15 | 16 | it('should create an instance', () => { 17 | const directive = new NestableExpandCollapseDirective(elementRef); 18 | expect(directive).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable-expand-collapse-handle/nestable-expand-collapse.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; 2 | import { EXPAND_COLLAPSE } from '../nestable.constant'; 3 | 4 | @Directive({ 5 | selector: '[ngxNestableExpandCollapse]' 6 | }) 7 | export class NestableExpandCollapseDirective { 8 | @Input() public ngxNestableExpandCollapse; 9 | 10 | constructor(private _el: ElementRef) {} 11 | 12 | @HostListener('mousedown', ['$event']) 13 | public onMouseDown(event) { 14 | event.stopPropagation(); 15 | } 16 | 17 | @HostListener('click', ['$event']) 18 | public onClick(event) { 19 | this.ngxNestableExpandCollapse.item['$$expanded'] = !this.ngxNestableExpandCollapse.item['$$expanded']; 20 | this._el.nativeElement.dispatchEvent( 21 | new CustomEvent(EXPAND_COLLAPSE, { 22 | bubbles: true, 23 | detail: this.ngxNestableExpandCollapse 24 | }) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | 22 | 23 |
24 | 26 | 27 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.component.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 2 | 3 | ul { 4 | .dd-placeholder { 5 | margin: 5px 0; 6 | padding: 0; 7 | min-height: 30px; 8 | background: #f2fbff; 9 | border: 1px dashed #b6bcbf; 10 | box-sizing: border-box; 11 | } 12 | 13 | li { 14 | 15 | .nestable-item-mask { 16 | display: none; 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | right: 0; 21 | left: 0; 22 | z-index: 9998; 23 | } 24 | 25 | .nestable-expand-button { 26 | display: block; 27 | position: relative; 28 | cursor: pointer; 29 | float: left; 30 | width: 25px; 31 | height: 14px; 32 | padding: 0; 33 | white-space: nowrap; 34 | overflow: hidden; 35 | border: 0; 36 | background: transparent; 37 | font-size: 18px; 38 | line-height: 1; 39 | text-align: center; 40 | font-weight: bold; 41 | outline: 0; 42 | } 43 | 44 | .nestable-item-container { 45 | position: relative; 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | color: rgba(0, 0, 0, .87); 50 | min-height: 32px; 51 | font-size: 16px; 52 | font-family: 'Roboto', sans-serif; 53 | cursor: pointer; 54 | outline: 0; 55 | margin-bottom: 2px; 56 | padding-left: 8px; 57 | } 58 | 59 | .nestable-item-container:hover { 60 | background: rgba(0, 0, 0, .04); 61 | } 62 | } 63 | } 64 | 65 | ol, ul { 66 | list-style: none; 67 | } 68 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NestableComponent } from './nestable.component'; 4 | 5 | describe('NestableComponent', () => { 6 | let component: NestableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NestableComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NestableComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | Input, 8 | NgZone, 9 | OnDestroy, 10 | OnInit, 11 | Output, 12 | Renderer2, 13 | ViewContainerRef, 14 | ViewEncapsulation 15 | } from '@angular/core'; 16 | 17 | import * as helper from './nestable.helper'; 18 | 19 | import { defaultSettings, DRAG_START, EXPAND_COLLAPSE, mouse, REGISTER_HANDLE } from './nestable.constant'; 20 | 21 | const PX = 'px'; 22 | const hasPointerEvents = (function () { 23 | const el = document.createElement('div'), 24 | docEl = document.documentElement; 25 | 26 | if (!('pointerEvents' in el.style)) { 27 | return false; 28 | } 29 | 30 | el.style.pointerEvents = 'auto'; 31 | el.style.pointerEvents = 'x'; 32 | docEl.appendChild(el); 33 | const supports = 34 | window.getComputedStyle && 35 | window.getComputedStyle(el, '').pointerEvents === 'auto'; 36 | docEl.removeChild(el); 37 | return !!supports; 38 | })(); 39 | 40 | @Component({ 41 | selector: 'ngx-nestable', 42 | templateUrl: './nestable.component.html', 43 | styleUrls: ['./nestable.component.scss'], 44 | encapsulation: ViewEncapsulation.None, 45 | changeDetection: ChangeDetectionStrategy.OnPush 46 | }) 47 | export class NestableComponent implements OnInit, OnDestroy { 48 | @Output() public listChange = new EventEmitter(); 49 | @Output() public drop = new EventEmitter(); 50 | @Output() public drag = new EventEmitter(); 51 | @Output() public disclosure = new EventEmitter(); 52 | 53 | @Input() public template: ViewContainerRef; 54 | @Input() public options = defaultSettings; 55 | @Input() public disableDrag = false; 56 | 57 | @Input() 58 | public get list() { 59 | return this._list; 60 | } 61 | 62 | public set list(list) { 63 | this._list = list; 64 | this._generateItemIds(); 65 | } 66 | 67 | public dragRootEl = null; 68 | public dragEl = null; 69 | public dragModel = null; 70 | public moving = false; 71 | 72 | /** 73 | * Dragged element contains children, and those children contain other children and so on... 74 | * This property gives you the number of generations contained within the dragging item. 75 | */ 76 | public dragDepth = 0; 77 | 78 | /** 79 | * The depth of dragging item relative to element root (ngx-nestable) 80 | */ 81 | public relativeDepth = 0; 82 | 83 | public hasNewRoot = false; 84 | public pointEl = null; 85 | public items = []; 86 | 87 | private _componentActive = false; 88 | private _mouse = Object.assign({}, mouse); 89 | private _list = []; 90 | // private _options = Object.assign({}, defaultSettings) as NestableSettings; 91 | private _cancelMousemove: Function; 92 | private _cancelMouseup: Function; 93 | private _placeholder; 94 | private _itemId = 0; 95 | private _registerHandleDirective = false; 96 | private _dragIndex; 97 | private _parentDragId; 98 | private _oldListLength: any; 99 | 100 | constructor( 101 | private ref: ChangeDetectorRef, 102 | private renderer: Renderer2, 103 | private el: ElementRef, 104 | private zone: NgZone 105 | ) {} 106 | 107 | ngOnInit(): void { 108 | // set/extend default options 109 | this._componentActive = true; 110 | const optionKeys = Object.keys(defaultSettings); 111 | for (const key of optionKeys) { 112 | if (typeof this.options[key] === 'undefined') { 113 | this.options[key] = defaultSettings[key]; 114 | } 115 | } 116 | 117 | this._generateItemIds(); 118 | this._generateItemExpanded(); 119 | this._createHandleListener(); 120 | } 121 | 122 | ngOnDestroy(): void {} 123 | 124 | private _generateItemIds() { 125 | helper._traverseChildren(this._list, item => { 126 | item['$$id'] = this._itemId++; 127 | }); 128 | } 129 | 130 | private _generateItemExpanded() { 131 | helper._traverseChildren(this._list, item => { 132 | if (typeof item.expanded === 'undefined') { 133 | item['$$expanded'] = true; 134 | } else { 135 | item['$$expanded'] = item.expanded; 136 | } 137 | }); 138 | } 139 | 140 | private _createHandleListener() { 141 | this.renderer.listen(this.el.nativeElement, REGISTER_HANDLE, () => { 142 | this._registerHandleDirective = true; 143 | }); 144 | 145 | this.renderer.listen(this.el.nativeElement, DRAG_START, data => { 146 | this.dragStart( 147 | data.detail.event, 148 | data.detail.param.item, 149 | data.detail.param.parentList 150 | ); 151 | }); 152 | 153 | this.renderer.listen(this.el.nativeElement, EXPAND_COLLAPSE, data => { 154 | this.disclosure.emit({ 155 | item: data.detail.item, 156 | expanded: data.detail.item['$$expanded'] 157 | }); 158 | }); 159 | } 160 | 161 | private _createDragClone(event, dragItem) { 162 | this._mouseStart(event, dragItem); 163 | 164 | if (!this._registerHandleDirective) { 165 | this._mouse.offsetY = dragItem.nextElementSibling 166 | ? dragItem.nextElementSibling.clientHeight / 2 167 | : dragItem.clientHeight / 2; 168 | } 169 | 170 | // create drag clone 171 | this.dragEl = document.createElement(this.options.listNodeName); 172 | document.body.appendChild(this.dragEl); 173 | 174 | this.renderer.addClass(this.dragEl, this.options.dragClass); 175 | 176 | // add drag clone to body and set css 177 | this.renderer.setStyle( 178 | this.dragEl, 179 | 'left', 180 | event.pageX - this._mouse.offsetX + PX 181 | ); 182 | this.renderer.setStyle( 183 | this.dragEl, 184 | 'top', 185 | event.pageY - this._mouse.offsetY + PX 186 | ); 187 | this.renderer.setStyle(this.dragEl, 'position', 'absolute'); 188 | this.renderer.setStyle(this.dragEl, 'z-index', 9999); 189 | this.renderer.setStyle(this.dragEl, 'pointer-events', 'none'); 190 | } 191 | 192 | private _createPlaceholder(event, dragItem) { 193 | this._placeholder = document.createElement('div'); 194 | this._placeholder.classList.add(this.options.placeClass); 195 | helper._insertAfter(this._placeholder, dragItem); 196 | dragItem.parentNode.removeChild(dragItem); 197 | this.dragEl.appendChild(dragItem); 198 | this.dragRootEl = dragItem; 199 | } 200 | 201 | /** 202 | * Sets depth proerties (relative and drag) 203 | */ 204 | private _calculateDepth() { 205 | // total depth of dragging item 206 | let depth; 207 | const items = this.dragEl.querySelectorAll(this.options.itemNodeName); 208 | for (let i = 0; i < items.length; i++) { 209 | depth = helper._getParents(items[i], this.dragEl).length; 210 | if (depth > this.dragDepth) { 211 | this.dragDepth = depth; 212 | } 213 | } 214 | 215 | // depth relative to root 216 | this.relativeDepth = helper._getParents( 217 | this._placeholder, 218 | this.el.nativeElement.querySelector(this.options.listNodeName) 219 | ).length; 220 | } 221 | 222 | private _mouseStart(event, dragItem) { 223 | this._mouse.offsetX = event.pageX - helper._offset(dragItem).left; 224 | this._mouse.offsetY = event.pageY - helper._offset(dragItem).top; 225 | this._mouse.startX = this._mouse.lastX = event.pageX; 226 | this._mouse.startY = this._mouse.lastY = event.pageY; 227 | } 228 | 229 | private _mouseUpdate(event) { 230 | // mouse position last events 231 | this._mouse.lastX = this._mouse.nowX; 232 | this._mouse.lastY = this._mouse.nowY; 233 | // mouse position this events 234 | this._mouse.nowX = event.pageX; 235 | this._mouse.nowY = event.pageY; 236 | // distance mouse moved between events 237 | this._mouse.distX = this._mouse.nowX - this._mouse.lastX; 238 | this._mouse.distY = this._mouse.nowY - this._mouse.lastY; 239 | // direction mouse was moving 240 | this._mouse.lastDirX = this._mouse.dirX; 241 | this._mouse.lastDirY = this._mouse.dirY; 242 | // direction mouse is now moving (on both axis) 243 | this._mouse.dirX = 244 | this._mouse.distX === 0 ? 0 : this._mouse.distX > 0 ? 1 : -1; 245 | this._mouse.dirY = 246 | this._mouse.distY === 0 ? 0 : this._mouse.distY > 0 ? 1 : -1; 247 | } 248 | 249 | private _showMasks() { 250 | const masks = this.el.nativeElement.getElementsByClassName( 251 | 'nestable-item-mask' 252 | ); 253 | for (let i = 0; i < masks.length; i++) { 254 | masks[i].style.display = 'block'; 255 | } 256 | } 257 | 258 | private _hideMasks() { 259 | const masks = this.el.nativeElement.getElementsByClassName( 260 | 'nestable-item-mask' 261 | ); 262 | for (let i = 0; i < masks.length; i++) { 263 | masks[i].style.display = 'none'; 264 | } 265 | } 266 | 267 | /** 268 | * calc mouse traverse distance on axis 269 | * @param m - mouse 270 | */ 271 | private _calcMouseDistance(m) { 272 | m.distAxX += Math.abs(m.distX); 273 | if (m.dirX !== 0 && m.dirX !== m.lastDirX) { 274 | m.distAxX = 0; 275 | } 276 | 277 | m.distAxY += Math.abs(m.distY); 278 | if (m.dirY !== 0 && m.dirY !== m.lastDirY) { 279 | m.distAxY = 0; 280 | } 281 | } 282 | 283 | private _move(event) { 284 | let depth, list; 285 | 286 | const dragRect = this.dragEl.getBoundingClientRect(); 287 | this.renderer.setStyle( 288 | this.dragEl, 289 | 'left', 290 | event.pageX - this._mouse.offsetX + PX 291 | ); 292 | this.renderer.setStyle( 293 | this.dragEl, 294 | 'top', 295 | event.pageY - this._mouse.offsetY + PX 296 | ); 297 | 298 | this._mouseUpdate(event); 299 | 300 | // axis mouse is now moving on 301 | const newAx = 302 | Math.abs(this._mouse.distX) > Math.abs(this._mouse.distY) ? 1 : 0; 303 | 304 | // do nothing on first move 305 | if (!this._mouse.moving) { 306 | this._mouse.dirAx = newAx; 307 | this._mouse.moving = 1; 308 | return; 309 | } 310 | 311 | // calc distance moved on this axis (and direction) 312 | if (this._mouse.dirAx !== newAx) { 313 | this._mouse.distAxX = 0; 314 | this._mouse.distAxY = 0; 315 | } else { 316 | this._calcMouseDistance(this._mouse); 317 | } 318 | this._mouse.dirAx = newAx; 319 | 320 | // find list item under cursor 321 | if (!hasPointerEvents) { 322 | this.dragEl.style.visibility = 'hidden'; 323 | } 324 | 325 | const pointEl = document.elementFromPoint( 326 | event.pageX - document.body.scrollLeft, 327 | event.pageY - (window.pageYOffset || document.documentElement.scrollTop) 328 | ); 329 | 330 | if (!hasPointerEvents) { 331 | this.dragEl.style.visibility = 'visible'; 332 | } 333 | 334 | if ( 335 | pointEl && 336 | (pointEl.classList.contains('nestable-item-mask') || 337 | pointEl.classList.contains(this.options.placeClass)) 338 | ) { 339 | this.pointEl = pointEl.parentElement.parentElement; 340 | } else { 341 | return; 342 | } 343 | 344 | /** 345 | * move horizontal 346 | */ 347 | if ( 348 | !this.options.fixedDepth && 349 | this._mouse.dirAx && 350 | this._mouse.distAxX >= this.options.threshold 351 | ) { 352 | // reset move distance on x-axis for new phase 353 | this._mouse.distAxX = 0; 354 | const previous = this._placeholder.previousElementSibling; 355 | 356 | // increase horizontal level if previous sibling exists, is not collapsed, and can have children 357 | if (this._mouse.distX > 0 && previous) { 358 | list = previous.querySelectorAll(this.options.listNodeName); 359 | list = list[list.length - 1]; 360 | 361 | // check if depth limit has reached 362 | depth = helper._getParents( 363 | this._placeholder, 364 | this.el.nativeElement.querySelector(this.options.listNodeName) 365 | ).length; 366 | 367 | if (depth + this.dragDepth <= this.options.maxDepth) { 368 | // create new sub-level if one doesn't exist 369 | if (!list) { 370 | list = document.createElement(this.options.listNodeName); 371 | list.style.paddingLeft = this.options.threshold + PX; 372 | list.appendChild(this._placeholder); 373 | previous.appendChild(list); 374 | // this.setParent(previous); 375 | } else { 376 | // else append to next level up 377 | list = previous.querySelector( 378 | `:scope > ${ this.options.listNodeName }` 379 | ); 380 | list.appendChild(this._placeholder); 381 | } 382 | } 383 | } 384 | // decrease horizontal level 385 | if (this._mouse.distX < 0) { 386 | // we can't decrease a level if an item preceeds the current one 387 | const next = document.querySelector( 388 | `.${ this.options.placeClass } + ${ this.options.itemNodeName }` 389 | ); 390 | const parentElement = this._placeholder.parentElement; 391 | if (!next && parentElement) { 392 | const closestItem = helper._closest( 393 | this._placeholder, 394 | this.options.itemNodeName 395 | ); 396 | 397 | if (closestItem) { 398 | parentElement.removeChild(this._placeholder); 399 | helper._insertAfter(this._placeholder, closestItem); 400 | } 401 | } 402 | } 403 | } 404 | 405 | if (!pointEl.classList.contains('nestable-item-mask')) { 406 | return; 407 | } 408 | 409 | // find root list of item under cursor 410 | const pointElRoot = helper._closest( 411 | this.pointEl, 412 | `.${ this.options.rootClass }` 413 | ), 414 | isNewRoot = pointElRoot 415 | ? this.dragRootEl.dataset['nestable-id'] !== 416 | pointElRoot.dataset['nestable-id'] 417 | : false; 418 | 419 | /** 420 | * move vertical 421 | */ 422 | if (!this._mouse.dirAx || isNewRoot) { 423 | // check if groups match if dragging over new root 424 | if ( 425 | isNewRoot && 426 | this.options.group !== pointElRoot.dataset['nestable-group'] 427 | ) { 428 | return; 429 | } 430 | 431 | // check depth limit 432 | depth = 433 | this.dragDepth - 434 | 1 + 435 | helper._getParents( 436 | this.pointEl, 437 | this.el.nativeElement.querySelector(this.options.listNodeName) 438 | ).length; 439 | 440 | if (depth > this.options.maxDepth) { 441 | return; 442 | } 443 | 444 | const before = 445 | event.pageY < 446 | helper._offset(this.pointEl).top + this.pointEl.clientHeight / 2; 447 | const placeholderParent = this._placeholder.parentNode; 448 | 449 | // get point element depth 450 | let pointRelativeDepth; 451 | pointRelativeDepth = helper._getParents( 452 | this.pointEl, 453 | this.el.nativeElement.querySelector(this.options.listNodeName) 454 | ).length; 455 | 456 | if (this.options.fixedDepth) { 457 | if (pointRelativeDepth === this.relativeDepth - 1) { 458 | const childList = this.pointEl.querySelector( 459 | this.options.listNodeName 460 | ); 461 | if (!childList.children.length) { 462 | childList.appendChild(this._placeholder); 463 | } 464 | } else if (pointRelativeDepth === this.relativeDepth) { 465 | if (before) { 466 | this.pointEl.parentElement.insertBefore( 467 | this._placeholder, 468 | this.pointEl 469 | ); 470 | } else { 471 | helper._insertAfter(this._placeholder, this.pointEl); 472 | } 473 | 474 | if ( 475 | Array.prototype.indexOf.call( 476 | this.pointEl.parentElement.children, 477 | this.pointEl 478 | ) === 479 | this.pointEl.parentElement.children.length - 1 480 | ) { 481 | helper._insertAfter(this._placeholder, this.pointEl); 482 | } 483 | } 484 | } else if (before) { 485 | this.pointEl.parentElement.insertBefore( 486 | this._placeholder, 487 | this.pointEl 488 | ); 489 | } else { 490 | helper._insertAfter(this._placeholder, this.pointEl); 491 | } 492 | } 493 | } 494 | 495 | public reset() { 496 | const keys = Object.keys(this._mouse); 497 | for (const key of keys) { 498 | this._mouse[key] = 0; 499 | } 500 | 501 | this._itemId = 0; 502 | this.moving = false; 503 | this.dragEl = null; 504 | this.dragRootEl = null; 505 | this.dragDepth = 0; 506 | this.relativeDepth = 0; 507 | this.hasNewRoot = false; 508 | this.pointEl = null; 509 | } 510 | 511 | public dragStartFromItem(event, item, parentList) { 512 | if (!this._registerHandleDirective) { 513 | this.dragStart(event, item, parentList); 514 | } 515 | } 516 | 517 | private dragStart(event, item, parentList) { 518 | 519 | this._oldListLength = this.list.length; 520 | 521 | if (!this.options.disableDrag) { 522 | event.stopPropagation(); 523 | event.preventDefault(); 524 | 525 | if (event.originalEvent) { 526 | event = event.originalEvent; 527 | } 528 | 529 | // allow only first mouse button 530 | if (event.type.indexOf('mouse') === 0) { 531 | if (event.button !== 0) { 532 | return; 533 | } 534 | } else { 535 | if (event.touches.length !== 1) { 536 | return; 537 | } 538 | } 539 | 540 | this.ref.detach(); 541 | this._dragIndex = parentList.indexOf(item); 542 | this.dragModel = parentList.splice(parentList.indexOf(item), 1)[0]; 543 | 544 | const dragItem = helper._closest(event.target, this.options.itemNodeName); 545 | if (dragItem === null) { 546 | return; 547 | } 548 | this._parentDragId = Number.parseInt( 549 | dragItem.parentElement.parentElement.id 550 | ); 551 | 552 | const dragRect = dragItem.getBoundingClientRect(); 553 | 554 | this._showMasks(); 555 | this._createDragClone(event, dragItem); 556 | this.renderer.setStyle(this.dragEl, 'width', dragRect.width + PX); 557 | 558 | this._createPlaceholder(event, dragItem); 559 | this.renderer.setStyle(this._placeholder, 'height', dragRect.height + PX); 560 | 561 | this._calculateDepth(); 562 | this.drag.emit({ 563 | originalEvent: event, 564 | item 565 | }); 566 | 567 | this._cancelMouseup = this.renderer.listen( 568 | document, 569 | 'mouseup', 570 | this.dragStop.bind(this) 571 | ); 572 | this._cancelMousemove = this.renderer.listen( 573 | document, 574 | 'mousemove', 575 | this.dragMove.bind(this) 576 | ); 577 | } 578 | } 579 | 580 | public dragStop(event) { 581 | this._cancelMouseup(); 582 | this._cancelMousemove(); 583 | this._hideMasks(); 584 | 585 | if (this.dragEl) { 586 | const draggedId = Number.parseInt(this.dragEl.firstElementChild.id); 587 | let placeholderContainer = helper._closest( 588 | this._placeholder, 589 | this.options.itemNodeName 590 | ); 591 | 592 | let changedElementPosition = 593 | this._dragIndex !== 594 | Array.prototype.indexOf.call( 595 | this._placeholder.parentElement.children, 596 | this._placeholder 597 | ); 598 | 599 | const index = Array.prototype.indexOf.call(this._placeholder.parentElement.children, this._placeholder); 600 | 601 | if ((this._dragIndex === index) && (this._oldListLength === this.list.length)) { 602 | changedElementPosition = true; 603 | } 604 | 605 | // placeholder in root 606 | if (placeholderContainer === null) { 607 | this.list.splice( 608 | Array.prototype.indexOf.call( 609 | this._placeholder.parentElement.children, 610 | this._placeholder 611 | ), 612 | 0, 613 | { ...this.dragModel } 614 | ); 615 | } else { 616 | // palceholder nested 617 | placeholderContainer = helper._findObjectInTree( 618 | this.list, 619 | Number.parseInt(placeholderContainer.id) 620 | ); 621 | if (!placeholderContainer.children) { 622 | placeholderContainer.children = []; 623 | placeholderContainer.children.push({ ...this.dragModel }); 624 | } else { 625 | placeholderContainer.children.splice( 626 | Array.prototype.indexOf.call( 627 | this._placeholder.parentElement.children, 628 | this._placeholder 629 | ), 630 | 0, 631 | { ...this.dragModel } 632 | ); 633 | } 634 | if (index === this._dragIndex) { 635 | changedElementPosition = false; 636 | } 637 | if (!changedElementPosition) { 638 | changedElementPosition = 639 | placeholderContainer['$$id'] !== this._parentDragId; 640 | } 641 | } 642 | 643 | this._placeholder.parentElement.removeChild(this._placeholder); 644 | this.dragEl.parentNode.removeChild(this.dragEl); 645 | this.dragEl.remove(); 646 | this.reset(); 647 | 648 | this.listChange.emit(this.list); 649 | this.drop.emit({ 650 | originalEvent: event, 651 | destination: placeholderContainer, 652 | item: this.dragModel, 653 | changedElementPosition 654 | }); 655 | this.ref.reattach(); 656 | } 657 | } 658 | 659 | public dragMove(event) { 660 | if (this.dragEl) { 661 | event.preventDefault(); 662 | 663 | if (event.originalEvent) { 664 | event = event.originalEvent; 665 | } 666 | this._move(event.type.indexOf('mouse') === 0 ? event : event.touches[0]); 667 | } 668 | } 669 | 670 | public expandAll() { 671 | helper._traverseChildren(this._list, item => { 672 | item['$$expanded'] = true; 673 | }); 674 | this.ref.markForCheck(); 675 | } 676 | 677 | public collapseAll() { 678 | helper._traverseChildren(this._list, item => { 679 | item['$$expanded'] = false; 680 | }); 681 | this.ref.markForCheck(); 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.constant.ts: -------------------------------------------------------------------------------- 1 | import { NestableSettings } from './nestable.models'; 2 | 3 | export const REGISTER_HANDLE = 'NESTABLE_DRAG_HANDLE_REGISTER'; 4 | export const DRAG_START = 'NESTABLE_DRAG_HANDLE_START'; 5 | export const EXPAND_COLLAPSE = 'NESTABLE_EXPAND_COLLAPSE_EVENT'; 6 | 7 | export const defaultSettings = { 8 | listNodeName: 'ul', 9 | itemNodeName: 'li', 10 | rootClass: 'dd', 11 | listClass: 'dd-list', 12 | itemClass: 'dd-item', 13 | dragClass: 'dd-dragel', 14 | handleClass: 'dd-handle', 15 | collapsedClass: 'dd-collapsed', 16 | placeClass: 'dd-placeholder', 17 | group: 0, // TODO 18 | maxDepth: 5, 19 | threshold: 20, 20 | fixedDepth: false, // fixed item's depth 21 | exportCollapsed: true, // TODO 22 | disableDrag: false, 23 | } as NestableSettings; 24 | 25 | export const mouse = { 26 | moving: 0, 27 | offsetX: 0, 28 | offsetY: 0, 29 | startX: 0, 30 | startY: 0, 31 | lastX: 0, 32 | lastY: 0, 33 | nowX: 0, 34 | nowY: 0, 35 | distX: 0, 36 | distY: 0, 37 | dirAx: 0, 38 | dirX: 0, 39 | dirY: 0, 40 | lastDirX: 0, 41 | lastDirY: 0, 42 | distAxX: 0, 43 | distAxY: 0 44 | }; 45 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.helper.ts: -------------------------------------------------------------------------------- 1 | export const _traverseChildren = (tree, callback, parent = null) => { 2 | for (let i = 0; i < tree.length; i++) { 3 | const item = tree[i]; 4 | if (typeof item === 'undefined') { 5 | continue; 6 | } 7 | const callbackResult = callback(item, parent); 8 | 9 | if (callbackResult) { 10 | break; 11 | } 12 | 13 | if (item.children) { 14 | _traverseChildren(item.children, callback, item); 15 | } 16 | } 17 | }; 18 | 19 | export const _insertAfter = (newNode, referenceNode) => { 20 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 21 | }; 22 | 23 | export const _replace = (newNode, referenceNode) => { 24 | referenceNode.parentNode.replaceChild(newNode, referenceNode); 25 | }; 26 | 27 | export const _replaceTargetWithElements = (target, elements) => { 28 | let i = elements.length; 29 | 30 | if (target.parentNode) { 31 | while (i--) { 32 | target.parentNode.insertBefore(elements[i], target); 33 | } 34 | 35 | /// remove the target. 36 | target.parentNode.removeChild(target); 37 | } 38 | }; 39 | 40 | export const _getParents = (el, parentSelector = document.body) => { 41 | 42 | const parents = []; 43 | let parentNode = el.parentNode; 44 | 45 | while (parentNode !== parentSelector) { 46 | const o = parentNode; 47 | if (!parentNode) { 48 | break; 49 | } 50 | if (parentNode.tagName === parentSelector.tagName) { 51 | parents.push(o); 52 | } 53 | parentNode = o.parentNode; 54 | } 55 | parents.push(parentSelector); // Push that parentSelector you wanted to stop at 56 | 57 | return parents; 58 | }; 59 | 60 | export const _closest = (el, selector) => { 61 | let matchesFn; 62 | 63 | // find vendor prefix 64 | ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some(function (fn) { 65 | if (typeof document.body[fn] === 'function') { 66 | matchesFn = fn; 67 | return true; 68 | } 69 | return false; 70 | }); 71 | 72 | let parent; 73 | 74 | // traverse parents 75 | while (el) { 76 | parent = el.parentElement; 77 | if (parent === null) { 78 | break; 79 | } 80 | const matches = parent[matchesFn](selector); 81 | if (parent && matches) { 82 | return parent; 83 | } 84 | el = parent; 85 | } 86 | 87 | return null; 88 | }; 89 | 90 | export const _offset = (elem) => { 91 | let box = { top: 0, left: 0 }; 92 | 93 | // BlackBerry 5, iOS 3 (original iPhone) 94 | if (typeof elem.getBoundingClientRect !== undefined) { 95 | box = elem.getBoundingClientRect(); 96 | } 97 | 98 | return { 99 | top: box.top + (window.pageYOffset || elem.scrollTop) - (elem.clientTop || 0), 100 | left: box.left + (window.pageXOffset || elem.scrollLeft) - (elem.clientLeft || 0) 101 | }; 102 | }; 103 | 104 | export const _findObjectInTree = (array, id) => { 105 | let result = null; 106 | 107 | _traverseChildren(array, item => { 108 | if (item['$$id'] === Number.parseInt(id)) { 109 | result = item; 110 | return true; 111 | } 112 | }); 113 | 114 | return result; 115 | }; 116 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.models.ts: -------------------------------------------------------------------------------- 1 | export interface NestableSettings { 2 | listNodeName?: string; 3 | itemNodeName?: string; 4 | handleNodeName?: string; 5 | contentNodeName?: string; 6 | rootClass?: string; 7 | listClass?: string; 8 | itemClass?: string; 9 | dragClass?: string; 10 | handleClass?: string; 11 | contentClass?: string; 12 | collapsedClass?: string; 13 | placeClass?: string; 14 | noDragClass?: string; 15 | noChildrenClass?: string; 16 | emptyClass?: string; 17 | expandBtnHTML?: string; 18 | collapseBtnHTML?: string; 19 | group?: number; 20 | maxDepth?: number; 21 | threshold?: number; 22 | fixedDepth?: boolean; 23 | fixed?: boolean; 24 | exportCollapsed?: boolean; 25 | disableDrag?: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /projects/nestable/src/lib/nestable.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { NestableComponent } from './nestable.component'; 5 | import { NestableDragHandleDirective } from './nestable-drag-handle/nestable-drag-handle.directive'; 6 | 7 | import { NestableExpandCollapseDirective } from './nestable-expand-collapse-handle/nestable-expand-collapse.directive'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule], 11 | declarations: [ 12 | NestableComponent, 13 | NestableDragHandleDirective, 14 | NestableExpandCollapseDirective 15 | ], 16 | exports: [ 17 | NestableComponent, 18 | NestableDragHandleDirective, 19 | NestableExpandCollapseDirective 20 | ] 21 | }) 22 | export class NestableModule { 23 | } 24 | -------------------------------------------------------------------------------- /projects/nestable/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of nestable 3 | */ 4 | 5 | export * from './lib/nestable.component'; 6 | export * from './lib/nestable.module'; 7 | -------------------------------------------------------------------------------- /projects/nestable/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/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /projects/nestable/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true 21 | }, 22 | "exclude": [ 23 | "src/test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /projects/nestable/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/nestable/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "ngx", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "ngx", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | Ngx-nestable demo 3 |
4 | 6 |
7 |
8 | 9 |
10 |
13 | 16 | 19 | 22 |
23 | 25 | Fixed depth 26 | 27 |
28 | 29 |
33 | 41 | 42 | 43 |
47 | 48 |
49 |
50 |
51 | 52 | 54 | 58 | 64 |
Item: {{row.item.id}}
65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .demo-content { 8 | flex-direction: column; 9 | flex: 1 1 auto; 10 | padding: 0 16px; 11 | } 12 | 13 | .demo-white-circle { 14 | background-color: white; 15 | width: 56px; 16 | height: 56px; 17 | border-radius: 50%; 18 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Renderer2 } from '@angular/core'; 2 | import { NestableSettings } from '../../projects/nestable/src/lib/nestable.models'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent { 10 | 11 | public idCount = 13; 12 | public options = { 13 | fixedDepth: false 14 | } as NestableSettings; 15 | public list = [ 16 | { 'id': 1 }, 17 | { 18 | 'expanded': true, 19 | 'id': 2, 'children': [ 20 | { 'id': 3 }, 21 | { 'id': 4 }, 22 | { 23 | 'expanded': false, 24 | 'id': 5, 'children': [ 25 | { 'id': 6 }, 26 | { 'id': 7 }, 27 | { 'id': 8 } 28 | ] 29 | }, 30 | { 'id': 9 }, 31 | { 'id': 10 } 32 | ] 33 | }, 34 | { 'id': 11 }, 35 | { 36 | 'id': 12, 37 | 'children': [ 38 | { 'id': 13 } 39 | ] 40 | }, 41 | { 'id': 14 }, 42 | { 'id': 15 } 43 | ]; 44 | 45 | constructor( 46 | private el: ElementRef, 47 | private renderer: Renderer2 48 | ) { 49 | this.renderer.listen(this.el.nativeElement, 'listUpdated', e => { 50 | this.list = e.detail.list; 51 | }); 52 | } 53 | 54 | public pushItem() { 55 | this.list.push({ id: ++this.idCount }); 56 | this.list = [...this.list]; 57 | } 58 | 59 | public toggleFixedDepth() { 60 | this.options.fixedDepth = !this.options.fixedDepth; 61 | } 62 | 63 | public drag(e) { 64 | console.log(e); 65 | } 66 | 67 | public drop(e) { 68 | console.log(e); 69 | } 70 | 71 | public onDisclosure(e) { 72 | console.log(e); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { NestableModule } from '../../projects/nestable/src/lib/nestable.module'; 7 | import { MatButtonModule, MatIconModule, MatSlideToggleModule, MatToolbarModule } from '@angular/material'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | AppComponent 12 | ], 13 | imports: [ 14 | BrowserModule, 15 | NestableModule, 16 | MatButtonModule, 17 | MatIconModule, 18 | MatToolbarModule, 19 | MatSlideToggleModule, 20 | FlexLayoutModule 21 | ], 22 | providers: [], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppModule { } 26 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybercomet/ngx-nestable/350489fb643a9c6754cbc74f62aaefef0eb3c90c/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/ngx_nestable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybercomet/ngx-nestable/350489fb643a9c6754cbc74f62aaefef0eb3c90c/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxNestable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/theming'; 3 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 4 | @import "../projects/nestable/src/lib/nestable-theme"; 5 | // Plus imports for other components in your app. 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat-core(); 11 | 12 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 13 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 14 | // hue. Available color palettes: https://www.google.com/design/spec/style/color.html 15 | $candy-app-primary: mat-palette($mat-indigo); 16 | $candy-app-accent: mat-palette($mat-pink, A200, A100, A400); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $candy-app-warn: mat-palette($mat-red); 20 | 21 | // Create the theme object (a Sass map containing all of the palettes). 22 | $candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn); 23 | 24 | // Include theme styles for core and each component used in your app. 25 | // Alternatively, you can import and @include the theme mixins for each component 26 | // that you are using. 27 | @include angular-material-theme($candy-app-theme); 28 | @include nestable-theme($candy-app-theme); 29 | /* http://meyerweb.com/eric/tools/css/reset/ 30 | v2.0 | 20110126 31 | License: none (public domain) 32 | */ 33 | 34 | html, body, div, span, applet, object, iframe, 35 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 36 | a, abbr, acronym, address, big, cite, code, 37 | del, dfn, em, img, ins, kbd, q, s, samp, 38 | small, strike, strong, sub, sup, tt, var, 39 | b, u, i, center, 40 | dl, dt, dd, ol, ul, li, 41 | fieldset, form, label, legend, 42 | table, caption, tbody, tfoot, thead, tr, th, td, 43 | article, aside, canvas, details, embed, 44 | figure, figcaption, footer, header, hgroup, 45 | menu, nav, output, ruby, section, summary, 46 | time, mark, audio, video { 47 | margin: 0; 48 | padding: 0; 49 | border: 0; 50 | font-size: 100%; 51 | font: inherit; 52 | vertical-align: baseline; 53 | } 54 | /* HTML5 display-role reset for older browsers */ 55 | article, aside, details, figcaption, figure, 56 | footer, header, hgroup, menu, nav, section { 57 | display: block; 58 | } 59 | body { 60 | line-height: 1; 61 | background-color: mat-color(map-get($candy-app-theme, background), background); 62 | } 63 | 64 | blockquote, q { 65 | quotes: none; 66 | } 67 | blockquote:before, blockquote:after, 68 | q:before, q:after { 69 | content: ''; 70 | content: none; 71 | } 72 | table { 73 | border-collapse: collapse; 74 | border-spacing: 0; 75 | } 76 | -------------------------------------------------------------------------------- /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/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts", 14 | "polyfills.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "nestable": [ 23 | "dist/nestable" 24 | ], 25 | "nestable/*": [ 26 | "dist/nestable/*" 27 | ] 28 | } 29 | }, 30 | "angularCompilerOptions": { 31 | "fullTemplateTypeCheck": true, 32 | "strictInjectionParameters": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /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 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "directive-selector": [ 120 | true, 121 | "attribute", 122 | ["ngx", "app"], 123 | "camelCase" 124 | ], 125 | "component-selector": [ 126 | true, 127 | "element", 128 | ["ngx", "app"], 129 | "kebab-case" 130 | ], 131 | "no-output-on-prefix": true, 132 | "use-input-property-decorator": true, 133 | "use-output-property-decorator": true, 134 | "use-host-property-decorator": true, 135 | "no-input-rename": true, 136 | "no-output-rename": true, 137 | "use-life-cycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "component-class-suffix": true, 140 | "directive-class-suffix": true 141 | } 142 | } 143 | --------------------------------------------------------------------------------