├── .all-contributorsrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── angular.json ├── jest.base.config.js ├── package-lock.json ├── package.json ├── projects ├── ng-sortgrid-demo │ ├── .browserslistrc │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── app.routing.module.ts │ │ │ ├── introduction │ │ │ │ ├── examples │ │ │ │ │ ├── async-pipe │ │ │ │ │ │ ├── async-pipe-memory.component.css │ │ │ │ │ │ ├── async-pipe-memory.component.html │ │ │ │ │ │ └── async-pipe-memory.component.ts │ │ │ │ │ ├── drag-handle │ │ │ │ │ │ ├── drag-handle.component.css │ │ │ │ │ │ ├── drag-handle.component.html │ │ │ │ │ │ └── drag-handle.component.ts │ │ │ │ │ ├── getting-started │ │ │ │ │ │ ├── getting-started-memory.component.html │ │ │ │ │ │ └── getting-started-memory.component.ts │ │ │ │ │ ├── groups │ │ │ │ │ │ ├── groups-memory.component.html │ │ │ │ │ │ └── groups-memory.component.ts │ │ │ │ │ └── react-on-changes │ │ │ │ │ │ ├── react-on-changes-memory.component.html │ │ │ │ │ │ └── react-on-changes-memory.component.ts │ │ │ │ ├── introduction.component.html │ │ │ │ ├── introduction.component.spec.ts │ │ │ │ ├── introduction.component.ts │ │ │ │ ├── introduction.module.ts │ │ │ │ └── introduction.routing.module.ts │ │ │ ├── scrolling │ │ │ │ ├── scrolling.component.html │ │ │ │ ├── scrolling.component.spec.ts │ │ │ │ ├── scrolling.component.ts │ │ │ │ ├── scrolling.module.ts │ │ │ │ └── scrolling.routing.module.ts │ │ │ └── shared │ │ │ │ ├── card │ │ │ │ ├── card.component.css │ │ │ │ ├── card.component.html │ │ │ │ └── card.component.ts │ │ │ │ ├── header │ │ │ │ ├── header.component.css │ │ │ │ ├── header.component.html │ │ │ │ └── header.component.ts │ │ │ │ ├── nav │ │ │ │ ├── nav.component.css │ │ │ │ ├── nav.component.html │ │ │ │ └── nav.component.ts │ │ │ │ ├── shared.module.ts │ │ │ │ └── step │ │ │ │ ├── step.component.css │ │ │ │ ├── step.component.html │ │ │ │ └── step.component.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── grid-demo.gif │ │ │ ├── gs1.png │ │ │ ├── gs2.png │ │ │ ├── gs3.png │ │ │ ├── gs4.png │ │ │ ├── gs5.png │ │ │ ├── gs6.png │ │ │ ├── gs7.png │ │ │ ├── ng-sortgrid-logo.png │ │ │ ├── scrolling-code.png │ │ │ └── scrolling.png │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ └── styles.css │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json └── ng-sortgrid │ ├── jest.config.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── helpers │ │ │ ├── class │ │ │ │ ├── ngsg-class.service.spec.ts │ │ │ │ └── ngsg-class.service.ts │ │ │ ├── element │ │ │ │ ├── ngsg-elements.helper.spec.ts │ │ │ │ └── ngsg-elements.helper.ts │ │ │ └── scroll │ │ │ │ ├── scroll-helper.service.spec.ts │ │ │ │ └── scroll-helper.service.ts │ │ ├── mutliselect │ │ │ ├── ngsg-selection.service.spec.ts │ │ │ └── ngsg-selection.service.ts │ │ ├── ngsg-drag-handle.directive.spec.ts │ │ ├── ngsg-drag-handle.directive.ts │ │ ├── ngsg-item.directive.spec.ts │ │ ├── ngsg-item.directive.ts │ │ ├── ngsg.css │ │ ├── ngsg.module.ts │ │ ├── shared │ │ │ ├── ngsg-dragelement.model.ts │ │ │ ├── ngsg-events.service.ts │ │ │ └── ngsg-order-change.model.ts │ │ ├── sort │ │ │ ├── reflection │ │ │ │ ├── ngsg-reflect.service.spec.ts │ │ │ │ └── ngsg-reflect.service.ts │ │ │ └── sort │ │ │ │ ├── ngsg-sort.service.spec.ts │ │ │ │ └── ngsg-sort.service.ts │ │ └── store │ │ │ ├── ngsg-store.service.spec.ts │ │ │ └── ngsg-store.service.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── setupJest.ts ├── tsconfig.json └── tslint.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "GonCarvalho98", 12 | "name": "Gonçalo", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/103566451?v=4", 14 | "profile": "https://github.com/GonCarvalho98", 15 | "contributions": [ 16 | "code" 17 | ] 18 | } 19 | ], 20 | "contributorsPerLine": 7, 21 | "skipCi": true, 22 | "repoType": "github", 23 | "repoHost": "https://github.com", 24 | "projectName": "ng-sortgrid", 25 | "projectOwner": "kreuzerk" 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/no-explicit-any": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kreuzerk] 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: feature-branch 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | - '*/*' 7 | - '**' 8 | - '!master' 9 | jobs: 10 | CI: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | - name: Install node modules 18 | run: npm ci 19 | - name: Test 20 | run: npm run test 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | persist-credentials: false 14 | - name: Install Node & NPM 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - name: Install node_modules 19 | run: npm ci 20 | - name: Test 21 | run: npm run test 22 | - name: Build 23 | run: npm run build 24 | - name: Release 25 | uses: cycjimmy/semantic-release-action@v3 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # library readme - will be copied during build 4 | projects/ng-sortgrid/README.md 5 | 6 | # compiled output 7 | /dist 8 | /tmp 9 | /out-tsc 10 | # Only exists if Bazel was run 11 | /bazel-out 12 | 13 | # dependencies 14 | /node_modules 15 | 16 | # profiling files 17 | chrome-profiler-events.json 18 | speed-measure-plugin.json 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | .history/* 36 | 37 | # misc 38 | /.angular/cache 39 | /.sass-cache 40 | /connect.lock 41 | /coverage 42 | /libpeerconnection.log 43 | npm-debug.log 44 | yarn-error.log 45 | testem.log 46 | /typings 47 | 48 | # System Files 49 | .DS_Store 50 | Thumbs.db 51 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "pkgRoot": "dist/ng-sortgrid", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | "@semantic-release/npm", 8 | ["@semantic-release/exec", { 9 | "prepareCmd": "VERSION=${nextRelease.version} npm run bump-version" 10 | }], 11 | ["@semantic-release/git", { 12 | "assets": ["package.json", "CHANGELOG.md"], 13 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 14 | }], 15 | "@semantic-release/github" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [18.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v17.0.0...v18.0.0) (2024-07-01) 2 | 3 | 4 | ### Features 5 | 6 | * 🎸 release Angular 18 ([6496fb2](https://github.com/kreuzerk/ng-sortgrid/commit/6496fb26cbea7687e49854ffb41722cf7fc4bfef)) 7 | 8 | 9 | ### BREAKING CHANGES 10 | 11 | * 🧨 Angular 18 12 | 13 | # [18.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v17.0.0...v18.0.0) (2024-07-01) 14 | 15 | 16 | ### Features 17 | 18 | * 🎸 release Angular 18 ([6496fb2](https://github.com/kreuzerk/ng-sortgrid/commit/6496fb26cbea7687e49854ffb41722cf7fc4bfef)) 19 | 20 | 21 | ### BREAKING CHANGES 22 | 23 | * 🧨 Angular 18 24 | 25 | # [18.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v17.0.0...v18.0.0) (2024-07-01) 26 | 27 | 28 | ### Features 29 | 30 | * 🎸 release Angular 18 ([6496fb2](https://github.com/kreuzerk/ng-sortgrid/commit/6496fb26cbea7687e49854ffb41722cf7fc4bfef)) 31 | 32 | 33 | ### BREAKING CHANGES 34 | 35 | * 🧨 Angular 18 36 | 37 | # [17.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v16.0.0...v17.0.0) (2024-03-18) 38 | 39 | 40 | ### Features 41 | 42 | * 🎸 Angular 17 ([bb5e66e](https://github.com/kreuzerk/ng-sortgrid/commit/bb5e66e2d8d21f169216bf1ade05bfaa08a6024b)) 43 | 44 | 45 | ### BREAKING CHANGES 46 | 47 | * 🧨 Angular 17 48 | 49 | # [16.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v15.0.1...v16.0.0) (2023-06-21) 50 | 51 | 52 | ### Features 53 | 54 | * 🎸 Angular 16 version ([5da5d74](https://github.com/kreuzerk/ng-sortgrid/commit/5da5d748194d66b0a40380b7f057e1e6ca69993a)), closes [#114](https://github.com/kreuzerk/ng-sortgrid/issues/114) 55 | 56 | 57 | ### BREAKING CHANGES 58 | 59 | * 🧨 Angular 16 60 | 61 | ## [15.0.1](https://github.com/kreuzerk/ng-sortgrid/compare/v15.0.0...v15.0.1) (2023-01-27) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * **deps:** update package.json deps ([2e01f3f](https://github.com/kreuzerk/ng-sortgrid/commit/2e01f3f241dd3743438062316e24e5597973d04b)) 67 | 68 | # [15.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v14.0.0...v15.0.0) (2023-01-18) 69 | 70 | 71 | ### Features 72 | 73 | * 🎸 angular 15 ([060e2e0](https://github.com/kreuzerk/ng-sortgrid/commit/060e2e0d04c8acc43a701129d811d232d8941679)) 74 | 75 | 76 | ### BREAKING CHANGES 77 | 78 | * 🧨 Angular 15 79 | 80 | # [14.0.0](https://github.com/kreuzerk/ng-sortgrid/compare/v13.0.0...v14.0.0) (2022-09-06) 81 | 82 | 83 | ### Features 84 | 85 | * 🎸 Angular 14 ([c0ff789](https://github.com/kreuzerk/ng-sortgrid/commit/c0ff7892e845ca13b50ea3f67eee67c5482b2f97)) 86 | 87 | 88 | ### BREAKING CHANGES 89 | 90 | * 🧨 Angular 14 91 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | Contributor Covenant Code of Conduct 3 | Our Pledge 4 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | 6 | Our Standards 7 | Examples of behavior that contributes to creating a positive environment include: 8 | 9 | Using welcoming and inclusive language 10 | Being respectful of differing viewpoints and experiences 11 | Gracefully accepting constructive criticism 12 | Focusing on what is best for the community 13 | Showing empathy towards other community members 14 | Examples of unacceptable behavior by participants include: 15 | 16 | The use of sexualized language or imagery and unwelcome sexual attention or advances 17 | Trolling, insulting/derogatory comments, and personal or political attacks 18 | Public or private harassment 19 | Publishing others' private information, such as a physical or electronic address, without explicit permission 20 | Other conduct which could reasonably be considered inappropriate in a professional setting 21 | Our Responsibilities 22 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 23 | 24 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 25 | 26 | Scope 27 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 28 | 29 | Enforcement 30 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kevin.kreuzer90@icloud.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 31 | 32 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 33 | 34 | Attribution 35 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ng-sortgrid 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | [![Travis build badge](https://img.shields.io/travis/kreuzerk/ng-sortgrid.svg)](https://travis-ci.org/kreuzerk/ng-sortgrid) 7 | [![codecov](https://codecov.io/gh/kreuzerk/ng-sortgrid/branch/master/graph/badge.svg)](https://codecov.io/gh/kreuzerk/ng-sortgrid) 8 | [![angular10](https://img.shields.io/badge/angular%2010%20ready-true-green.svg)]() 9 | 10 | ![Logo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/ng-sortgrid-logo.png) 11 | 12 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/grid-demo.gif) 13 | 14 | - - 15 | 16 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 17 | 18 | - [Ng-sortgrid](#ng-sortgrid) 19 | - [Getting started](#getting-started) 20 | - [Download](#download) 21 | - [Apply the directive](#apply-the-directive) 22 | - [React on changes](#react-on-changes) 23 | - [Group sortgrids](#group-sortgrids) 24 | - [Use the async pipe](#use-the-async-pipe) 25 | - [Style your items on different events](#style-your-items-on-different-events) 26 | - [Integrate the build in CSS](#integrate-the-build-in-css) 27 | - [Scrolling](#scrolling) 28 | - [Custom scroll points](#custom-scroll-points) 29 | - [Scroll speed (*default 50*)](#scroll-speed-default-50) 30 | - [API](#api) 31 | - [Inputs](#inputs) 32 | - [Outputs](#outputs) 33 | - [Mobile usage](#mobile-usage) 34 | 35 | 36 | 37 | ## Download 38 | 39 | ``` 40 | npm i ng-sortgrid 41 | ``` 42 | 43 | Import the ```NgsgModule``` in your ```AppModule```. 44 | 45 | ``` 46 | import {NgsgModule} from 'ng-sortgrid' 47 | ... 48 | @NgModule({ 49 | imports: [BrowserModule, NgsgModule], 50 | //... 51 | }) 52 | ... 53 | ``` 54 | 55 | ## Apply the directive 56 | Loop over your elements with *ngFor. 🛎️ the items needs to be an array. Alternate you can also use the async pipe to pass in your items. 57 | 58 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs1.png) 59 | 60 | Apply the ngSortgridItem directive 61 | 62 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs2.png) 63 | 64 | ## React on changes 65 | In most cases you are interested in the new sort order. Often you want to store them in local storage or even send them to the backend. To do so the following two steps are needed in addition to the "Getting started" step. 66 | 67 | Pass your items to the directive via the ngSortGridItems input. 68 | 69 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs3.png) 70 | React on the 'sorted' output event. The `sorted` output event emits a `NgsgOrderChange` which contains the `previousOrder` and the `currentOrder` 71 | 72 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs4.png) 73 | 74 | ## Group sortgrids 75 | In case you have more than one sortgriditem on the page you need to group the sortgriditems to avoid dropping drags from one group in another group. 76 | Pass in a unique name to the ngSortGridGroup input 77 | 78 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs5.png) 79 | 80 | ## Use the async pipe 81 | You can also use the async pipe to display items 82 | 83 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/gs6.png) 84 | 85 | # Style your items on different events 86 | The ng-sortgrid adds different classes on different events to your items. You can either use those classes to style the appereance 87 | of your items on certain events or you can include the build in CSS from the ng-sortgrid library. 88 | 89 | ## Integrate the build in CSS 90 | To integrate the built in Stylesheet just import in in your angular.json. 91 | 92 | ``` 93 | "styles": [ 94 | "node_modules/ng-sortgrid/styles/ngsg.css", 95 | ], 96 | ``` 97 | 98 | Alternative you can provide custom styles for the different classes listed bellow 99 | 100 | | Class | Description | 101 | |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------| 102 | | ng-sg-placeholder | This class is added to the placeholder item which previews where the item is inserted | 103 | | ng-sg-dropped | This class is added as soon after you drop an item. The class will be on the item for 500 milliseconds before it gets removed | 104 | | ng-sg-selected | This class is added when you press the CMD or the Ctrl Key and Click on an item. It indicates which items are selected for the multi drag&drop | 105 | | ng-sg-active | This class is added when dragging item| | 106 | 107 | # Scrolling 108 | The ng-sortgrid has a *autoScroll* flag which you can use to enable autoScroll. If you enable autoScroll the screen will start to scroll 109 | in the following scenario. 110 | 111 | ![Grid demo](https://raw.githubusercontent.com/kreuzerk/ng-sortgrid/master/projects/ng-sortgrid-demo/src/assets/scrolling.png) 112 | 113 | - If you drag an element in the top 50px of the screen 114 | - If you drag an element in the bottom 50px of the screen 115 | 116 | ## Custom scroll points 117 | Sometimes its not enough to only scroll once you drag over the top view port border. Imagine that you have a fixed navbar 118 | at the top of your page. In this case you need to scroll once you drag an element over the navbar. 119 | 120 | ## Scroll speed (*default 50*) 121 | The *scrollSpeed* property accepts a number and allows you to specify the scrolling speed. 122 | 123 | [Check out the scroll demo](https://kreuzerk.github.io/ng-sortgrid/scrolling) 124 | 125 | # API 126 | 127 | ## Inputs 128 | | Value | Description | Default| 129 | |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------|--------| 130 | | ngSortGridGroup: string | Groups a grid - avoids that items from one grid can be dragged to another grid |undefined| 131 | | ngSortGridItems: any[] | Sort grid items. Pass down a list of all your items. This list is needed to enable the sorting feature.|undefined| 132 | | autoScroll: boolean | Flag to enable autoscrolling|false| 133 | | scrollPointTop: number | Custom top scrollpoint in pixels|if autoscroll is applied we start scrolling if we pass the top border| 134 | | scrollPointBottom: number | Custom bottom scrollpoint in pixels|if autoscroll is applied we start scrolling if we pass the bottom border| 135 | | scrollSpeed: number | Scrollspeed, the higher the value, the higher we scroll.|50 - only applies if autoscrolling is on| 136 | 137 | ## Outputs 138 | | Value | Description | Default| 139 | |-------------------|------------------------------------------------------------------------------------------------------------------------------------------------|--------| 140 | | sorted: EventEmitter | Emits an event after we sorted the items, each event is of type NgsgOrderChange. The NgsgOrderChange contains the previousOrder and the currentOrder. Both are freshly created arrays. |undefined| 141 | 142 | # Mobile usage 143 | 144 | If you want to use those events on mobile you probably have to use some polyfills in order to emit all the needed events. Including this polyfill in your app should do the trick. https://github.com/timruffles/mobile-drag-drop. 145 | 146 | ## Contributors ✨ 147 | 148 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
Gonçalo
Gonçalo

