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();
@for (colGroup of _columnsByPin(); track colGroup.type) { @if (colGroup.columns.length) { @@ -59,9 +61,9 @@ import { DataTableHeaderCellComponent } from './header-cell.component'; dragStartDelay="500" [datatableDraggable]="reorderable() && column.draggable" [dragModel]="column" - [isTarget]="column.isTarget" + [isTarget]="targetColumn() === column" [targetMarkerTemplate]="targetMarkerTemplate()" - [targetMarkerContext]="column.targetMarkerContext" + [targetMarkerContext]="targetMarkerContext()" [column]="column" [showResizeHandle]="lastColumnId() !== column.$$id && column.resizeable" [sortType]="sortType()" @@ -73,6 +75,9 @@ import { DataTableHeaderCellComponent } from './header-cell.component'; [allRowsSelected]="allRowsSelected()" [enableClearingSortState]="enableClearingSortState()" [ariaHeaderCheckboxMessage]="ariaHeaderCheckboxMessage()" + (dragStart)="onDragStart($event)" + (dragMove)="onDragMove($event)" + (dragEnd)="onDragEnd($event)" (resize)="onColumnResized($event)" (resizing)="onColumnResizing($event)" (sort)="onSort($event)" @@ -94,7 +99,9 @@ import { DataTableHeaderCellComponent } from './header-cell.component'; } }) export class DataTableHeaderComponent { - private scrollbarHelper = inject(ScrollbarHelper); + private readonly document = inject(DOCUMENT); + private readonly scrollbarHelper = inject(ScrollbarHelper); + private readonly headerCells = viewChildren(DataTableHeaderCellComponent, { read: ElementRef }); readonly lastColumnId = computed(() => this.columns().at(-1)?.$$id); @@ -113,7 +120,6 @@ export class DataTableHeaderComponent { readonly reorderable = input(); readonly verticalScrollVisible = input(false); readonly ariaHeaderCheckboxMessage = input.required(); - readonly headerHeight = input.required<'auto' | number>(); readonly columns = input.required(); @@ -127,21 +133,16 @@ export class DataTableHeaderComponent { column: TableColumnInternal; }>(); - readonly _columnsByPin = computed(() => { - return columnsByPinArr(this.columns()); - }); + readonly _columnsByPin = computed(() => columnsByPinArr(this.columns())); readonly _columnGroupWidths = computed(() => { const colsByPin = columnsByPin(this.columns()); return columnGroupWidths(colsByPin, this.columns()); }); - readonly _styleByGroup = computed(() => { - return { - left: this.calcStylesByGroup('left'), - center: this.calcStylesByGroup('center'), - right: this.calcStylesByGroup('right') - }; - }); - + readonly _styleByGroup = computed(() => ({ + left: this.calcStylesByGroup('left'), + center: this.calcStylesByGroup('center'), + right: this.calcStylesByGroup('right') + })); readonly headerWidth = computed(() => { if (this.scrollbarH()) { const width = this.verticalScrollVisible() @@ -152,6 +153,15 @@ export class DataTableHeaderComponent { return '100%'; }); + readonly columnGroups = computed(() => this._columnsByPin()); + private readonly renderedColumns = computed(() => + this.columnGroups().flatMap(group => group.columns) + ); + + private dragInitialIndex?: number; + private dragTargetIndex?: number; + readonly targetColumn = signal(undefined); + readonly targetMarkerContext = signal<{ class: string } | undefined>(undefined); onColumnResized({ width, column }: { width: number; column: TableColumnInternal }): void { this.resize.emit(this.makeResizeEvent(width, column)); @@ -177,44 +187,59 @@ export class DataTableHeaderComponent { }; } - onColumnReordered(event: ReorderEventInternal): void { - const column = this.getColumn(event.newValue); - column.isTarget = false; - column.targetMarkerContext = undefined; - this.reorder.emit(event); + onDragStart({ model }: DragEvent): void { + this.dragInitialIndex = model ? this.renderedColumns().indexOf(model) : undefined; } - onTargetChanged({ prevIndex, newIndex, initialIndex }: TargetChangedEvent): void { - if (prevIndex || prevIndex === 0) { - const oldColumn = this.getColumn(prevIndex); - oldColumn.isTarget = false; - oldColumn.targetMarkerContext = undefined; - } - if (newIndex || newIndex === 0) { - const newColumn = this.getColumn(newIndex); - newColumn.isTarget = true; + onDragMove(event: DragEvent): void { + const targetIndex = this.getDragTargetIndex(event); + if (targetIndex !== this.dragTargetIndex) { + if (this.dragTargetIndex !== undefined) { + this.targetColumn.set(undefined); + this.targetMarkerContext.set(undefined); + } - if (initialIndex !== newIndex) { - newColumn.targetMarkerContext = { - class: 'targetMarker '.concat(initialIndex > newIndex ? 'dragFromRight' : 'dragFromLeft') - }; + if (targetIndex !== undefined && this.dragInitialIndex !== undefined) { + this.targetColumn.set(this.renderedColumns()[targetIndex]); + if (this.dragInitialIndex !== targetIndex) { + this.targetMarkerContext.set({ + class: `targetMarker ${this.dragInitialIndex > targetIndex ? 'dragFromRight' : 'dragFromLeft'}` + }); + } } + this.dragTargetIndex = targetIndex; } + + event.element.style.transform = `translateX(${event.currentX - event.initialX}px)`; } - getColumn(index: number): any { - const _columnsByPin = this._columnsByPin(); - const leftColumnCount = _columnsByPin[0].columns.length; - if (index < leftColumnCount) { - return _columnsByPin[0].columns[index]; + onDragEnd(event: DragEvent): void { + event.element.style.transform = ''; + + const targetIndex = this.getDragTargetIndex(event); + if (this.dragTargetIndex !== undefined) { + this.targetColumn.set(undefined); + this.targetMarkerContext.set(undefined); } - const centerColumnCount = _columnsByPin[1].columns.length; - if (index < leftColumnCount + centerColumnCount) { - return _columnsByPin[1].columns[index - leftColumnCount]; + if (event.model && this.dragInitialIndex !== undefined && targetIndex !== undefined) { + this.reorder.emit({ + prevValue: this.dragInitialIndex, + newValue: targetIndex, + column: event.model + }); } - return _columnsByPin[2].columns[index - leftColumnCount - centerColumnCount]; + this.dragInitialIndex = undefined; + this.dragTargetIndex = undefined; + } + + private getDragTargetIndex({ currentX, currentY, element }: DragEvent): number | undefined { + const elementsAtPoint = this.document.elementsFromPoint(currentX, currentY); + const index = this.headerCells().findIndex( + cell => cell.nativeElement !== element && elementsAtPoint.includes(cell.nativeElement) + ); + return index === -1 ? undefined : index; } onSort({ column, prevValue, newValue }: InnerSortEvent): void { @@ -264,9 +289,6 @@ export class DataTableHeaderComponent { calcStylesByGroup(group: 'center' | 'right' | 'left'): NgStyle['ngStyle'] { const widths = this._columnGroupWidths(); - - return { - width: `${widths[group]}px` - }; + return { width: `${widths[group]}px` }; } } diff --git a/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts b/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts index 9f397a904..e2396e4e2 100644 --- a/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts +++ b/projects/ngx-datatable/src/lib/directives/datatable-draggable.directive.ts @@ -20,6 +20,8 @@ export interface DragEvent { initialY: number; currentX: number; currentY: number; + element: HTMLElement; + model?: TableColumnInternal; } @Directive({ @@ -38,13 +40,15 @@ export class DatatableDraggableDirective implements OnDestroy { readonly dragStartDelay = input(0, { transform: numberAttribute }); readonly enabled = input(true, { transform: booleanAttribute, alias: 'datatableDraggable' }); readonly dragMove = output(); - readonly dragEnd = output(); - readonly dragStart = output(); + readonly dragEnd = output(); + readonly dragStart = output(); private timeoutId?: number; private touchId?: number; private readonly startX = signal(undefined); private readonly startY = signal(undefined); + private currentX?: number; + private currentY?: number; protected readonly isLongPressing = computed( () => this.dragStartDelay() !== 0 && this.isDragging() ); @@ -130,20 +134,20 @@ export class DatatableDraggableDirective implements OnDestroy { private starting(clientX: number, clientY: number): void { this.startX.set(clientX); this.startY.set(clientY); - this.dragStart.emit(); + this.currentX = clientX; + this.currentY = clientY; + this.dragStart.emit(this.dragEvent()); } private moving(clientX: number, clientY: number): void { - this.dragMove.emit({ - initialX: this.startX()!, - initialY: this.startY()!, - currentX: clientX, - currentY: clientY - }); + this.currentX = clientX; + this.currentY = clientY; + this.dragMove.emit(this.dragEvent()); } private ending = (): void => { const dragged = this.isDragging(); + const dragEvent = dragged ? this.dragEvent() : undefined; this.document.removeEventListener('mousemove', this.mousemove); this.document.removeEventListener('touchmove', this.touchmove); this.document.removeEventListener('mouseup', this.ending); @@ -156,10 +160,21 @@ export class DatatableDraggableDirective implements OnDestroy { // In that case, we don't want to emit dragEnd. if (dragged) { this.setDragging(false); - this.dragEnd.emit(); + this.dragEnd.emit(dragEvent!); } }; + private dragEvent(): DragEvent { + return { + initialX: this.startX()!, + initialY: this.startY()!, + currentX: this.currentX!, + currentY: this.currentY!, + element: this.element, + model: this.dragModel() + }; + } + private setDragging(dragging: boolean): void { const model = this.dragModel(); if (model) { diff --git a/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts b/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts index 47b69fcb7..1709616bd 100644 --- a/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts +++ b/projects/ngx-datatable/src/lib/directives/draggable.directive.spec.ts @@ -15,7 +15,7 @@ import { DraggableHarness } from './testing/draggable.harness'; [datatableDraggable]="enabled()" [dragStartDelay]="dragStartDelay()" (dragStart)="dragStart()" - (dragEnd)="dragEnd()" + (dragEnd)="dragEnd($event)" (dragMove)="dragMove($event)" >
` @@ -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;