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/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-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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": "^21.0.0",
28 | "@angular/cli": "^21.0.0",
29 | "@angular/common": "^21.0.0",
30 | "@angular/compiler": "^21.0.0",
31 | "@angular/core": "^21.0.0",
32 | "@angular/forms": "^21.0.0",
33 | "@angular/platform-browser": "^21.0.0",
34 | "@angular/platform-browser-dynamic": "^21.0.0",
35 | "@angular/router": "^21.0.0",
36 | "rxjs": "~7.8.1",
37 | "tslib": "^2.6.2",
38 | "zone.js": "~0.15.0"
39 | },
40 | "devDependencies": {
41 | "@angular-devkit/build-angular": "^21.0.0",
42 | "@angular-eslint/builder": "^21.1.0",
43 | "@angular-eslint/eslint-plugin": "^21.1.0",
44 | "@angular-eslint/eslint-plugin-template": "^21.1.0",
45 | "@angular-eslint/schematics": "^21.1.0",
46 | "@angular-eslint/template-parser": "^21.1.0",
47 | "@angular/compiler-cli": "^21.0.0",
48 | "@angular/language-service": "^21.0.0",
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": "^21.0.0",
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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-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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ng-sortgrid
2 |
3 | [](#contributors-)
4 |
5 |
6 | [](https://travis-ci.org/kreuzerk/ng-sortgrid)
7 | [](https://codecov.io/gh/kreuzerk/ng-sortgrid)
8 | []()
9 |
10 | 
11 |
12 | 
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 | 
59 |
60 | Apply the ngSortgridItem directive
61 |
62 | 
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 | 
70 | React on the 'sorted' output event. The `sorted` output event emits a `NgsgOrderChange` which contains the `previousOrder` and the `currentOrder`
71 |
72 | 
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 | 
79 |
80 | ## Use the async pipe
81 | You can also use the async pipe to display items
82 |
83 | 
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 | 
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 |