💻
160 | 161 | 162 | 163 | 164 | 165 | 166 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ng-sortgrid": { 10 | "root": "projects/ng-sortgrid", 11 | "sourceRoot": "projects/ng-sortgrid/src", 12 | "projectType": "library", 13 | "prefix": "lib", 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:ng-packagr", 17 | "options": { 18 | "tsConfig": "projects/ng-sortgrid/tsconfig.lib.json", 19 | "project": "projects/ng-sortgrid/ng-package.json" 20 | }, 21 | "configurations": { 22 | "production": { 23 | "tsConfig": "projects/ng-sortgrid/tsconfig.lib.json" 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | "ng-sortgrid-demo": { 30 | "root": "projects/ng-sortgrid-demo/", 31 | "sourceRoot": "projects/ng-sortgrid-demo/src", 32 | "projectType": "application", 33 | "prefix": "app", 34 | "schematics": {}, 35 | "architect": { 36 | "build": { 37 | "builder": "@angular-devkit/build-angular:application", 38 | "options": { 39 | "outputPath": { 40 | "base": "dist/ng-sortgrid-demo" 41 | }, 42 | "index": "projects/ng-sortgrid-demo/src/index.html", 43 | "polyfills": [ 44 | "projects/ng-sortgrid-demo/src/polyfills.ts" 45 | ], 46 | "tsConfig": "projects/ng-sortgrid-demo/tsconfig.app.json", 47 | "assets": [ 48 | "projects/ng-sortgrid-demo/src/favicon.ico", 49 | "projects/ng-sortgrid-demo/src/assets" 50 | ], 51 | "styles": [ 52 | "projects/ng-sortgrid-demo/src/styles.css", 53 | "projects/ng-sortgrid/src/lib/ngsg.css", 54 | "node_modules/@fortawesome/fontawesome-free/css/all.css" 55 | ], 56 | "scripts": [], 57 | "extractLicenses": false, 58 | "sourceMap": true, 59 | "optimization": false, 60 | "namedChunks": true, 61 | "browser": "projects/ng-sortgrid-demo/src/main.ts" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "fileReplacements": [ 66 | { 67 | "replace": "projects/ng-sortgrid-demo/src/environments/environment.ts", 68 | "with": "projects/ng-sortgrid-demo/src/environments/environment.prod.ts" 69 | } 70 | ], 71 | "optimization": true, 72 | "outputHashing": "all", 73 | "sourceMap": false, 74 | "namedChunks": false, 75 | "extractLicenses": true, 76 | "budgets": [ 77 | { 78 | "type": "initial", 79 | "maximumWarning": "2mb", 80 | "maximumError": "5mb" 81 | }, 82 | { 83 | "type": "anyComponentStyle", 84 | "maximumWarning": "6kb" 85 | } 86 | ] 87 | } 88 | }, 89 | "defaultConfiguration": "" 90 | }, 91 | "serve": { 92 | "builder": "@angular-devkit/build-angular:dev-server", 93 | "options": { 94 | "buildTarget": "ng-sortgrid-demo:build" 95 | }, 96 | "configurations": { 97 | "production": { 98 | "buildTarget": "ng-sortgrid-demo:build:production" 99 | } 100 | } 101 | }, 102 | "extract-i18n": { 103 | "builder": "@angular-devkit/build-angular:extract-i18n", 104 | "options": { 105 | "buildTarget": "ng-sortgrid-demo:build" 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /jest.base.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-preset-angular', 3 | setupFilesAfterEnv: ['/../../setupJest.ts'], 4 | globalSetup: 'jest-preset-angular/global-setup', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-sortgrid", 3 | "version": "18.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "bump-version": "rjp package.json version $VERSION", 8 | "copy:readme": "copyfiles ./README.md ./projects/ng-sortgrid", 9 | "copy:styles": "copyfiles -f ./projects/ng-sortgrid/src/lib/ngsg.css ./dist/ng-sortgrid/styles", 10 | "format:check": "prettier --list-different 'projects/**/*.ts'", 11 | "format:write": "prettier --write 'projects/**/*.ts'", 12 | "import-conductor": "import-conductor --source 'projects/**/*.ts'", 13 | "test": "npm run test:lib", 14 | "test:coverage": "ng test --code-coverage --watch=false", 15 | "test:lib": "jest --config ./projects/ng-sortgrid/jest.config.js", 16 | "test:lib:coverage": "jest --config ./projects/ng-sortgrid/jest.config.js --coverage", 17 | "report-coverage:lib": "cat ./coverage/ng-sortgrid/lcov.info | codecov", 18 | "build": "npm run build:lib && npm run build:demo", 19 | "build:lib": "npm run copy:readme && ng build ng-sortgrid && npm run copy:styles", 20 | "build:demo": "ng build ng-sortgrid-demo --base-href='https://kreuzerk.github.io/ng-sortgrid/'", 21 | "publish": "npm run publish:lib", 22 | "publish:demo": "npx angular-cli-ghpages --dir=./dist/ng-sortgrid-demo", 23 | "publish:lib": "cd dist/ng-sortgrid && npx semantic-release", 24 | "lint": "eslint projects/**/*.ts" 25 | }, 26 | "dependencies": { 27 | "@angular/animations": "^19.0.4", 28 | "@angular/cli": "^19.0.4", 29 | "@angular/common": "^19.0.4", 30 | "@angular/compiler": "^19.0.4", 31 | "@angular/core": "^19.0.4", 32 | "@angular/forms": "^19.0.4", 33 | "@angular/platform-browser": "^19.0.4", 34 | "@angular/platform-browser-dynamic": "^19.0.4", 35 | "@angular/router": "^19.0.4", 36 | "rxjs": "~7.8.1", 37 | "tslib": "^2.6.2", 38 | "zone.js": "~0.15.0" 39 | }, 40 | "devDependencies": { 41 | "@angular-devkit/build-angular": "^19.0.4", 42 | "@angular-eslint/builder": "^18.0.1", 43 | "@angular-eslint/eslint-plugin": "^18.0.1", 44 | "@angular-eslint/eslint-plugin-template": "^18.0.1", 45 | "@angular-eslint/schematics": "^18.0.1", 46 | "@angular-eslint/template-parser": "^18.0.1", 47 | "@angular/compiler-cli": "^19.0.4", 48 | "@angular/language-service": "^19.0.4", 49 | "@fortawesome/fontawesome-free": "^6.5.1", 50 | "@semantic-release/changelog": "^6.0.3", 51 | "@semantic-release/exec": "^6.0.3", 52 | "@semantic-release/git": "^10.0.1", 53 | "@semantic-release/npm": "^12.0.1", 54 | "@types/jest": "^29.5.12", 55 | "@types/node": "^20.11.27", 56 | "@typescript-eslint/eslint-plugin": "^7.2.0", 57 | "@typescript-eslint/parser": "^7.2.0", 58 | "codecov": "^3.8.2", 59 | "codelyzer": "^6.0.2", 60 | "copyfiles": "^2.4.1", 61 | "eslint": "^8.57.0", 62 | "husky": "^4.2.5", 63 | "import-conductor": "^2.6.1", 64 | "jest": "^29.7.0", 65 | "jest-preset-angular": "^14.4.2", 66 | "lint-staged": "^15.2.7", 67 | "ng-packagr": "^19.0.1", 68 | "prettier": "^3.2.5", 69 | "protractor": "~7.0.0", 70 | "replace-json-property": "^1.9.0", 71 | "ts-jest": "^29.1.2", 72 | "ts-node": "~10.9.2", 73 | "tslint": "~6.1.0", 74 | "typescript": "^5.6.3" 75 | }, 76 | "private": true, 77 | "license": "MIT", 78 | "husky": { 79 | "hooks": { 80 | "pre-commit": "lint-staged" 81 | } 82 | }, 83 | "lint-staged": { 84 | "{src,__mocks__,bin}/**/*.ts": [ 85 | "prettier --write", 86 | "git add" 87 | ] 88 | }, 89 | "repository": { 90 | "type": "git", 91 | "url": "https://github.com/kreuzerk/ng-sortgrid.git" 92 | }, 93 | "bugs": { 94 | "url": "https://github.com/kreuzerk/ng-sortgrid/issues" 95 | }, 96 | "homepage": "https://github.com/kreuzerk/ng-sortgrid#readme" 97 | } 98 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../jest.base.config'); 2 | module.exports = { 3 | ...baseConfig, 4 | }; 5 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding-bottom: 20px; 3 | } 4 | 5 | .chaptor-separator { 6 | margin-top: 50px; 7 | margin-bottom: 30px; 8 | border: none; 9 | height: 2px; 10 | /* Set the hr color */ 11 | color: #333; /* old IE */ 12 | background-color: #333; /* Modern Browsers */ 13 | } 14 | 15 | /* 16 | Custom styles 17 | */ 18 | .container-fluid { 19 | padding: 20px; 20 | } 21 | 22 | .example-list { 23 | list-style-type: none; 24 | padding: 0; 25 | } 26 | 27 | .example-list li { 28 | display: table-cell; 29 | padding: 4px; 30 | } 31 | 32 | .example-container { 33 | display: flex; 34 | flex-wrap: wrap; 35 | width: 100%; 36 | justify-content: center; 37 | } 38 | 39 | .btn { 40 | background: #c30230; 41 | font-size: 20px; 42 | border: 1px solid white; 43 | } 44 | 45 | .btn:hover { 46 | color: #c30230; 47 | background: white; 48 | border: 1px solid #c30230; 49 | } 50 | 51 | .border-primary { 52 | border-color: #c30230 !important; 53 | } 54 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'], 7 | encapsulation: ViewEncapsulation.None, 8 | standalone: false 9 | }) 10 | export class AppComponent { 11 | public items = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 12 | 13 | public gridOneSorted(sortedItems: string): void { 14 | console.log('Grid one sorted', sortedItems); 15 | } 16 | 17 | public gridTwoSorted(sortedItems: string): void { 18 | console.log('Grid two sorted', sortedItems); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {AppRoutingModule} from './app.routing.module'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [BrowserModule, AppRoutingModule], 10 | bootstrap: [AppComponent] 11 | }) 12 | export class AppModule { 13 | } 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/app.routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | 4 | 5 | @NgModule({ 6 | imports: [RouterModule.forRoot([ 7 | {path: '', loadChildren: () => import('./introduction/introduction.module').then(m => m.IntroductionModule)}, 8 | {path: 'scrolling', loadChildren: () => import('./scrolling/scrolling.module').then(m => m.ScrollingModule)} 9 | ])], 10 | exports: [RouterModule] 11 | }) 12 | export class AppRoutingModule { 13 | } 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/async-pipe/async-pipe-memory.component.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 40px; 3 | height: 40px; 4 | 5 | position: relative; 6 | margin: 100px auto; 7 | } 8 | 9 | .double-bounce1, .double-bounce2 { 10 | width: 100%; 11 | height: 100%; 12 | border-radius: 50%; 13 | background-color: #333; 14 | opacity: 0.6; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | 19 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 20 | animation: sk-bounce 2.0s infinite ease-in-out; 21 | } 22 | 23 | .double-bounce2 { 24 | -webkit-animation-delay: -1.0s; 25 | animation-delay: -1.0s; 26 | } 27 | 28 | @-webkit-keyframes sk-bounce { 29 | 0%, 100% { -webkit-transform: scale(0.0) } 30 | 50% { -webkit-transform: scale(1.0) } 31 | } 32 | 33 | @keyframes sk-bounce { 34 | 0%, 100% { 35 | transform: scale(0.0); 36 | -webkit-transform: scale(0.0); 37 | } 50% { 38 | transform: scale(1.0); 39 | -webkit-transform: scale(1.0); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/async-pipe/async-pipe-memory.component.html: -------------------------------------------------------------------------------- 1 |
4. Load items and use them with the async pipe
2 | 3 |
4 |
5 |

Previous sort order

6 |

{{ previousSortOrder }}

7 |
8 |
9 |

Current sort order

10 |

{{ currentSortOrder }}

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/async-pipe/async-pipe-memory.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Observable, of} from 'rxjs'; 3 | import {delay, tap} from 'rxjs/operators'; 4 | 5 | import {NgsgOrderChange} from '../../../../../../ng-sortgrid/src/lib/shared/ngsg-order-change.model'; 6 | 7 | @Component({ 8 | selector: 'ngsg-demo-async', 9 | templateUrl: './async-pipe-memory.component.html', 10 | styleUrls: ['./async-pipe-memory.component.css'], 11 | standalone: false 12 | }) 13 | export class AsyncPipeMemoryComponent implements OnInit { 14 | 15 | item$: Observable; 16 | loading = false; 17 | public currentSortOrder: number[]; 18 | public previousSortOrder: number[]; 19 | 20 | ngOnInit(): void { 21 | this.previousSortOrder = []; 22 | this.currentSortOrder = []; 23 | } 24 | 25 | public loadItems(): void { 26 | this.loading = true; 27 | this.item$ = of([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).pipe( 28 | delay(1500), 29 | tap(() => this.loading = false) 30 | ); 31 | } 32 | 33 | public applyOrder(orderChange: NgsgOrderChange): void { 34 | this.currentSortOrder = orderChange.currentOrder; 35 | this.previousSortOrder = orderChange.previousOrder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/drag-handle/drag-handle.component.css: -------------------------------------------------------------------------------- 1 | .example-box { 2 | width: 200px; 3 | height: 200px; 4 | padding: 10px; 5 | box-sizing: border-box; 6 | border: solid 1px #ccc; 7 | color: rgba(0, 0, 0, 0.87); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | text-align: center; 12 | background: #fff; 13 | border-radius: 4px; 14 | position: relative; 15 | z-index: 1; 16 | transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1); 17 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 18 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 19 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 20 | } 21 | 22 | .example-handle { 23 | position: absolute; 24 | top: 10px; 25 | right: 10px; 26 | color: #ccc; 27 | cursor: move; 28 | width: 24px; 29 | height: 24px; 30 | } 31 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/drag-handle/drag-handle.component.html: -------------------------------------------------------------------------------- 1 |
1. Drag the items around using handle
2 | 3 |
4 |
8 | 9 |

10 | {{ item }} 11 |

12 | 13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/drag-handle/drag-handle.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-drag-handle', 5 | templateUrl: 'drag-handle.component.html', 6 | styleUrls: ['./drag-handle.component.css'], 7 | standalone: false 8 | }) 9 | export class DragHandleComponent { 10 | 11 | public items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/getting-started/getting-started-memory.component.html: -------------------------------------------------------------------------------- 1 |
3. Drag the items around - hold CMD or Control and click on an item to select multiple 2 | files
3 |
4 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/getting-started/getting-started-memory.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-memory', 5 | templateUrl: 'getting-started-memory.component.html', 6 | standalone: false 7 | }) 8 | export class GettingStartedMemoryComponent { 9 | 10 | public items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/groups/groups-memory.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 10 | {{ item }} 11 | 12 |
13 |
14 |
15 |
16 | 22 | {{ item }} 23 | 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/groups/groups-memory.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-groups-memory', 5 | templateUrl: 'groups-memory.component.html', 6 | standalone: false 7 | }) 8 | export class GroupsMemoryComponent { 9 | 10 | public items = [1, 2, 3, 4]; 11 | } 12 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/react-on-changes/react-on-changes-memory.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Previous sort order

4 |

{{ previousSortOrder }}

5 |
6 |
7 |

Current sort order

8 |

{{ currentSortOrder }}

9 |
10 |
11 |
12 | 19 | {{ item }} 20 | 21 |
22 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/examples/react-on-changes/react-on-changes-memory.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | import {NgsgOrderChange} from '../../../../../../ng-sortgrid/src/lib/shared/ngsg-order-change.model'; 4 | 5 | @Component({ 6 | selector: 'ngsg-demo-react-on-changes-memory', 7 | templateUrl: 'react-on-changes-memory.component.html', 8 | standalone: false 9 | }) 10 | export class ReactOnChangesMemoryComponent implements OnInit { 11 | 12 | public items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 13 | public currentSortOrder: number[]; 14 | public previousSortOrder: number[]; 15 | 16 | ngOnInit(): void { 17 | this.currentSortOrder = [...this.items]; 18 | this.previousSortOrder = []; 19 | } 20 | 21 | public applyOrder(orderChange: NgsgOrderChange): void { 22 | this.currentSortOrder = orderChange.currentOrder; 23 | this.previousSortOrder = orderChange.previousOrder; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/introduction.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

1. Getting started

6 | 7 | 8 | 9 | 10 |
11 | 12 |

2. React on changes

13 |

In most cases you are interested in the new sort order. Often you want to store them in local storage or even send them 14 | to the backend. To do so the following two steps are needed in addition to the "Getting started" step.

15 | 16 | 17 | 18 | 19 |
20 |

3. Group sortgrids

21 |

In case you have more than one sortgriditem on the page you need to group the sortgriditems to avoid dropping drags from 22 | one group in another group.

23 | 24 | 25 | 26 |
27 |

4. Use the async pipe

28 | 29 | 30 | 31 |
32 |

5. Scrolling

33 |

34 | The scrolling demo is in a separate page because we need more items and a sticky navheader. 35 |

36 | 37 | 38 | 39 |
40 | 41 |

6. Customizing the drag area using a handle

42 | 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/introduction.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { IntroductionComponent } from './introduction.component'; 4 | import {IntroductionModule} from './introduction.module'; 5 | 6 | describe('IntroductionComponent', () => { 7 | let component: IntroductionComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [IntroductionModule] 13 | }) 14 | .compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(IntroductionComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/introduction.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-introduction', 5 | templateUrl: './introduction.component.html', 6 | standalone: false 7 | }) 8 | export class IntroductionComponent { 9 | } 10 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/introduction.module.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {NgsgModule} from '../../../../ng-sortgrid/src/lib/ngsg.module'; 5 | import {SharedModule} from '../shared/shared.module'; 6 | 7 | import {AsyncPipeMemoryComponent} from './examples/async-pipe/async-pipe-memory.component'; 8 | import { DragHandleComponent } from './examples/drag-handle/drag-handle.component'; 9 | import {GettingStartedMemoryComponent} from './examples/getting-started/getting-started-memory.component'; 10 | import {GroupsMemoryComponent} from './examples/groups/groups-memory.component'; 11 | import {ReactOnChangesMemoryComponent} from './examples/react-on-changes/react-on-changes-memory.component'; 12 | import {IntroductionComponent} from './introduction.component'; 13 | import {IntroductionRoutingModule} from './introduction.routing.module'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | IntroductionComponent, 18 | GettingStartedMemoryComponent, 19 | ReactOnChangesMemoryComponent, 20 | GroupsMemoryComponent, 21 | AsyncPipeMemoryComponent, 22 | DragHandleComponent 23 | ], 24 | imports: [ 25 | CommonModule, 26 | IntroductionRoutingModule, 27 | NgsgModule, 28 | SharedModule 29 | ] 30 | }) 31 | export class IntroductionModule { 32 | } 33 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/introduction/introduction.routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | 4 | import {IntroductionComponent} from './introduction.component'; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forChild([ 8 | {path: '', component: IntroductionComponent} 9 | ])], 10 | exports: [RouterModule] 11 | }) 12 | export class IntroductionRoutingModule { 13 | } 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/scrolling/scrolling.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Scrolling

4 | 5 | 6 |
7 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/scrolling/scrolling.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ScrollingComponent } from './scrolling.component'; 4 | 5 | describe('ScrollingComponent', () => { 6 | let component: ScrollingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ScrollingComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ScrollingComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/scrolling/scrolling.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-scrolling', 5 | templateUrl: './scrolling.component.html', 6 | standalone: false 7 | }) 8 | export class ScrollingComponent { 9 | 10 | height = 350; 11 | public items = Array.from(Array(50).keys()); 12 | } 13 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/scrolling/scrolling.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import {NgsgModule} from '../../../../ng-sortgrid/src/lib/ngsg.module'; 5 | import {SharedModule} from '../shared/shared.module'; 6 | 7 | import { ScrollingComponent } from './scrolling.component'; 8 | import {ScrollingRoutingModule} from './scrolling.routing.module'; 9 | 10 | @NgModule({ 11 | declarations: [ScrollingComponent], 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | ScrollingRoutingModule, 16 | NgsgModule 17 | ] 18 | }) 19 | export class ScrollingModule { } 20 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/scrolling/scrolling.routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule} from '@angular/router'; 3 | 4 | import {ScrollingComponent} from './scrolling.component'; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forChild([ 8 | {path: '', component: ScrollingComponent} 9 | ])] 10 | }) 11 | export class ScrollingRoutingModule { 12 | } 13 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/card/card.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 200px; 3 | height: 200px; 4 | border: solid 1px #ccc; 5 | font-size: 30pt; 6 | font-weight: bold; 7 | color: rgba(0, 0, 0, 0.87); 8 | cursor: move; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | text-align: center; 13 | background: #fff; 14 | border-radius: 4px; 15 | position: relative; 16 | z-index: 1; 17 | transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1); 18 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 19 | 0 2px 2px 0 rgba(0, 0, 0, 0.14), 20 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 21 | } 22 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/card/card.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{ item }} 4 |

5 |
6 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-card', 5 | templateUrl: './card.component.html', 6 | styleUrls: ['./card.component.css'], 7 | standalone: false 8 | }) 9 | export class CardComponent { 10 | 11 | @Input() item: number; 12 | } 13 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/header/header.component.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #c30230; 3 | } 4 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Draggable sort grid with multiselction

6 |

Use the ngSortgridItem directive to turn your lists into a grid where single 7 | or even multiple files can be sorted via drag & drop.

8 |
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-header', 5 | templateUrl: 'header.component.html', 6 | styleUrls: ['./header.component.css'], 7 | standalone: false 8 | }) 9 | export class HeaderComponent { 10 | } 11 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/nav/nav.component.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background: white; 3 | border-bottom: 1px solid #c30230; 4 | } 5 | 6 | .logo { 7 | margin-top: 30px; 8 | width: 250px; 9 | height: 80px; 10 | } 11 | 12 | .icon { 13 | color: #c30230; 14 | font-size: 40px; 15 | } 16 | 17 | .subtitle { 18 | color: darkgray; 19 | display: block; 20 | margin-left: 50px; 21 | font-style: italic; 22 | } 23 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/nav/nav.component.html: -------------------------------------------------------------------------------- 1 | 29 | 30 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-nav', 5 | templateUrl: './nav.component.html', 6 | styleUrls: ['./nav.component.css'], 7 | standalone: false 8 | }) 9 | export class NavComponent { 10 | @Input() fixed = false; 11 | @Input() height = '140px'; 12 | @Input() subtitle; 13 | } 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import {CardComponent} from './card/card.component'; 5 | import {HeaderComponent} from './header/header.component'; 6 | import {NavComponent} from './nav/nav.component'; 7 | import {StepComponent} from './step/step.component'; 8 | 9 | @NgModule({ 10 | declarations: [StepComponent, HeaderComponent, NavComponent, CardComponent], 11 | exports: [StepComponent, HeaderComponent, NavComponent, CardComponent], 12 | imports: [ 13 | CommonModule 14 | ] 15 | }) 16 | export class SharedModule { } 17 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/step/step.component.css: -------------------------------------------------------------------------------- 1 | .step-title { 2 | margin-bottom: 20px; 3 | } 4 | 5 | .step-image { 6 | max-width: 100%; 7 | padding-bottom: 30px; 8 | } 9 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/step/step.component.html: -------------------------------------------------------------------------------- 1 |
{{title}}
2 | 3 |
4 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/app/shared/step/step.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngsg-demo-step', 5 | templateUrl: 'step.component.html', 6 | styleUrls: ['step.component.css'], 7 | standalone: false 8 | }) 9 | export class StepComponent { 10 | 11 | @Input() title: string; 12 | @Input() image: string; 13 | } 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/grid-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/grid-demo.gif -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs1.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs2.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs3.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs4.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs5.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs6.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/gs7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/gs7.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/ng-sortgrid-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/ng-sortgrid-logo.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/scrolling-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/scrolling-code.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/assets/scrolling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/assets/scrolling.png -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/ng-sortgrid/4c18b0579183a686441f8e8482a76dc9598aa8fa/projects/ng-sortgrid-demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgSortgridDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/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() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "projects/ng-sortgrid-demo/src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jest", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/polyfills.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-sortgrid-demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | false, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | false, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../jest.base.config'); 2 | module.exports = { 3 | ...baseConfig, 4 | }; 5 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-sortgrid", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ng-sortgrid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-sortgrid", 3 | "version": "19.0.0", 4 | "license": "MIT", 5 | "peerDependencies": { 6 | "@angular/common": "^19.0.0", 7 | "@angular/core": "^19.0.0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kreuzerk/ng-sortgrid.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/kreuzerk/ng-sortgrid/issues" 15 | }, 16 | "homepage": "https://github.com/kreuzerk/ng-sortgrid#readme" 17 | } 18 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/class/ngsg-class.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {NgsgClassService} from './ngsg-class.service'; 2 | 3 | describe('NgsgClassService', () => { 4 | 5 | let sut: NgsgClassService; 6 | 7 | beforeEach(() => sut = new NgsgClassService()); 8 | 9 | it('should add the placeholder class', () => { 10 | const addClassSpy = jest.fn(); 11 | const element = {classList: {add: addClassSpy}} as any; 12 | sut.addPlaceHolderClass(element); 13 | expect(addClassSpy).toHaveBeenCalledWith('ng-sg-placeholder'); 14 | }); 15 | 16 | it('should remove the placeholder class', () => { 17 | const removeClassSpy = jest.fn(); 18 | const element = {classList: {remove: removeClassSpy}} as any; 19 | sut.removePlaceHolderClass(element); 20 | expect(removeClassSpy).toHaveBeenCalledWith('ng-sg-placeholder'); 21 | }); 22 | 23 | it('should add the dropped class', () => { 24 | const addClassSpy = jest.fn(); 25 | const element = {classList: {add: addClassSpy}} as any; 26 | sut.addDroppedClass(element); 27 | expect(addClassSpy).toHaveBeenCalledWith('ng-sg-dropped'); 28 | }); 29 | 30 | it('should remove the placeholder class', () => { 31 | const removeClassSpy = jest.fn(); 32 | const element = {classList: {remove: removeClassSpy}} as any; 33 | sut.removeDroppedClass(element); 34 | expect(removeClassSpy).toHaveBeenCalledWith('ng-sg-dropped'); 35 | }); 36 | 37 | it('should add the dropped class', () => { 38 | const addClassSpy = jest.fn(); 39 | const element = {classList: {add: addClassSpy}} as any; 40 | sut.addSelectedClass(element); 41 | expect(addClassSpy).toHaveBeenCalledWith('ng-sg-selected'); 42 | }); 43 | 44 | it('should remove the placeholder class', () => { 45 | const removeClassSpy = jest.fn(); 46 | const element = {classList: {remove: removeClassSpy}} as any; 47 | sut.removeSelectedClass(element); 48 | expect(removeClassSpy).toHaveBeenCalledWith('ng-sg-selected'); 49 | }); 50 | 51 | it('should add the active class', () => { 52 | const addClassSpy = jest.fn(); 53 | const element = {classList: {add: addClassSpy}} as any; 54 | sut.addActiveClass(element); 55 | expect(addClassSpy).toHaveBeenCalledWith('ng-sg-active'); 56 | }); 57 | 58 | it('should remove the active class', () => { 59 | const removeClassSpy = jest.fn(); 60 | const element = {classList: {remove: removeClassSpy}} as any; 61 | sut.removeActiveClass(element); 62 | expect(removeClassSpy).toHaveBeenCalledWith('ng-sg-active'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/class/ngsg-class.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class NgsgClassService { 7 | private SELECTED_DEFAULT_CLASS = 'ng-sg-selected'; 8 | private PLACEHOLDER_DEFAULT_CLASS = 'ng-sg-placeholder'; 9 | private DROPPED_DEFAULT_CLASS = 'ng-sg-dropped'; 10 | private ACTIVE_DEFAULT_CLASS = 'ng-sg-active'; 11 | 12 | public addPlaceHolderClass(element: Element): void { 13 | element.classList.add(this.PLACEHOLDER_DEFAULT_CLASS); 14 | } 15 | 16 | public removePlaceHolderClass(element: Element): void { 17 | element.classList.remove(this.PLACEHOLDER_DEFAULT_CLASS); 18 | } 19 | 20 | public addDroppedClass(element: Element): void { 21 | element.classList.add(this.DROPPED_DEFAULT_CLASS); 22 | } 23 | 24 | public removeDroppedClass(element: Element): void { 25 | element.classList.remove(this.DROPPED_DEFAULT_CLASS); 26 | } 27 | 28 | public addSelectedClass(element: Element): void { 29 | element.classList.add(this.SELECTED_DEFAULT_CLASS); 30 | } 31 | 32 | public removeSelectedClass(element: Element): void { 33 | element.classList.remove(this.SELECTED_DEFAULT_CLASS); 34 | } 35 | 36 | public addActiveClass(element: Element): void { 37 | element.classList.add(this.ACTIVE_DEFAULT_CLASS); 38 | } 39 | 40 | public removeActiveClass(element: Element): void { 41 | element.classList.remove(this.ACTIVE_DEFAULT_CLASS); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/element/ngsg-elements.helper.spec.ts: -------------------------------------------------------------------------------- 1 | import {NgsgElementsHelper} from './ngsg-elements.helper'; 2 | 3 | describe('NgsgElementsHelper', () => { 4 | 5 | it('must return the correct index of the element', () => { 6 | const elementOne = {name: 'child one'} as any; 7 | const elementTwo = {name: 'child two'} as any; 8 | const elementThree = {name: 'chil three'} as any; 9 | const elementFour = {name: 'chil four'} as any; 10 | 11 | const children = [elementOne, elementTwo, elementThree, elementFour]; 12 | elementOne.parentNode = {children}; 13 | elementTwo.parentNode = {children}; 14 | elementThree.parentNode = {children}; 15 | elementFour.parentNode = {children}; 16 | 17 | const index = NgsgElementsHelper.findIndex(elementThree); 18 | expect(index).toBe(2); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/element/ngsg-elements.helper.ts: -------------------------------------------------------------------------------- 1 | export class NgsgElementsHelper { 2 | 3 | public static findIndex(element: Element): number { 4 | const allElements = element.parentNode.children; 5 | return Array.prototype.indexOf.call(allElements, element); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/scroll/scroll-helper.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {ScrollHelperService} from './scroll-helper.service'; 2 | 3 | describe('Scroll helper', () => { 4 | 5 | let sut: ScrollHelperService; 6 | const documentMock = { 7 | defaultView: { 8 | innerHeight: 0, 9 | innerWidth: 0, 10 | scrollBy: jest.fn() 11 | } as any 12 | }; 13 | let scrollSpy: any; 14 | 15 | beforeEach(() => { 16 | sut = new ScrollHelperService(documentMock); 17 | scrollSpy = jest.spyOn(documentMock.defaultView, 'scrollBy'); 18 | }); 19 | 20 | describe('Top scroll', () => { 21 | 22 | it(`should scroll to the top with the default scroll speed when we drag over 23 | the top viewport + scroll buffer`, () => { 24 | documentMock.defaultView.scrollY = 0; 25 | const event = { 26 | pageY: 40 27 | }; 28 | sut.scrollIfNecessary(event); 29 | expect(scrollSpy).toHaveBeenCalledWith({top: -50, behavior: 'smooth'}); 30 | }); 31 | 32 | it('should scroll to the top with the default scroll speed when we drag over the top scroll position', () => { 33 | documentMock.defaultView.scrollY = 0; 34 | const event = { 35 | pageY: 110 36 | }; 37 | sut.scrollIfNecessary(event, {top: 140}); 38 | expect(scrollSpy).toHaveBeenCalledWith({top: -50, behavior: 'smooth'}); 39 | }); 40 | 41 | it('should scroll to the top with the custom scroll speed when we drag over the top viewport', () => { 42 | documentMock.defaultView.scrollY = 0; 43 | const event = { 44 | pageY: 40 45 | }; 46 | const scrollSpeed = 100; 47 | sut.scrollIfNecessary(event, {}, scrollSpeed); 48 | expect(scrollSpy).toHaveBeenCalledWith({top: -scrollSpeed, behavior: 'smooth'}); 49 | }); 50 | 51 | }); 52 | 53 | describe('Bottom scroll', () => { 54 | 55 | it(`should scroll to the bottom with the default scroll speed when we drag 56 | over the bottom viewport - scroll buffer`, () => { 57 | documentMock.defaultView.scrollY = 0; 58 | documentMock.defaultView.innerHeight = 100; 59 | const event = { 60 | pageY: 80 61 | }; 62 | sut.scrollIfNecessary(event); 63 | expect(scrollSpy).toHaveBeenCalledWith({top: 50, behavior: 'smooth'}); 64 | }); 65 | 66 | it('should scroll to the bottom with the default scroll speed when we drag over the bottom scroll position', () => { 67 | documentMock.defaultView.scrollY = 0; 68 | const event = { 69 | pageY: 141 70 | }; 71 | sut.scrollIfNecessary(event, {bottom: 140}); 72 | expect(scrollSpy).toHaveBeenCalledWith({top: 50, behavior: 'smooth'}); 73 | }); 74 | 75 | it('should scroll to the top with the custom scroll speed when we drag over the top viewport', () => { 76 | documentMock.defaultView.scrollY = 0; 77 | const event = { 78 | pageY: 110 79 | }; 80 | const scrollSpeed = 100; 81 | sut.scrollIfNecessary(event, {}, scrollSpeed); 82 | expect(scrollSpy).toHaveBeenCalledWith({top: scrollSpeed, behavior: 'smooth'}); 83 | }); 84 | 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/helpers/scroll/scroll-helper.service.ts: -------------------------------------------------------------------------------- 1 | import {DOCUMENT} from '@angular/common'; 2 | import {Inject, Injectable} from '@angular/core'; 3 | 4 | export interface ScrollPoints { 5 | top?: number; 6 | bottom?: number; 7 | } 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class ScrollHelperService { 13 | 14 | private window: WindowProxy; 15 | private DEFAULT_SCROLLSPEED = 50; 16 | private SCROLL_BUFFER = 50; 17 | 18 | constructor(@Inject(DOCUMENT) private document) { 19 | this.window = document.defaultView; 20 | } 21 | 22 | public scrollIfNecessary(event: any, scrollPoints: ScrollPoints = {}, scrollSpeed?: number): void { 23 | const currentPosition = event.pageY - this.window.scrollY; 24 | 25 | if (this.isTopScrollNeeded(currentPosition, scrollPoints.top)) { 26 | this.window.scrollBy({top: -scrollSpeed || -this.DEFAULT_SCROLLSPEED, behavior: 'smooth'}); 27 | return; 28 | } 29 | 30 | if (this.isBottomScrollNeeded(currentPosition, scrollPoints.bottom)) { 31 | this.window.scrollBy({top: scrollSpeed || this.DEFAULT_SCROLLSPEED, behavior: 'smooth'}); 32 | } 33 | } 34 | 35 | private isTopScrollNeeded(currentPosition: number, scrollPointTop: number): boolean { 36 | return scrollPointTop ? currentPosition < scrollPointTop : 37 | currentPosition < this.SCROLL_BUFFER; 38 | } 39 | 40 | private isBottomScrollNeeded(currentPosition: number, scrollPointBottom: number): boolean { 41 | return scrollPointBottom ? currentPosition > scrollPointBottom : 42 | currentPosition > this.window.innerHeight - this.SCROLL_BUFFER; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/mutliselect/ngsg-selection.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgsgElementsHelper } from '../helpers/element/ngsg-elements.helper'; 2 | 3 | import { NgsgSelectionService } from './ngsg-selection.service'; 4 | 5 | describe('NgsgSelectionService', () => { 6 | const ngsgClassService = { 7 | addSelectedClass: jest.fn(), 8 | addActiveClass: jest.fn(), 9 | removeSelectedClass: jest.fn(), 10 | removeActiveClass: jest.fn(), 11 | } as any; 12 | 13 | const ngsgStore = { 14 | addSelectedItem: jest.fn(), 15 | getSelectedItems: jest.fn(), 16 | hasSelectedItems: jest.fn(), 17 | removeSelectedItem: jest.fn(), 18 | resetSelectedItems: jest.fn(), 19 | } as any; 20 | 21 | let sut: NgsgSelectionService; 22 | 23 | beforeEach(() => { 24 | sut = new NgsgSelectionService(ngsgClassService, ngsgStore); 25 | }); 26 | 27 | afterEach(() => { 28 | const keyupEvent = new KeyboardEvent('keyun', {}); 29 | window.dispatchEvent(keyupEvent); 30 | }); 31 | 32 | describe('selectElementIfNoSelection', () => { 33 | it('should call hasSelectedItems with the group', () => { 34 | ngsgStore.hasSelectedItems = jest.fn(); 35 | ngsgStore.hasSelectedItems.mockReturnValue(true); 36 | const dragedElement = 'Cool element' as any; 37 | const group = 'herogroup'; 38 | 39 | sut.selectElementIfNoSelection(group, dragedElement); 40 | expect(ngsgStore.hasSelectedItems).toHaveBeenCalledWith(group); 41 | }); 42 | 43 | it('should not addSelectedItem to the store if there are allready items selected', () => { 44 | ngsgStore.hasSelectedItems = () => true; 45 | const dragedElement = 'Cool element' as any; 46 | const group = 'herogroup'; 47 | 48 | sut.selectElementIfNoSelection(group, dragedElement); 49 | expect(ngsgStore.addSelectedItem).not.toHaveBeenCalled(); 50 | }); 51 | 52 | it('should addSelectedItem to the store if no item is yet selected', () => { 53 | ngsgStore.hasSelectedItems = () => false; 54 | ngsgStore.addSelectedItem = jest.fn(); 55 | const dragedElement = 'Cool element' as any; 56 | const group = 'herogroup'; 57 | const originalIndex = 2; 58 | 59 | const findIndexSpy = jest.fn(); 60 | findIndexSpy.mockReturnValue(originalIndex); 61 | NgsgElementsHelper.findIndex = findIndexSpy; 62 | 63 | sut.selectElementIfNoSelection(group, dragedElement); 64 | 65 | expect(findIndexSpy).toHaveBeenCalledWith(dragedElement); 66 | expect(ngsgStore.addSelectedItem).toHaveBeenCalledWith(group, { 67 | node: dragedElement, 68 | originalIndex, 69 | }); 70 | }); 71 | 72 | describe('Selection change', () => { 73 | it('should add the selectedItem if the Meta key is pressed and the item is clicked', () => { 74 | const event = new KeyboardEvent('keydown', { 75 | key: 'Meta', 76 | }); 77 | const group = 'groupOne'; 78 | const item = 'Some element' as any; 79 | const selected = true; 80 | const index = 2; 81 | NgsgElementsHelper.findIndex = () => index; 82 | 83 | window.dispatchEvent(event); 84 | sut.updateSelectedDragItem(group, item, selected); 85 | 86 | expect(ngsgStore.addSelectedItem).toHaveBeenCalledWith(group, { node: item, originalIndex: index }); 87 | }); 88 | 89 | it('should remove the selectedItem if the Meta key is pressed and the selected item is clicked', () => { 90 | const event = new KeyboardEvent('keydown', { 91 | key: 'Meta', 92 | }); 93 | const group = 'groupOne'; 94 | const item = 'Some element' as any; 95 | const selected = false; 96 | const index = 2; 97 | NgsgElementsHelper.findIndex = () => index; 98 | 99 | window.dispatchEvent(event); 100 | sut.updateSelectedDragItem(group, item, selected); 101 | 102 | expect(ngsgStore.removeSelectedItem).toHaveBeenCalledWith(group, item); 103 | }); 104 | 105 | it(`should remove the selected class from the selected item if the Meta key is pressed 106 | and the selected item is clicked`, () => { 107 | const event = new KeyboardEvent('keydown', { 108 | key: 'Meta', 109 | }); 110 | const group = 'groupOne'; 111 | const item = 'Some element' as any; 112 | const selected = false; 113 | const index = 2; 114 | NgsgElementsHelper.findIndex = () => index; 115 | 116 | window.dispatchEvent(event); 117 | sut.updateSelectedDragItem(group, item, selected); 118 | 119 | expect(ngsgClassService.removeSelectedClass).toHaveBeenCalledWith(item); 120 | }); 121 | 122 | it(`should reset the selected items if we click on an item without holding the shift key`, () => { 123 | const event = new KeyboardEvent('keyup', { 124 | key: 'Meta', 125 | }); 126 | const itemOne = { node: 'Foo' } as any; 127 | const itemTwo = { node: 'Bar' } as any; 128 | const items = [itemOne, itemTwo]; 129 | const group = 'groupOne'; 130 | const item = 'Some element' as any; 131 | const selected = false; 132 | const index = 2; 133 | 134 | NgsgElementsHelper.findIndex = () => index; 135 | ngsgStore.getSelectedItems = () => items; 136 | window.dispatchEvent(event); 137 | sut.updateSelectedDragItem(group, item, selected); 138 | 139 | expect(ngsgClassService.removeSelectedClass).toHaveBeenCalledWith(itemOne.node); 140 | expect(ngsgClassService.removeSelectedClass).toHaveBeenCalledWith(itemTwo.node); 141 | expect(ngsgStore.resetSelectedItems).toHaveBeenCalledWith(group); 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/mutliselect/ngsg-selection.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {fromEvent, map, merge, Observable, Subject} from 'rxjs'; 3 | import {filter, mapTo, withLatestFrom} from 'rxjs/operators'; 4 | 5 | import {NgsgClassService} from '../helpers/class/ngsg-class.service'; 6 | import {NgsgElementsHelper} from '../helpers/element/ngsg-elements.helper'; 7 | import {NgsgStoreService} from '../store/ngsg-store.service'; 8 | 9 | enum ChangeAction { 10 | ADD, 11 | REMOVE 12 | } 13 | 14 | interface SelectionChange { 15 | key: string; 16 | item: Element; 17 | action: ChangeAction; 18 | } 19 | 20 | @Injectable({ 21 | providedIn: 'root' 22 | }) 23 | export class NgsgSelectionService { 24 | private COMMAND_KEY = 'Meta'; 25 | private CONTROL_KEY = 'Control'; 26 | 27 | private selectionChange$ = new Subject(); 28 | 29 | constructor(private classService: NgsgClassService, private ngsgStore: NgsgStoreService) { 30 | const selectionKeyPressed$ = this.selectionKeyPressed(); 31 | this.selectionChange$ 32 | .pipe(withLatestFrom(selectionKeyPressed$)) 33 | .subscribe(([selectionChange, selectionKeyPressed]) => { 34 | selectionKeyPressed 35 | ? this.handleSelectionChange(selectionChange) 36 | : this.resetSelectedItems(selectionChange.key); 37 | }); 38 | } 39 | 40 | private resetSelectedItems(group: string): void { 41 | this.ngsgStore.getSelectedItems(group).forEach(item => this.classService.removeSelectedClass(item.node)); 42 | this.ngsgStore.resetSelectedItems(group); 43 | } 44 | 45 | private handleSelectionChange(selectionChange: SelectionChange): void { 46 | if (selectionChange.action === ChangeAction.ADD) { 47 | this.classService.addSelectedClass(selectionChange.item); 48 | this.ngsgStore.addSelectedItem(selectionChange.key, { 49 | node: selectionChange.item, 50 | originalIndex: NgsgElementsHelper.findIndex(selectionChange.item) 51 | }); 52 | } 53 | if (selectionChange.action === ChangeAction.REMOVE) { 54 | this.classService.removeSelectedClass(selectionChange.item); 55 | this.ngsgStore.removeSelectedItem(selectionChange.key, selectionChange.item); 56 | } 57 | } 58 | 59 | private selectionKeyPressed(): Observable { 60 | const selectionKeyPressed = fromEvent(window, 'keydown').pipe( 61 | filter( 62 | (keyboardEvent: KeyboardEvent) => 63 | keyboardEvent.key === this.COMMAND_KEY || keyboardEvent.key === this.CONTROL_KEY 64 | ), 65 | map(() => true) 66 | ); 67 | const keyup = fromEvent(window, 'keyup').pipe(mapTo(false)); 68 | return merge(selectionKeyPressed, keyup); 69 | } 70 | 71 | public selectElementIfNoSelection(group: string, dragedElement: Element): void { 72 | if (this.ngsgStore.hasSelectedItems(group)) { 73 | return; 74 | } 75 | this.ngsgStore.addSelectedItem(group, { 76 | node: dragedElement, 77 | originalIndex: NgsgElementsHelper.findIndex(dragedElement) 78 | }); 79 | } 80 | 81 | public updateSelectedDragItem(key: string, item: Element, selected: boolean): void { 82 | this.selectionChange$.next({ 83 | key, 84 | item, 85 | action: selected ? ChangeAction.ADD : ChangeAction.REMOVE 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg-drag-handle.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgsgDragHandleDirective } from './ngsg-drag-handle.directive'; 2 | 3 | describe('NgsgDragHandleDirective', () => { 4 | let sut: NgsgDragHandleDirective; 5 | 6 | const elementRef = { nativeElement: {} } as any; 7 | 8 | beforeEach(() => { 9 | sut = new NgsgDragHandleDirective( 10 | elementRef 11 | ); 12 | }); 13 | 14 | it('should create an instance', () => { 15 | expect(sut).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg-drag-handle.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[ngSortgridDragHandle]', 5 | standalone: false 6 | }) 7 | export class NgsgDragHandleDirective { 8 | 9 | constructor(public el: ElementRef){} 10 | } 11 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg-item.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgsgElementsHelper } from './helpers/element/ngsg-elements.helper'; 2 | import { NgsgItemDirective } from './ngsg-item.directive'; 3 | import { NgsgEventsService } from './shared/ngsg-events.service'; 4 | import { NgsgOrderChange } from './shared/ngsg-order-change.model'; 5 | 6 | describe('NgsgItemDirective', () => { 7 | let sut: NgsgItemDirective; 8 | 9 | console.warn = jest.fn(); 10 | 11 | const elementRef = { nativeElement: {} } as any; 12 | const ngsgSortService = { 13 | initSort: jest.fn(), 14 | sort: jest.fn(), 15 | endSort: jest.fn(), 16 | } as any; 17 | const ngsgSelectionService = { 18 | selectElementIfNoSelection: jest.fn(), 19 | updateSelectedDragItem: jest.fn(), 20 | } as any; 21 | const ngsgReflectService = { reflectChanges: jest.fn() } as any; 22 | const ngsgStore = { 23 | initState: jest.fn(), 24 | hasSelectedItems: jest.fn(), 25 | getSelectedItems: jest.fn(), 26 | resetSelectedItems: jest.fn(), 27 | hasGroup: jest.fn(), 28 | hasItems: jest.fn(), 29 | setItems: jest.fn(), 30 | getItems: jest.fn(), 31 | } as any; 32 | const ngsgEventService = new NgsgEventsService(); 33 | const scrollHelperService = { 34 | scrollIfNecessary: jest.fn(), 35 | } as any; 36 | const classService = { 37 | addActiveClass: jest.fn(), 38 | } as any; 39 | 40 | beforeEach(() => { 41 | sut = new NgsgItemDirective( 42 | elementRef, 43 | ngsgSortService, 44 | ngsgSelectionService, 45 | ngsgReflectService, 46 | ngsgStore, 47 | ngsgEventService, 48 | scrollHelperService, 49 | classService 50 | ); 51 | }); 52 | 53 | it('should not set selectedElements if the event did not occur on the host', () => { 54 | const event = { 55 | target: { 56 | matches: () => false, 57 | }, 58 | }; 59 | sut.dragStart(event); 60 | expect(ngsgSelectionService.selectElementIfNoSelection).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it('should call selectionService selectElementIfNoSelection if the event occured on the host', () => { 64 | const sortGroup = 'test-group'; 65 | sut.ngSortGridGroup = sortGroup; 66 | const event = { 67 | target: { 68 | matches: () => true, 69 | }, 70 | } as any; 71 | sut.dragStart(event); 72 | expect(ngsgSelectionService.selectElementIfNoSelection).toHaveBeenCalledWith(sortGroup, event.target); 73 | expect(classService.addActiveClass).toHaveBeenCalledWith(event.target); 74 | }); 75 | 76 | it('should init the sort for the current group', () => { 77 | const sortGroup = 'test-group'; 78 | sut.ngSortGridGroup = sortGroup; 79 | const event = { 80 | target: { 81 | matches: () => true, 82 | }, 83 | }; 84 | sut.dragStart(event); 85 | expect(ngsgSortService.initSort).toHaveBeenCalledWith(sortGroup); 86 | }); 87 | 88 | it('should call sort with the host if the event occured on the host', () => { 89 | ngsgStore.hasSelectedItems = () => true; 90 | 91 | sut.dragEnter(); 92 | expect(ngsgSortService.sort).toHaveBeenCalledWith(elementRef.nativeElement); 93 | }); 94 | 95 | it('should sort the items if the event occured on the host and on the correct group', () => { 96 | ngsgStore.hasSelectedItems = () => true; 97 | sut.dragEnter(); 98 | expect(ngsgSortService.sort).toHaveBeenCalledWith(elementRef.nativeElement); 99 | }); 100 | 101 | it('must call event preventDefault', () => { 102 | const preventDefaultSpy = jest.fn(); 103 | const event = { preventDefault: preventDefaultSpy }; 104 | sut.dragOver(event); 105 | expect(preventDefaultSpy).toHaveBeenCalled(); 106 | }); 107 | 108 | it('must return false on dragOver', () => { 109 | const actual = sut.dragOver({}); 110 | expect(actual).toBeFalsy(); 111 | }); 112 | 113 | it('should not call endSort if the group does not contain selectedItems', () => { 114 | ngsgStore.hasSelectedItems = () => false; 115 | sut.drop(); 116 | expect(ngsgSortService.endSort).not.toHaveBeenCalled(); 117 | }); 118 | 119 | it('should sort if the group contains selectedItems', () => { 120 | ngsgStore.hasSelectedItems = () => true; 121 | ngsgStore.getItems = () => []; 122 | ngsgStore.hasItems = () => true; 123 | sut.drop(); 124 | expect(ngsgSortService.endSort).toHaveBeenCalled(); 125 | }); 126 | 127 | it('should call the reflection service with the host if the event occured on it', () => { 128 | const group = 'test-group'; 129 | sut.ngSortGridGroup = group; 130 | ngsgStore.hasSelectedItems = () => true; 131 | ngsgStore.getItems = () => []; 132 | 133 | sut.drop(); 134 | expect(ngsgReflectService.reflectChanges).toHaveBeenCalledWith(group, elementRef.nativeElement); 135 | }); 136 | 137 | it('should emit a OrderChange containing the previous item order and the new itemorder', (done) => { 138 | const group = 'test-group'; 139 | const currentItemOrder = ['item one', 'item two', 'item three']; 140 | const newItemOrder = ['item two', 'item one', 'item three']; 141 | const expectedOrderChange: NgsgOrderChange = { 142 | previousOrder: currentItemOrder, 143 | currentOrder: newItemOrder, 144 | }; 145 | 146 | ngsgStore.hasSelectedItems = () => true; 147 | ngsgStore.hasItems = () => true; 148 | ngsgStore.getItems = () => currentItemOrder; 149 | ngsgReflectService.reflectChanges = () => newItemOrder; 150 | sut.ngSortGridGroup = group; 151 | 152 | sut.sorted.subscribe((orderChange: NgsgOrderChange) => { 153 | expect(orderChange).toEqual(expectedOrderChange); 154 | done(); 155 | }); 156 | sut.drop(); 157 | }); 158 | 159 | it('should reset the selected items on drop', () => { 160 | ngsgStore.hasSelectedItems = () => true; 161 | ngsgStore.hasItems = () => true; 162 | sut.drop(); 163 | expect(ngsgStore.resetSelectedItems).toHaveBeenCalled(); 164 | }); 165 | 166 | it('should stream the dropped event on the eventservice', (done) => { 167 | ngsgStore.hasSelectedItems = () => true; 168 | ngsgStore.hasItems = () => true; 169 | ngsgEventService.dropped$.subscribe(() => done()); 170 | sut.drop(); 171 | }); 172 | 173 | it('should call the selctionservice with the host if the event occured on the host', () => { 174 | const group = 'test-group'; 175 | NgsgElementsHelper.findIndex = () => 0; 176 | ngsgStore.getSelectedItems = () => []; 177 | sut.ngSortGridGroup = group; 178 | 179 | sut.clicked(); 180 | expect(ngsgSelectionService.updateSelectedDragItem).toHaveBeenCalledWith(group, elementRef.nativeElement, true); 181 | }); 182 | 183 | it('should call the selection service with false if the item is selected', () => { 184 | const originalIndex = 0; 185 | const group = 'test-group'; 186 | const element = { originalIndex }; 187 | NgsgElementsHelper.findIndex = () => originalIndex; 188 | ngsgStore.getSelectedItems = () => [element] as any; 189 | sut.ngSortGridGroup = group; 190 | 191 | sut.clicked(); 192 | expect(ngsgSelectionService.updateSelectedDragItem).toHaveBeenCalledWith(group, elementRef.nativeElement, false); 193 | }); 194 | 195 | it(`should init the state with empty items if group has yet not been 196 | initialized and the currentValue is null`, () => { 197 | const group = 'test-group'; 198 | const changes = { 199 | ngSortGridItems: { 200 | currentValue: null, 201 | }, 202 | } as any; 203 | sut.ngSortGridGroup = group; 204 | ngsgStore.hasGroup = () => false; 205 | 206 | sut.ngOnChanges(changes); 207 | expect(ngsgStore.initState).toHaveBeenCalledWith(group, []); 208 | }); 209 | 210 | it('should init the state with items from the currentValue if group has yet not been initialized', () => { 211 | const group = 'test-group'; 212 | const changes = { 213 | ngSortGridItems: { 214 | currentValue: null, 215 | }, 216 | } as any; 217 | sut.ngSortGridGroup = group; 218 | ngsgStore.hasGroup = () => false; 219 | 220 | sut.ngOnChanges(changes); 221 | expect(ngsgStore.initState).toHaveBeenCalledWith(group, []); 222 | }); 223 | 224 | it('should set the items if the group has allready been initialized', () => { 225 | const group = 'test-group'; 226 | const items = ['Item one', 'item two']; 227 | const changes = { 228 | ngSortGridItems: { 229 | currentValue: items, 230 | }, 231 | } as any; 232 | sut.ngSortGridGroup = group; 233 | ngsgStore.hasGroup = () => true; 234 | 235 | sut.ngOnChanges(changes); 236 | expect(ngsgStore.setItems).toHaveBeenCalledWith(group, items); 237 | }); 238 | 239 | it('should log a warning message if you drop and you did not provide any items', () => { 240 | const expectedWarniningMessage = `Ng-sortgrid: No items provided - please use [sortGridItems] to pass in an array of items - 241 | otherwhise the ordered items can not be emitted in the (sorted) event`; 242 | const consoleWarnSpy = jest.spyOn(console, 'warn'); 243 | ngsgStore.hasItems = () => false; 244 | 245 | sut.drop(); 246 | expect(consoleWarnSpy).toHaveBeenCalledWith(expectedWarniningMessage); 247 | }); 248 | 249 | }); 250 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg-item.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ContentChild, 4 | Directive, 5 | ElementRef, 6 | EventEmitter, 7 | HostListener, 8 | Input, 9 | OnChanges, 10 | OnDestroy, 11 | OnInit, 12 | Output, 13 | SimpleChanges 14 | } from '@angular/core'; 15 | import {fromEvent, Subject} from 'rxjs'; 16 | import {takeUntil, takeWhile, throttleTime} from 'rxjs/operators'; 17 | 18 | import {NgsgElementsHelper} from './helpers/element/ngsg-elements.helper'; 19 | import {ScrollHelperService} from './helpers/scroll/scroll-helper.service'; 20 | import {NgsgSelectionService} from './mutliselect/ngsg-selection.service'; 21 | import {NgsgEventsService} from './shared/ngsg-events.service'; 22 | import {NgsgOrderChange} from './shared/ngsg-order-change.model'; 23 | import {NgsgReflectService} from './sort/reflection/ngsg-reflect.service'; 24 | import {NgsgSortService} from './sort/sort/ngsg-sort.service'; 25 | import {NgsgStoreService} from './store/ngsg-store.service'; 26 | import { NgsgClassService } from './helpers/class/ngsg-class.service'; 27 | import { NgsgDragHandleDirective } from './ngsg-drag-handle.directive'; 28 | 29 | const selector = '[ngSortgridItem]'; 30 | 31 | @Directive({ 32 | selector, 33 | standalone: false 34 | }) 35 | export class NgsgItemDirective implements OnInit, OnChanges, AfterViewInit, OnDestroy { 36 | @Input() ngSortGridGroup = 'defaultGroup'; 37 | @Input() ngSortGridItems: any[]; 38 | @Input() scrollPointTop: number; 39 | @Input() scrollPointBottom: number; 40 | @Input() scrollSpeed: number; 41 | @Input() autoScroll = false; 42 | 43 | @Output() sorted = new EventEmitter>(); 44 | 45 | @ContentChild(NgsgDragHandleDirective) handle: NgsgDragHandleDirective; 46 | 47 | private handleElement: HTMLElement; 48 | private selected = false; 49 | private destroy$ = new Subject(); 50 | 51 | constructor( 52 | public el: ElementRef, 53 | private sortService: NgsgSortService, 54 | private selectionService: NgsgSelectionService, 55 | private reflectService: NgsgReflectService, 56 | private ngsgStore: NgsgStoreService, 57 | private ngsgEventService: NgsgEventsService, 58 | private scrollHelperService: ScrollHelperService, 59 | private classService: NgsgClassService 60 | ) { 61 | } 62 | 63 | ngOnInit(): void { 64 | this.ngsgEventService.dropped$.pipe( 65 | takeUntil(this.destroy$) 66 | ).subscribe(() => this.selected = false); 67 | 68 | fromEvent(this.el.nativeElement, 'drag').pipe( 69 | throttleTime(20), 70 | takeUntil(this.destroy$), 71 | takeWhile(() => this.autoScroll) 72 | ).subscribe(() => { 73 | this.scrollHelperService.scrollIfNecessary(event, { 74 | top: this.scrollPointTop, 75 | bottom: this.scrollPointBottom 76 | }, this.scrollSpeed); 77 | } 78 | ); 79 | } 80 | 81 | ngOnChanges(changes: SimpleChanges): void { 82 | const sortGridItemChanges = changes.ngSortGridItems; 83 | const sortGridItems = sortGridItemChanges.currentValue ? sortGridItemChanges.currentValue : []; 84 | 85 | if (!this.ngsgStore.hasGroup(this.ngSortGridGroup)) { 86 | this.ngsgStore.initState(this.ngSortGridGroup, sortGridItems); 87 | return; 88 | } 89 | this.ngsgStore.setItems(this.ngSortGridGroup, sortGridItems); 90 | } 91 | 92 | ngAfterViewInit(): void { 93 | this.handleElement = this.handle?.el?.nativeElement || this.el.nativeElement; 94 | 95 | fromEvent(this.handleElement, 'mousedown').pipe( 96 | takeUntil(this.destroy$) 97 | ).subscribe(() => { 98 | this.el.nativeElement.draggable = true; 99 | } 100 | ); 101 | } 102 | 103 | ngOnDestroy(): void { 104 | this.destroy$.next(true); 105 | this.destroy$.complete(); 106 | } 107 | 108 | @HostListener('dragstart', ['$event']) 109 | dragStart(event): void { 110 | if (!this.occuredOnHost(event)) { 111 | return; 112 | } 113 | this.selectionService.selectElementIfNoSelection(this.ngSortGridGroup, event.target); 114 | this.classService.addActiveClass(event.target); 115 | this.sortService.initSort(this.ngSortGridGroup); 116 | } 117 | 118 | @HostListener('dragenter') 119 | dragEnter(): void { 120 | if (!this.ngsgStore.hasSelectedItems(this.ngSortGridGroup)) { 121 | return; 122 | } 123 | this.sortService.sort(this.el.nativeElement); 124 | } 125 | 126 | @HostListener('dragover', ['$event']) 127 | dragOver(event): boolean { 128 | if (event.preventDefault) { 129 | // Necessary. Allows us to drop. 130 | event.preventDefault(); 131 | } 132 | return false; 133 | } 134 | 135 | @HostListener('dragend') 136 | drop(): void { 137 | this.el.nativeElement.draggable = false; 138 | if (!this.ngsgStore.hasSelectedItems(this.ngSortGridGroup)) { 139 | return; 140 | } 141 | 142 | if (!this.ngsgStore.hasItems(this.ngSortGridGroup)) { 143 | console.warn(`Ng-sortgrid: No items provided - please use [sortGridItems] to pass in an array of items - 144 | otherwhise the ordered items can not be emitted in the (sorted) event`); 145 | return; 146 | } 147 | const previousOrder = [...this.ngsgStore.getItems(this.ngSortGridGroup)]; 148 | this.sortService.endSort(); 149 | const currentOrder = this.reflectService.reflectChanges(this.ngSortGridGroup, this.el.nativeElement); 150 | this.sorted.next({previousOrder, currentOrder}); 151 | this.ngsgStore.resetSelectedItems(this.ngSortGridGroup); 152 | this.ngsgEventService.dropped$.next(true); 153 | } 154 | 155 | @HostListener('click') 156 | clicked(): void { 157 | this.selected = !this.isItemCurrentlySelected(); 158 | this.selectionService.updateSelectedDragItem(this.ngSortGridGroup, this.el.nativeElement, this.selected); 159 | } 160 | 161 | private isItemCurrentlySelected(): boolean { 162 | const index = NgsgElementsHelper.findIndex(this.el.nativeElement); 163 | return !!this.ngsgStore.getSelectedItems(this.ngSortGridGroup) 164 | .find(element => element.originalIndex === index); 165 | } 166 | 167 | private occuredOnHost(event): boolean { 168 | return event.target.matches(selector); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg.css: -------------------------------------------------------------------------------- 1 | .ng-sg-placeholder { 2 | background-color: #f4f4f4 !important; 3 | color: darkgrey !important; 4 | } 5 | 6 | .ng-sg-dropped { 7 | border: 1px solid cornflowerblue !important; 8 | } 9 | 10 | .ng-sg-selected { 11 | background: lightblue!important; 12 | } 13 | 14 | .ng-sg-active { 15 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 16 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), 17 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); 18 | opacity: .6; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/ngsg.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { NgsgItemDirective } from './ngsg-item.directive'; 4 | import { NgsgDragHandleDirective } from './ngsg-drag-handle.directive'; 5 | 6 | @NgModule({ 7 | declarations: [NgsgItemDirective, NgsgDragHandleDirective], 8 | exports: [NgsgItemDirective, NgsgDragHandleDirective] 9 | }) 10 | export class NgsgModule {} 11 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/shared/ngsg-dragelement.model.ts: -------------------------------------------------------------------------------- 1 | export interface NgsgDragelement { 2 | node: Element; 3 | originalIndex: number; 4 | } 5 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/shared/ngsg-events.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Subject} from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class NgsgEventsService { 8 | public dropped$ = new Subject(); 9 | } 10 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/shared/ngsg-order-change.model.ts: -------------------------------------------------------------------------------- 1 | export interface NgsgOrderChange { 2 | previousOrder: T[]; 3 | currentOrder: T[]; 4 | } 5 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/sort/reflection/ngsg-reflect.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {NgsgElementsHelper} from '../../helpers/element/ngsg-elements.helper'; 2 | import {NgsgDragelement} from '../../shared/ngsg-dragelement.model'; 3 | 4 | import {NgsgReflectService} from './ngsg-reflect.service'; 5 | 6 | describe('NgsgReflectService', () => { 7 | const ngsgStoreMock = { 8 | getItems: jest.fn(), 9 | getSelectedItems: jest.fn(), 10 | setItems: jest.fn(), 11 | } as any; 12 | let sut: NgsgReflectService; 13 | 14 | beforeEach(() => { 15 | sut = new NgsgReflectService(ngsgStoreMock); 16 | }); 17 | 18 | it('must emit 1,2,4,5,6,3,7,8,9,10 if we drag 3 to position 6', () => { 19 | const initialOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 20 | const dropIndex = 6; 21 | const selectedItems: NgsgDragelement[] = [{ node: undefined, originalIndex: 2 }]; 22 | const expectedOrder = [1, 2, 4, 5, 6, 7, 3, 8, 9, 10]; 23 | 24 | ngsgStoreMock.getItems = () => initialOrder; 25 | ngsgStoreMock.getSelectedItems = () => selectedItems; 26 | NgsgElementsHelper.findIndex = () => dropIndex; 27 | 28 | const sortedItems = sut.reflectChanges('', {} as any); 29 | expect(sortedItems).toEqual(expectedOrder); 30 | }); 31 | 32 | it('must emit 1,4,5,6,2,3,7,8,9,10 if we drag 2 and 3 to position 6', () => { 33 | const initialOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 34 | const dropIndex = 6; 35 | const selectedItems: NgsgDragelement[] = [ 36 | { node: undefined, originalIndex: 1 }, 37 | { node: undefined, originalIndex: 2 }, 38 | ]; 39 | const expectedOrder = [1, 4, 5, 6, 7, 8, 2, 3, 9, 10]; 40 | 41 | ngsgStoreMock.getItems = () => initialOrder; 42 | ngsgStoreMock.getSelectedItems = () => selectedItems; 43 | NgsgElementsHelper.findIndex = () => dropIndex; 44 | 45 | const sortedItems = sut.reflectChanges('', {} as any); 46 | expect(sortedItems).toEqual(expectedOrder); 47 | }); 48 | 49 | it('must emit 1,4,5,6,3,2,7,8,9,10 if we drag 3 and 2 to position 6', () => { 50 | const initialOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 51 | const dropIndex = 6; 52 | const selectedItems: NgsgDragelement[] = [ 53 | { node: undefined, originalIndex: 2 }, 54 | { node: undefined, originalIndex: 1 }, 55 | ]; 56 | const expectedOrder = [1, 4, 5, 6, 7, 8, 3, 2, 9, 10]; 57 | 58 | ngsgStoreMock.getItems = () => initialOrder; 59 | ngsgStoreMock.getSelectedItems = () => selectedItems; 60 | NgsgElementsHelper.findIndex = () => dropIndex; 61 | 62 | const sortedItems = sut.reflectChanges('', {} as any); 63 | expect(sortedItems).toEqual(expectedOrder); 64 | }); 65 | 66 | it('must emit the same order if the dropIndex ins in the selection', () => { 67 | const initialOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 68 | const dropIndex = 1; 69 | const selectedItems: NgsgDragelement[] = [ 70 | { node: 'some node' as any, originalIndex: 1 }, 71 | { node: 'another node' as any, originalIndex: 2 }, 72 | ]; 73 | const expectedOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 74 | 75 | ngsgStoreMock.getItems = () => initialOrder; 76 | ngsgStoreMock.getSelectedItems = () => selectedItems; 77 | NgsgElementsHelper.findIndex = () => dropIndex; 78 | 79 | const sortedItems = sut.reflectChanges('', 'some node' as any); 80 | expect(sortedItems).toEqual(expectedOrder); 81 | }); 82 | 83 | it('must set the new sort order on the store with ther correct group and the items', () => { 84 | const group = 'exampleGroup'; 85 | const initialOrder = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 86 | const dropIndex = 6; 87 | const selectedItems: NgsgDragelement[] = [{ node: undefined, originalIndex: 2 }]; 88 | const expectedOrder = [1, 2, 4, 5, 6, 7, 3, 8, 9, 10]; 89 | 90 | ngsgStoreMock.getItems = () => initialOrder; 91 | ngsgStoreMock.getSelectedItems = () => selectedItems; 92 | NgsgElementsHelper.findIndex = () => dropIndex; 93 | 94 | sut.reflectChanges(group, {} as any); 95 | expect(ngsgStoreMock.setItems).toHaveBeenCalledWith(group, expectedOrder); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/sort/reflection/ngsg-reflect.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import {NgsgElementsHelper} from '../../helpers/element/ngsg-elements.helper'; 4 | import {NgsgDragelement} from '../../shared/ngsg-dragelement.model'; 5 | import {NgsgStoreService} from '../../store/ngsg-store.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class NgsgReflectService { 11 | constructor(private ngsgStore: NgsgStoreService) { 12 | } 13 | 14 | public reflectChanges(key: string, element: Element): any[] { 15 | const items = this.ngsgStore.getItems(key); 16 | const selectedElements = this.ngsgStore.getSelectedItems(key); 17 | const selectedElementIndices = this.getSelectedElementsIndices(selectedElements); 18 | const selectedItems = this.getSelectedItems(items, selectedElementIndices); 19 | const sortedIndices = selectedElementIndices.sort((a, b) => a - b); 20 | const dropIndex = this.findDropIndex(selectedElements, element); 21 | 22 | while (sortedIndices.length > 0) { 23 | items.splice(sortedIndices.pop(), 1); 24 | } 25 | 26 | const result = this.getReflectedItems(items, selectedItems, dropIndex); 27 | this.ngsgStore.setItems(key, result); 28 | return result; 29 | } 30 | 31 | private getReflectedItems(items: any, selectedItems: any, dropIndex: number): any[] { 32 | const beforeSelection = items.slice(0, dropIndex); 33 | const afterSelection = items.slice(dropIndex, items.length); 34 | return [...beforeSelection, ...selectedItems, ...afterSelection]; 35 | } 36 | 37 | private getSelectedItems(items: any[], selectedElementIndexes: number[]): any[] { 38 | const selectedItems = []; 39 | selectedElementIndexes.forEach(index => { 40 | selectedItems.push(items[index]); 41 | }); 42 | return selectedItems; 43 | } 44 | 45 | private getSelectedElementsIndices(selectedElements: NgsgDragelement[]): number[] { 46 | return selectedElements.map((selectedElement: NgsgDragelement) => selectedElement.originalIndex); 47 | } 48 | 49 | private findDropIndex(selectedElements: NgsgDragelement[], element: Element): number { 50 | if (this.isDropInSelection(selectedElements, element)) { 51 | return NgsgElementsHelper.findIndex(selectedElements[0].node); 52 | } 53 | return NgsgElementsHelper.findIndex(element); 54 | } 55 | 56 | private isDropInSelection(collection: NgsgDragelement[], dropElement: Element): boolean { 57 | return !!collection.find((dragElment: NgsgDragelement) => dragElment.node === dropElement); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/sort/sort/ngsg-sort.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgsgElementsHelper } from '../../helpers/element/ngsg-elements.helper'; 2 | 3 | import { NgsgSortService } from './ngsg-sort.service'; 4 | 5 | describe('NgsgSortService', () => { 6 | let sut: NgsgSortService; 7 | const classService = { 8 | addPlaceHolderClass: jest.fn(), 9 | removePlaceHolderClass: jest.fn(), 10 | addDroppedClass: jest.fn(), 11 | addActiveClass: jest.fn(), 12 | removeSelectedClass: jest.fn(), 13 | removeDroppedClass: jest.fn(), 14 | removeActiveClass: jest.fn(), 15 | } as any; 16 | const ngsgStore = { 17 | getFirstSelectItem: jest.fn(), 18 | getSelectedItems: jest.fn(), 19 | } as any; 20 | 21 | beforeEach(() => { 22 | sut = new NgsgSortService(classService, ngsgStore); 23 | }); 24 | 25 | const createElement = (value, nextSibling) => 26 | ({ 27 | value, 28 | nextSibling, 29 | parentNode: { 30 | insertBefore: jest.fn(), 31 | }, 32 | } as any); 33 | 34 | it('should insert the first element in the middle if we drag it to the right', () => { 35 | const group = 'test-group'; 36 | 37 | const lastElement = createElement(3, null); 38 | const middleElement = createElement(2, lastElement); 39 | const firstElement = createElement(1, middleElement); 40 | const dragElement = { originalIndex: 0, node: firstElement } as any; 41 | const dropElement = middleElement as any; 42 | 43 | ngsgStore.getFirstSelectItem = () => ({ originalIndex: 0 } as any); 44 | ngsgStore.getSelectedItems = () => [dragElement] as any; 45 | const insertBeforeSpy = jest.spyOn(dropElement.parentNode, 'insertBefore'); 46 | NgsgElementsHelper.findIndex = () => 1; 47 | 48 | sut.initSort(group); 49 | sut.sort(dropElement); 50 | 51 | expect(insertBeforeSpy).toHaveBeenCalledWith(dragElement.node, lastElement); 52 | }); 53 | 54 | it('should insert the last element in the middle if we drag it to the left', () => { 55 | const group = 'test-group'; 56 | 57 | const lastElement = createElement(3, null); 58 | const middleElement = createElement(2, lastElement); 59 | const dragElement = { originalIndex: 2, node: lastElement } as any; 60 | const dropElement = middleElement as any; 61 | 62 | ngsgStore.getFirstSelectItem = () => ({ originalIndex: 2 } as any); 63 | ngsgStore.getSelectedItems = () => [dragElement]; 64 | const insertBeforeSpy = jest.spyOn(dropElement.parentNode, 'insertBefore'); 65 | NgsgElementsHelper.findIndex = () => 1; 66 | 67 | sut.initSort(group); 68 | sut.sort(dropElement); 69 | 70 | expect(insertBeforeSpy).toHaveBeenCalledWith(dragElement.node, middleElement); 71 | }); 72 | 73 | it('should remove the placeholder class on all selected elements if the sort ends', () => { 74 | const group = 'test-group'; 75 | const selectedItems = [{ node: 'ItemOne' }, { node: 'ItemTwo' }] as any; 76 | ngsgStore.getSelectedItems = () => selectedItems; 77 | 78 | sut.initSort(group); 79 | sut.endSort(); 80 | 81 | expect(classService.removePlaceHolderClass).toHaveBeenCalledWith(selectedItems[0].node); 82 | expect(classService.removePlaceHolderClass).toHaveBeenCalledWith(selectedItems[1].node); 83 | }); 84 | 85 | it('should add the dropped class on all selected elements if the sort ends', () => { 86 | const group = 'test-group'; 87 | const selectedItems = [{ node: 'ItemOne' }, { node: 'ItemTwo' }] as any; 88 | ngsgStore.getSelectedItems = () => selectedItems; 89 | 90 | sut.initSort(group); 91 | sut.endSort(); 92 | 93 | expect(classService.addDroppedClass).toHaveBeenCalledWith(selectedItems[0].node); 94 | expect(classService.addDroppedClass).toHaveBeenCalledWith(selectedItems[1].node); 95 | }); 96 | 97 | it('should remove the selected class on all selected elements if the sort ends', () => { 98 | const group = 'test-group'; 99 | const selectedItems = [{ node: 'ItemOne' }, { node: 'ItemTwo' }] as any; 100 | ngsgStore.getSelectedItems = () => selectedItems; 101 | 102 | sut.initSort(group); 103 | sut.endSort(); 104 | 105 | expect(classService.removeSelectedClass).toHaveBeenCalledWith(selectedItems[0].node); 106 | expect(classService.removeSelectedClass).toHaveBeenCalledWith(selectedItems[1].node); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/sort/sort/ngsg-sort.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { timer } from 'rxjs'; 3 | 4 | import {NgsgClassService} from '../../helpers/class/ngsg-class.service'; 5 | import {NgsgElementsHelper} from '../../helpers/element/ngsg-elements.helper'; 6 | import {NgsgDragelement} from '../../shared/ngsg-dragelement.model'; 7 | import {NgsgStoreService} from '../../store/ngsg-store.service'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class NgsgSortService { 13 | private dragIndex: number; 14 | private dragElements: NgsgDragelement[]; 15 | 16 | constructor( 17 | private classService: NgsgClassService, 18 | private ngsgStore: NgsgStoreService 19 | ) {} 20 | 21 | public initSort(group: string): void { 22 | this.dragIndex = this.ngsgStore.getFirstSelectItem(group).originalIndex; 23 | this.dragElements = this.ngsgStore.getSelectedItems(group); 24 | } 25 | 26 | public sort(dropElement: Element): void { 27 | const hoverIndex = NgsgElementsHelper.findIndex(dropElement); 28 | const el = this.getSibling(dropElement, this.dragIndex, hoverIndex); 29 | 30 | if (this.isDropInSelection(el)) { 31 | return; 32 | } 33 | this.dragElements.forEach((dragElement: NgsgDragelement) => { 34 | const insertedNode = dropElement.parentNode.insertBefore(dragElement.node, el.node); 35 | this.classService.addPlaceHolderClass(insertedNode as Element); 36 | }); 37 | this.dragIndex = NgsgElementsHelper.findIndex(this.dragElements[0].node); 38 | } 39 | 40 | public endSort(): void { 41 | this.dragElements.forEach((dragElement: NgsgDragelement) => { 42 | this.updateDropedItem(dragElement.node); 43 | }); 44 | } 45 | 46 | private getSibling(dropElement: any, dragIndex: number, hoverIndex: number): NgsgDragelement | null { 47 | if (dragIndex < hoverIndex) { 48 | return { node: dropElement.nextSibling, originalIndex: hoverIndex + 1 }; 49 | } 50 | return { node: dropElement, originalIndex: hoverIndex }; 51 | } 52 | 53 | private isDropInSelection(dropElement: NgsgDragelement): boolean { 54 | return !!this.dragElements.find((dragElment: NgsgDragelement) => dragElment.node === dropElement.node); 55 | } 56 | 57 | private updateDropedItem(item: Element): void { 58 | this.classService.removePlaceHolderClass(item); 59 | this.classService.addDroppedClass(item); 60 | this.classService.removeSelectedClass(item); 61 | this.classService.removeActiveClass(item); 62 | timer(500).subscribe(() => this.classService.removeDroppedClass(item)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/store/ngsg-store.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {NgsgStoreService} from './ngsg-store.service'; 2 | 3 | describe('NgsgStoreService', () => { 4 | 5 | let sut: NgsgStoreService; 6 | 7 | beforeEach(() => { 8 | sut = new NgsgStoreService(); 9 | }); 10 | 11 | describe('InitState', () => { 12 | 13 | it('should add the items to the group', () => { 14 | const group = 'testgroup'; 15 | const items = ['Item1', 'Item2']; 16 | const classes = []; 17 | 18 | sut.initState(group, items, classes); 19 | expect(sut.getItems(group)).toEqual(items); 20 | }); 21 | 22 | it('should add empty items to the group if we do not pass items in', () => { 23 | const group = 'testgroup'; 24 | const items = undefined; 25 | const classes = []; 26 | 27 | sut.initState(group, items, classes); 28 | expect(sut.getItems(group)).toEqual([]); 29 | }); 30 | 31 | it('should return false if the group does not contain items', () => { 32 | const group = 'testgroup'; 33 | sut.initState(group); 34 | 35 | const hasItems = sut.hasItems(group); 36 | expect(hasItems).toBeFalsy(); 37 | }); 38 | 39 | it('should return true if the group contains items', () => { 40 | const group = 'testgroup'; 41 | sut.initState(group, ['item one', 'item two']); 42 | 43 | const hasItems = sut.hasItems(group); 44 | expect(hasItems).toBeTruthy(); 45 | }); 46 | 47 | it('should return false if the current group has yet not been initialized', () => { 48 | const group = 'testgroup'; 49 | 50 | const hasGroup = sut.hasGroup(group); 51 | expect(hasGroup).toBeFalsy(); 52 | }); 53 | 54 | it('should return true if the current group has been initialized', () => { 55 | const group = 'testgroup'; 56 | sut.initState(group); 57 | 58 | const hasGroup = sut.hasGroup(group); 59 | expect(hasGroup).toBeTruthy(); 60 | }); 61 | 62 | it('should add the classes to the group', () => { 63 | const group = 'testgroup'; 64 | const items = ['Item1', 'Item2']; 65 | const classes = ['Class1', 'Class2']; 66 | 67 | sut.initState(group, items, classes); 68 | expect(sut.getClasses(group)).toEqual(classes); 69 | }); 70 | }); 71 | 72 | it('should set the items and then return it', () => { 73 | const group = 'testGroup'; 74 | const items = ['ItemOne', 'ItemTwo']; 75 | sut.initState(group); 76 | 77 | sut.setItems(group, items); 78 | expect(sut.getItems(group)).toEqual(items); 79 | }); 80 | 81 | it('should set the selectedItems and then return it', () => { 82 | const group = 'testGroup'; 83 | const selectedItems = ['ItemOne', 'ItemTwo']; 84 | sut.initState(group); 85 | 86 | sut.setSelectedItems(group, selectedItems); 87 | expect(sut.getSelectedItems(group) as any).toEqual(selectedItems); 88 | }); 89 | 90 | it('should get the first selectedItem', () => { 91 | const group = 'testGroup'; 92 | const firstItem = 'ItemOne' as any; 93 | const selectedItems = [firstItem, 'ItemTwo'] as any[]; 94 | sut.initState(group); 95 | 96 | sut.setSelectedItems(group, selectedItems); 97 | expect(sut.getFirstSelectItem(group)).toEqual(firstItem); 98 | }); 99 | 100 | it('should add selected items', () => { 101 | const group = 'test-group'; 102 | const selectedItem = 'Item one' as any; 103 | sut.initState(group); 104 | sut.addSelectedItem(group, selectedItem); 105 | 106 | expect(sut.getSelectedItems(group) as any).toEqual([selectedItem]); 107 | }); 108 | 109 | it('should remove the selected item', () => { 110 | const group = 'test-group'; 111 | const itemOne = {node: 'item one'}; 112 | const itemTwo = {node: 'item two'}; 113 | const itemThree = {node: 'item three'}; 114 | 115 | const selectedItems = [itemOne, itemTwo, itemThree]; 116 | sut.initState(group); 117 | sut.setSelectedItems(group, selectedItems); 118 | sut.removeSelectedItem(group, 'item two' as any); 119 | 120 | expect(sut.getSelectedItems(group) as any).toEqual([itemOne, itemThree]); 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/lib/store/ngsg-store.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | import {NgsgDragelement} from '../shared/ngsg-dragelement.model'; 4 | 5 | // TODO add interfaces for classes 6 | export interface NgsgState { 7 | items: any[]; 8 | classes: any; 9 | selectedItems: NgsgDragelement[]; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root' 14 | }) 15 | export class NgsgStoreService { 16 | private state = new Map(); 17 | 18 | public initState(group: string, items: any[] = [], classes: any = {}): void { 19 | this.state.set(group, {items: [...items], classes, selectedItems: []}); 20 | } 21 | 22 | public addSelectedItem(group: string, dragElement: NgsgDragelement): void { 23 | this.state.get(group).selectedItems.push(dragElement); 24 | } 25 | 26 | public removeSelectedItem(group: string, item: Element): void { 27 | const updatedItems = this.state.get(group).selectedItems 28 | .filter((dragElement: NgsgDragelement) => dragElement.node !== item); 29 | this.setSelectedItems(group, updatedItems); 30 | } 31 | 32 | public setItems(group: string, items: any): void { 33 | this.state.get(group).items = [...items]; 34 | } 35 | 36 | public getItems(group: string): any[] { 37 | return this.state.get(group).items; 38 | } 39 | 40 | public hasItems(group: string): boolean { 41 | return this.getItems(group).length > 0; 42 | } 43 | 44 | public hasGroup(group: string): boolean { 45 | return this.state.has(group); 46 | } 47 | 48 | public getSelectedItems(group: string): NgsgDragelement[] { 49 | return this.state.get(group).selectedItems; 50 | } 51 | 52 | public setSelectedItems(group: string, selectedItems: any[]): void { 53 | this.state.get(group).selectedItems = [...selectedItems]; 54 | } 55 | 56 | public getFirstSelectItem(group: string): NgsgDragelement { 57 | return this.state.get(group).selectedItems[0]; 58 | } 59 | 60 | public hasSelectedItems(group: string): boolean { 61 | return this.getSelectedItems(group).length > 0; 62 | } 63 | 64 | public resetSelectedItems(group: string): void { 65 | this.setSelectedItems(group, []); 66 | } 67 | 68 | public getClasses(group: string): any[] { 69 | return this.state.get(group).classes; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-sortgrid 3 | */ 4 | export * from './lib/ngsg.module'; 5 | export * from './lib/ngsg-item.directive'; 6 | export * from './lib/ngsg-drag-handle.directive'; 7 | export * from './lib/shared/ngsg-order-change.model'; 8 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": false, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "types": [], 14 | "lib": [ 15 | "dom", 16 | "es2018" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "annotateForClosureCompiler": true, 21 | "compilationMode": "partial", 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "enableResourceInlining": true 27 | }, 28 | "exclude": [ 29 | "**/*.spec.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jest", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | ], 12 | "include": [ 13 | "**/*.spec.ts", 14 | "**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /projects/ng-sortgrid/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | false, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /setupJest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "ng-sortgrid": [ 23 | "dist/ng-sortgrid" 24 | ], 25 | "ng-sortgrid/*": [ 26 | "dist/ng-sortgrid/*" 27 | ] 28 | }, 29 | "useDefineForClassFields": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-parens": false, 15 | "arrow-return-shorthand": true, 16 | "curly": true, 17 | "deprecation": { 18 | "severity": "warn" 19 | }, 20 | "eofline": true, 21 | "import-blacklist": [ 22 | true, 23 | "rxjs/Rx" 24 | ], 25 | "import-spacing": true, 26 | "indent": { 27 | "options": [ 28 | "spaces" 29 | ] 30 | }, 31 | "interface-name": false, 32 | "max-classes-per-file": false, 33 | "member-access": false, 34 | "member-ordering": [ 35 | true, 36 | { 37 | "order": [ 38 | "static-field", 39 | "instance-field", 40 | "static-method", 41 | "instance-method" 42 | ] 43 | } 44 | ], 45 | "no-consecutive-blank-lines": false, 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "object-literal-sort-keys": false, 68 | "ordered-imports": false, 69 | "quotemark": [ 70 | true, 71 | "single" 72 | ], 73 | "semicolon": { 74 | "options": [ 75 | "always" 76 | ] 77 | }, 78 | "space-before-function-paren": { 79 | "options": { 80 | "anonymous": "never", 81 | "asyncArrow": "always", 82 | "constructor": "never", 83 | "method": "never", 84 | "named": "never" 85 | } 86 | }, 87 | "trailing-comma": false, 88 | "no-output-on-prefix": true, 89 | "no-inputs-metadata-property": true, 90 | "no-outputs-metadata-property": true, 91 | "no-host-metadata-property": true, 92 | "no-input-rename": true, 93 | "no-output-rename": true, 94 | "typedef-whitespace": { 95 | "options": [ 96 | { 97 | "call-signature": "nospace", 98 | "index-signature": "nospace", 99 | "parameter": "nospace", 100 | "property-declaration": "nospace", 101 | "variable-declaration": "nospace" 102 | }, 103 | { 104 | "call-signature": "onespace", 105 | "index-signature": "onespace", 106 | "parameter": "onespace", 107 | "property-declaration": "onespace", 108 | "variable-declaration": "onespace" 109 | } 110 | ] 111 | }, 112 | "use-lifecycle-interface": true, 113 | "use-pipe-transform-interface": true, 114 | "component-class-suffix": true, 115 | "directive-class-suffix": true 116 | , "variable-name": { 117 | "options": [ 118 | "ban-keywords", 119 | "check-format", 120 | "allow-pascal-case" 121 | ] 122 | }, 123 | "whitespace": { 124 | "options": [ 125 | "check-branch", 126 | "check-decl", 127 | "check-operator", 128 | "check-separator", 129 | "check-type", 130 | "check-typecast" 131 | ] 132 | } 133 | } 134 | } 135 | --------------------------------------------------------------------------------