diff --git a/projects/ngx-datatable/src/lib/components/header/header.component.spec.ts b/projects/ngx-datatable/src/lib/components/header/header.component.spec.ts
index ba934769d..9eeabf879 100644
--- a/projects/ngx-datatable/src/lib/components/header/header.component.spec.ts
+++ b/projects/ngx-datatable/src/lib/components/header/header.component.spec.ts
@@ -270,4 +270,36 @@ describe('DataTableHeaderComponent', () => {
const rightGroupStyle = await harness.getTransformStyle('right');
expect(rightGroupStyle).toBe('width: 200px;');
});
+
+ it('should not reorder a column when the pointer is not moved', async () => {
+ const columns = toInternalColumn([
+ { prop: 'col1', name: 'Column 1', width: 100, draggable: true },
+ { prop: 'col2', name: 'Column 2', width: 200, draggable: true }
+ ]);
+ componentRef.setInput('columns', columns);
+ componentRef.setInput('reorderable', true);
+ await fixture.whenStable();
+
+ const firstCell = fixture.nativeElement.querySelector('datatable-header-cell') as HTMLElement;
+ const reorderSpy = vi.fn();
+ componentRef.instance.reorder.subscribe(reorderSpy);
+ const elementsFromPointSpy = vi
+ .spyOn(document, 'elementsFromPoint')
+ .mockReturnValue([firstCell]);
+
+ const dragEvent = {
+ initialX: 50,
+ initialY: 0,
+ currentX: 50,
+ currentY: 0,
+ element: firstCell,
+ model: columns[0]
+ };
+ componentRef.instance.onDragStart(dragEvent);
+ componentRef.instance.onDragEnd(dragEvent);
+
+ expect(reorderSpy).not.toHaveBeenCalled();
+ expect(firstCell.style.transform).toBe('');
+ elementsFromPointSpy.mockRestore();
+ });
});
diff --git a/projects/ngx-datatable/src/lib/components/header/header.component.ts b/projects/ngx-datatable/src/lib/components/header/header.component.ts
index 912984154..33ecf6454 100644
--- a/projects/ngx-datatable/src/lib/components/header/header.component.ts
+++ b/projects/ngx-datatable/src/lib/components/header/header.component.ts
@@ -2,23 +2,28 @@ import { NgStyle } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
+ computed,
+ DOCUMENT,
+ ElementRef,
inject,
- TemplateRef,
input,
- computed,
- output
+ output,
+ signal,
+ TemplateRef,
+ viewChildren
} from '@angular/core';
-import { DatatableDraggableDirective } from '../../directives/datatable-draggable.directive';
-import { OrderableDirective } from '../../directives/orderable.directive';
+import {
+ DatatableDraggableDirective,
+ DragEvent
+} from '../../directives/datatable-draggable.directive';
import { ScrollbarHelper } from '../../services/scrollbar-helper.service';
import {
ColumnResizeEventInternal,
InnerSortEvent,
ReorderEventInternal,
SortableTableColumnInternal,
- TableColumnInternal,
- TargetChangedEvent
+ TableColumnInternal
} from '../../types/internal.types';
import {
Row,
@@ -34,17 +39,14 @@ import { DataTableHeaderCellComponent } from './header-cell.component';
@Component({
selector: 'datatable-header',
- imports: [OrderableDirective, NgStyle, DataTableHeaderCellComponent, DatatableDraggableDirective],
+ imports: [NgStyle, DataTableHeaderCellComponent, DatatableDraggableDirective],
template: `
@let _columnGroupWidths = this._columnGroupWidths();
`
@@ -26,7 +26,7 @@ class TestFixtureComponent {
dragStart(): void {}
- dragEnd(): void {}
+ dragEnd(event: DragEvent): void {}
dragMove(event: DragEvent): void {}
}
@@ -36,7 +36,7 @@ describe('DraggableDirective', () => {
let component: TestFixtureComponent;
let harness: DraggableHarness;
let dragStartSpy: Mock<() => void>;
- let dragEndSpy: Mock<() => void>;
+ let dragEndSpy: Mock<(event: DragEvent) => void>;
let dragMoveSpy: Mock<(event: DragEvent) => void>;
beforeEach(async () => {
@@ -70,6 +70,19 @@ describe('DraggableDirective', () => {
vi.useRealTimers();
});
+ it('should provide the initial position when dragging ends without moving', async () => {
+ vi.useFakeTimers();
+ await harness.mouseDown(25);
+ vi.advanceTimersByTime(0);
+ await fixture.whenStable();
+ await harness.mouseUp();
+
+ expect(dragEndSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ currentX: 25, initialX: 25 })
+ );
+ vi.useRealTimers();
+ });
+
it('should fire touch drag events', async () => {
vi.useFakeTimers();
await harness.touchStart(0);
diff --git a/projects/ngx-datatable/src/lib/directives/orderable.directive.spec.ts b/projects/ngx-datatable/src/lib/directives/orderable.directive.spec.ts
deleted file mode 100644
index 862e37730..000000000
--- a/projects/ngx-datatable/src/lib/directives/orderable.directive.spec.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/* eslint-disable @typescript-eslint/dot-notation */
-import { Component, QueryList, signal, ViewChildren } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
-
-import { TableColumnInternal } from '../types/internal.types';
-import { toInternalColumn } from '../utils/column-helper';
-import { DatatableDraggableDirective } from './datatable-draggable.directive';
-import { OrderableDirective } from './orderable.directive';
-
-@Component({
- selector: 'test-fixture-component',
- imports: [OrderableDirective, DatatableDraggableDirective],
- template: `
-
- @for (item of draggables(); track $index) {
-
- }
-
- `
-})
-class TestFixtureComponent {
- readonly draggables = signal([]);
- @ViewChildren(DatatableDraggableDirective)
- draggableDirectives!: QueryList;
-}
-
-describe('OrderableDirective', () => {
- let fixture: ComponentFixture;
- let component: TestFixtureComponent;
-
- beforeEach(async () => {
- fixture = TestBed.createComponent(TestFixtureComponent);
- component = fixture.componentInstance;
- /* This is required in order to resolve the `ContentChildren`.
- * If we don't go through at least on change detection cycle
- * the `draggables` will be `undefined` and `ngOnDestroy` will
- * fail.
- */
- await fixture.whenStable();
- });
-
- describe('fixture', () => {
- let directive: OrderableDirective;
-
- beforeEach(() => {
- directive = fixture.debugElement
- .query(By.directive(OrderableDirective))
- .injector.get(OrderableDirective);
- });
-
- it('should have a component instance', () => {
- expect(component).toBeTruthy();
- });
-
- it('should have OrderableDirective directive', () => {
- expect(directive).toBeTruthy();
- });
-
- describe('when a draggable is removed', () => {
- const checkAllSubscriptionsForActiveObservers = () => {
- const subs = directive.draggables().map(d => {
- expect(d.dragStart['listeners']).not.toHaveLength(0);
- expect(d.dragEnd['listeners']).not.toHaveLength(0);
-
- return {
- dragStart: d.dragStart['listeners'],
- dragEnd: d.dragEnd['listeners']
- };
- });
-
- subs.forEach(sub => {
- expect(sub.dragStart.length).toBe(1);
- expect(sub.dragEnd.length).toBe(1);
- });
- };
-
- const newDraggable = (name: string): TableColumnInternal => {
- return toInternalColumn([{ name }])[0];
- };
-
- beforeEach(async () => {
- component.draggables.set([newDraggable('d1'), newDraggable('d2'), newDraggable('d3')]);
- await fixture.whenStable();
-
- checkAllSubscriptionsForActiveObservers();
- });
-
- it('then dragStart and dragEnd are unsubscribed from the removed draggable', async () => {
- const unsubbed = component.draggableDirectives.toArray()[0];
- component.draggables.update(items => items.slice(1));
-
- expect(unsubbed.dragStart['listeners']).not.toHaveLength(0);
- expect(unsubbed.dragEnd['listeners']).not.toHaveLength(0);
- await fixture.whenStable();
-
- expect(unsubbed.dragStart['listeners']).toHaveLength(0);
- expect(unsubbed.dragEnd['listeners']).toHaveLength(0);
- });
- });
- });
-});
diff --git a/projects/ngx-datatable/src/lib/directives/orderable.directive.ts b/projects/ngx-datatable/src/lib/directives/orderable.directive.ts
deleted file mode 100644
index 7231ba88f..000000000
--- a/projects/ngx-datatable/src/lib/directives/orderable.directive.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import {
- AfterContentInit,
- ContentChildren,
- Directive,
- effect,
- inject,
- KeyValueChangeRecord,
- KeyValueDiffer,
- KeyValueDiffers,
- OnDestroy,
- output,
- OutputRefSubscription,
- QueryList,
- signal,
- DOCUMENT
-} from '@angular/core';
-import { startWith } from 'rxjs';
-
-import {
- ReorderEventInternal,
- TableColumnInternal,
- TargetChangedEvent
-} from '../types/internal.types';
-import { DragEvent, DatatableDraggableDirective } from './datatable-draggable.directive';
-
-interface OrderPosition {
- left: number;
- right: number;
- index: number;
- element: HTMLElement;
-}
-
-@Directive({
- selector: '[orderable]'
-})
-export class OrderableDirective implements AfterContentInit, OnDestroy {
- private document = inject(DOCUMENT);
-
- readonly reorder = output();
- readonly targetChanged = output();
-
- // This should be contentChildren() query, but there is an open Angular issue with signal queries (https://github.com/angular/angular/issues/59067)
- // This problem causes the orderable directive to fail because the contentChildren query is resolved too early.
- // At that state, the input is not yet set, resulting in a NG0950 error.
- @ContentChildren(DatatableDraggableDirective, { descendants: true })
- draggablesQueryList!: QueryList;
-
- readonly draggables = signal([]);
-
- readonly subscriptions = new Map();
-
- positions?: Record;
- readonly differ: KeyValueDiffer = inject(KeyValueDiffers)
- .find({})
- .create();
- lastDraggingIndex?: number;
-
- constructor() {
- effect(() => {
- const diffMap = this.draggables().reduce(
- (acc, curr) => {
- acc[curr.dragModel()!.$$id] = curr;
- return acc;
- },
- {} as Record
- );
-
- this.updateSubscriptions(diffMap);
- });
- }
-
- ngAfterContentInit(): void {
- this.draggablesQueryList.changes.pipe(startWith(this.draggablesQueryList)).subscribe(() => {
- this.draggables.set(this.draggablesQueryList.toArray());
- });
- }
-
- ngOnDestroy(): void {
- this.subscriptions.forEach(subList => subList.forEach(sub => sub.unsubscribe()));
- }
-
- updateSubscriptions(diffMap: Record): void {
- const differResult = this.differ.diff(diffMap);
- if (!differResult) {
- return;
- }
- differResult.forEachAddedItem(record => this.subscribeToDraggable(record));
- differResult.forEachRemovedItem(record => this.unsubscribeFromDraggable(record));
- }
-
- private subscribeToDraggable = (
- record: KeyValueChangeRecord
- ): void => {
- this.unsubscribeFromDraggable(record);
- const { key, currentValue } = record;
- if (!currentValue) {
- return;
- }
- const subs = this.subscriptions.get(key) ?? [];
- let currentEvent: DragEvent;
- subs.push(
- currentValue.dragStart.subscribe(() => this.onDragStart()),
- currentValue.dragMove.subscribe(e => {
- currentEvent = e;
- this.onDragging(e, currentValue.dragModel()!, currentValue.element);
- }),
- currentValue.dragEnd.subscribe(() =>
- this.onDragEnd(currentEvent!, currentValue.dragModel()!, currentValue.element)
- )
- );
- this.subscriptions.set(key, subs);
- };
-
- private unsubscribeFromDraggable = (
- record: KeyValueChangeRecord
- ): void => {
- const { key, previousValue } = record;
- if (!previousValue) {
- return;
- }
- const subs = this.subscriptions.get(key);
- if (!subs) {
- return;
- }
- subs.forEach(sub => sub.unsubscribe());
- this.subscriptions.delete(key);
- };
-
- onDragStart(): void {
- const positions: Record = {};
- this.draggables().forEach((draggable, idx) => {
- const elm = draggable.element;
- const left = parseInt(elm.offsetLeft.toString(), 10);
- positions[draggable.dragModel()!.$$id] = {
- left,
- right: left + parseInt(elm.offsetWidth.toString(), 10),
- index: idx,
- element: elm
- };
- });
- this.positions = positions;
- }
-
- onDragging(
- { currentX, currentY, initialX }: DragEvent,
- model: TableColumnInternal,
- element: HTMLElement
- ): void {
- if (!this.positions) {
- return;
- }
- const prevPos = this.positions[model.$$id];
- const target = this.isTarget(model, currentX, currentY);
- if (target) {
- if (this.lastDraggingIndex !== target.index) {
- this.targetChanged.emit({
- prevIndex: this.lastDraggingIndex!,
- newIndex: target.index,
- initialIndex: prevPos.index
- });
- this.lastDraggingIndex = target.index;
- }
- } else if (this.lastDraggingIndex !== prevPos.index) {
- this.targetChanged.emit({
- prevIndex: this.lastDraggingIndex!,
- initialIndex: prevPos.index
- });
- this.lastDraggingIndex = prevPos.index;
- }
-
- requestAnimationFrame(() => (element.style.left = `${currentX - initialX}px`));
- }
-
- onDragEnd(
- { currentX, currentY }: DragEvent,
- model: TableColumnInternal,
- element: HTMLElement
- ): void {
- if (!this.positions) {
- return;
- }
- const prevPos = this.positions[model.$$id];
- const target = this.isTarget(model, currentX, currentY);
- if (target) {
- this.reorder.emit({
- prevValue: prevPos.index,
- newValue: target.index,
- column: model
- });
- }
- this.lastDraggingIndex = undefined;
- element.style.left = 'auto';
- }
-
- isTarget(
- model: TableColumnInternal,
- clientX: number,
- clientY: number
- ): { pos: OrderPosition; index: number } | undefined {
- if (!this.positions) {
- return undefined;
- }
- const elementsAtPoint = this.document.elementsFromPoint(clientX, clientY);
- return Object.entries(this.positions).reduce<{ pos: OrderPosition; index: number } | undefined>(
- (acc, [id, pos], idx) => {
- // since we drag the inner span, we need to find it in the elements at the cursor
- if (!acc && model.$$id !== id && elementsAtPoint.some(el => el === pos.element)) {
- return { pos, index: idx };
- }
- return acc;
- },
- undefined
- );
- }
-}
diff --git a/projects/ngx-datatable/src/lib/types/internal.types.ts b/projects/ngx-datatable/src/lib/types/internal.types.ts
index e36488095..64c421df6 100644
--- a/projects/ngx-datatable/src/lib/types/internal.types.ts
+++ b/projects/ngx-datatable/src/lib/types/internal.types.ts
@@ -18,12 +18,6 @@ export interface ColumnGroupWidth {
total: number;
}
-export interface TargetChangedEvent {
- newIndex?: number;
- prevIndex: number;
- initialIndex: number;
-}
-
export interface ColumnResizeEventInternal {
column: TableColumnInternal;
prevValue: number;
@@ -76,8 +70,6 @@ export interface BaseTableColumnInternal extends Omit<
$$originalColumn: TableColumn;
dragging?: boolean;
- isTarget?: boolean;
- targetMarkerContext?: any;
// Those properties are never null on the internal type:
name: string;