UNPKG

ng-reorder

Version:

Sort elements within a list by using drag-n-drop interface without any restrictions by direction

581 lines (566 loc) 20 kB
import { Injectable, Inject, NgZone, InjectionToken, Directive, SkipSelf, ElementRef, EventEmitter, Input, Output, ContentChildren, Self, NgModule } from '@angular/core'; import { Subject, Subscription } from 'rxjs'; import { DOCUMENT } from '@angular/common'; import { first, takeUntil, tap } from 'rxjs/operators'; class EventService { constructor(_document, _zone) { this._document = _document; this._zone = _zone; this.move = new Subject(); this.up = new Subject(); this.scroll = new Subject(); this._globalListeners = new Map(); } applyGlobalListeners(event) { const isMouse = event.type.startsWith('mouse'); const moveEvent = isMouse ? 'mousemove' : 'touchmove'; const endEvent = isMouse ? 'mouseup' : 'touchend'; this._globalListeners .set('scroll', { func: (e) => { this.scroll.next(e); }, options: true }) .set('selectstart', { func: (e) => { e.preventDefault(); }, options: false }) .set(moveEvent, { func: (e) => { this.move.next(e); e.preventDefault(); }, options: { passive: false } }) .set(endEvent, { func: e => { this.up.next(e); }, options: true }); this._zone.runOutsideAngular(() => { this._globalListeners.forEach((handler, e) => { this._document.addEventListener(e, handler.func, handler.options); }); }); } removeGlobalListeners() { this._zone.runOutsideAngular(() => { this._globalListeners.forEach((handler, event) => { this._document.removeEventListener(event, handler.func, handler.options); }); }); } } EventService.decorators = [ { type: Injectable } ]; EventService.ctorParameters = () => [ { type: HTMLElement, decorators: [{ type: Inject, args: [DOCUMENT,] }] }, { type: NgZone } ]; /** * @param array array to reorder * @param begin the index which is moving in the array * @param end the index where the array[begin] is moving to */ function reorderItems(array, begin, end) { if (array.length === 0) { return; } begin = fit(begin, array.length - 1); end = fit(end, array.length - 1); if (begin === end || end === -1 || begin === -1) { return array; } const shift = begin < end ? 1 : -1; const anchor = array[begin]; for (let i = begin; i !== end; i += shift) { array[i] = array[i + shift]; } array[end] = anchor; return array; } /** * @param array array to reorder * @param a the index of first element * @param b the index of second element */ function swapItems(array, a, b) { [array[a], array[b]] = [array[b], array[a]]; return array; } /** To ensure to get a number not less than zero and not greater than a given max value * @param value number to check * @param max max value */ function fit(value, max) { if (isNaN(value) || value === null) { return -1; } return Math.max(0, Math.min(value, max)); } /** Whether the event is touch or not */ function isTouchEvent(event) { return event.type[0] === 't'; } /** Returns { 0, 0 } point */ function createPoint() { return { x: 0, y: 0 }; } /** * Returns the sum of two points * @param first first point * @param second second point */ function pointSum(first, second) { return { x: first.x + second.x, y: first.y + second.y }; } /** * Returns the difference between two points * @param first first point * @param second second point */ function pointDif(first, second) { return { x: first.x - second.x, y: first.y - second.y }; } /** * Returns point from pointer event * @param event Mouse or Touch event */ function pointFromPointerEvent(event) { const $ = isTouchEvent(event) ? event.targetTouches[0] : event; return { x: $.clientX, y: $.clientY }; } /** * Returns time of a transition of a speciphic property in miliseconds. * @param element DOM element to check * @param property required CSS property * @param includeAll include or not the "all" property. By default true */ function transitionTimeOf(element, property, includeAll = true) { const style = getComputedStyle(element); const properties = style.transitionProperty.split(','); // filter for the 'all' property const target = ($) => { if (includeAll) { return $ === property || $ === 'all'; } else { return $ === property; } }; const foundProperty = properties.find(target); // If no fouded property returns zero if (!foundProperty) { return 0; } const index = properties.indexOf(foundProperty); let delay = style.transitionDelay.split(',')[index]; let duration = style.transitionDuration.split(',')[index]; // Destructuring assignment. // Next lines check whether the values in ms and return parsed time in ms [delay, duration] = [delay, duration].map($ => { const k = $.toLowerCase().indexOf('ms') !== -1 ? 1 : 1000; return parseFloat($) * k; }); return delay + duration; } const DRAG_UNIT_PARENT = new InjectionToken('Parent'); class DragHandleDirective { constructor(_parent, _host) { this._parent = _parent; this._host = _host; } /** @returns true if the hanlde is equal to an element */ is(element) { return this._host.nativeElement === element || this._host.nativeElement.contains(element); } } DragHandleDirective.decorators = [ { type: Directive, args: [{ selector: '[dragHandle]' },] } ]; DragHandleDirective.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [DRAG_UNIT_PARENT,] }, { type: SkipSelf }] }, { type: ElementRef } ]; class DragRejectorDirective { constructor(_parent, _host) { this._parent = _parent; this._host = _host; } /** @returns true if the rejector is equal to an element */ is(element) { return this._host.nativeElement === element || this._host.nativeElement.contains(element); } } DragRejectorDirective.decorators = [ { type: Directive, args: [{ selector: '[dragRejector]' },] } ]; DragRejectorDirective.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [DRAG_UNIT_PARENT,] }, { type: SkipSelf }] }, { type: ElementRef } ]; const DRAG_COLLECTION = new InjectionToken('Container'); class DragUnitDirective { constructor(container, eventService, _host, _zone) { this.container = container; this.eventService = eventService; this._host = _host; this._zone = _zone; /** Emits when the element is successfully touched */ this.unitTaken = new EventEmitter(); /** Emits when the element is released */ this.unitReleased = new EventEmitter(); /** Emits when the element is moved on the page */ this.unitMoved = new EventEmitter(); // Indicate if the element is dragging or not this._active = false; // Indicate if the element is dropped this._droppped = false; /** Emits when the element is destroyed */ this._destroy = new Subject(); this._moveSubscribtion = Subscription.EMPTY; this._upSubscription = Subscription.EMPTY; this._scrollSubscription = Subscription.EMPTY; this._origin = this._offset = this._scrollOrigin = this._scrollOffset = createPoint(); } get active() { return this._active; } get dropped() { return this._droppped; } get disabled() { return this._disabled; } ngAfterViewInit() { ['mousedown', 'touchstart'].forEach((e) => { this._host.nativeElement.addEventListener(e, this._pointerDown.bind(this), { passive: false, capture: true }); }); } ngOnDestroy() { this._destroy.next(); this._destroy.complete(); } applyTransformation(shift) { if (shift.x === 0 && shift.y === 0) { this._host.nativeElement.style.transform = ''; return; } this._host.nativeElement.style.transform = `translate(${shift.x}px, ${shift.y}px)`; } getRect() { return this._host.nativeElement.getBoundingClientRect(); } reset() { this._host.nativeElement.style.transform = null; this._active = false; this._droppped = false; this._offset = this._origin = this._scrollOrigin = this._scrollOffset = this._pointerPosition = { x: 0, y: 0 }; } setOffset(point) { this._offset = point; } _animateDroppedElement() { return new Promise((resolve, reject) => { this._zone.onStable.asObservable().pipe(first()).subscribe(resolve); }); } _initDragSequence(event) { this.eventService.applyGlobalListeners(event); this._scrollOrigin = { x: document.defaultView.scrollX, y: document.defaultView.scrollY }; this._moveSubscribtion = this.eventService.move .pipe(takeUntil(this._destroy), tap((e) => { this._zone.run(() => this._pointerMove(e)); })).subscribe(); this._upSubscription = this.eventService.up .pipe(takeUntil(this._destroy), tap((e) => { this._zone.run(() => this._pointerUp(e)); })).subscribe(); this._scrollSubscription = this.eventService.scroll .pipe(takeUntil(this._destroy), tap((e) => { this._zone.run(() => this._viewScroll()); })).subscribe(); return; } _viewScroll() { const view = document.defaultView; const currentScroll = { x: view.scrollX, y: view.scrollY }; this._scrollOffset = pointDif(currentScroll, this._scrollOrigin); const point = pointSum(this._pointerPosition, this._scrollOffset); this.applyTransformation(pointDif(point, this._origin)); } _startDragSequence(event) { if (this.container.disabled || this._disabled) return; event.stopPropagation(); this._initDragSequence(event); this._origin = this._pointerPosition = pointFromPointerEvent(event); this._active = true; this.container.start(this, this._origin); this.unitTaken.emit({ unit: this, event }); } _stopDragSequence() { this._moveSubscribtion.unsubscribe(); this._upSubscription.unsubscribe(); this._scrollSubscription.unsubscribe(); const delay = transitionTimeOf(this._host.nativeElement, 'transform'); setTimeout(() => { this.container.stop(this); this.eventService.removeGlobalListeners(); }, delay * 1.5); } _pointerDown(event) { if (this.container.inAction) return; const target = event.target; if (this._rejectors.length && this._rejectors.find(rejector => rejector.is(target))) return; else if (this._handles.length && !this._handles.find(handle => handle.is(target))) return; else if (true) this._zone.run(() => this._startDragSequence(event)); return; } _pointerMove(event) { this._pointerPosition = pointFromPointerEvent(event); const point = pointSum(this._pointerPosition, this._scrollOffset); this.applyTransformation(pointDif(point, this._origin)); this.container.moveUnitTo(this, point); this.unitMoved.emit({ unit: this, distance: pointDif(point, this._origin), event }); } _pointerUp(event) { this._active = false; this.applyTransformation(this._offset); this._droppped = true; this._animateDroppedElement().then(this._stopDragSequence.bind(this)); this.unitReleased.emit({ unit: this, event }); } } DragUnitDirective.decorators = [ { type: Directive, args: [{ selector: '[dragUnit]', providers: [ { provide: DRAG_UNIT_PARENT, useExisting: DragUnitDirective } ], host: { '[class.drag-unit]': 'true', '[class.active]': 'active', '[class.dropped]': 'dropped', '[class.disabled]': 'disabled' } },] } ]; DragUnitDirective.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [DRAG_COLLECTION,] }, { type: SkipSelf }] }, { type: EventService, decorators: [{ type: SkipSelf }] }, { type: ElementRef }, { type: NgZone } ]; DragUnitDirective.propDecorators = { _disabled: [{ type: Input, args: ['disabled',] }], unitTaken: [{ type: Output }], unitReleased: [{ type: Output }], unitMoved: [{ type: Output }], _handles: [{ type: ContentChildren, args: [DragHandleDirective, { descendants: true },] }], _rejectors: [{ type: ContentChildren, args: [DragRejectorDirective, { descendants: true },] }] }; class SortService { constructor() { this._listOfPositions = null; this._from = null; this._to = null; } cachePosition(unit) { this._listOfPositions.push(this.getPosition(unit)); } findIndex(point) { for (const $ of this._listOfPositions) { const left = point.x > $.rect.left + $.shift.x; const right = point.x < $.rect.right + $.shift.x; const top = point.y > $.rect.top + $.shift.y; const bottom = point.y < $.rect.bottom + $.shift.y; if (left && right && top && bottom) { return this._listOfPositions.indexOf($); } } return -1; } getPosition(unit) { return { unit, rect: unit.getRect(), shift: createPoint() }; } initService(container) { this._root = container; } cacheAllPositions() { this._listOfPositions = new Array(); this._root.units.forEach(unit => this.cachePosition(unit)); } moveUnits(unit, point) { const elements = this._listOfPositions; const oldIndex = elements.map(i => i.unit).indexOf(unit); const newIndex = this.findIndex(point); if (oldIndex === -1 || newIndex === -1 || newIndex === oldIndex || this._from === null) { return; } this._to = newIndex; const newPosition = { x: elements[newIndex].rect.left + elements[newIndex].shift.x, y: elements[newIndex].rect.top + elements[newIndex].shift.y }; const step = newIndex > oldIndex ? -1 : 1; for (let current = newIndex; current !== oldIndex; current += step) { const next = current + step; const shift = { x: (elements[next].rect.left + elements[next].shift.x) - (elements[current].rect.left + elements[current].shift.x), y: (elements[next].rect.top + elements[next].shift.y) - (elements[current].rect.top + elements[current].shift.y) }; elements[current].shift.x += shift.x; elements[current].shift.y += shift.y; elements[current].unit.applyTransformation(elements[current].shift); } elements[oldIndex].shift.x = (newPosition.x - elements[oldIndex].rect.left); elements[oldIndex].shift.y = (newPosition.y - elements[oldIndex].rect.top); unit.setOffset(elements[oldIndex].shift); this._listOfPositions = elements; this._listOfPositions = reorderItems(this._listOfPositions, oldIndex, newIndex); } start(units, point, from) { this.cacheAllPositions(); this._from = from; } stop(unit) { return new Promise((resolve) => { const $ = { collection: this._root, unit, previousIndex: this._from, currentIndex: this._to }; resolve($); this._listOfPositions = null; this._from = null; this._to = null; }); } } SortService.decorators = [ { type: Injectable } ]; SortService.ctorParameters = () => []; class DragCollectionDirective { constructor(_zone, sortService) { this._zone = _zone; this.sortService = sortService; this.dropCompleted = new EventEmitter(); this._activeItem = null; } get inAction() { return this._activeItem !== null; } get disabled() { return this._disabled; } ngAfterViewInit() { this.sortService.initService(this); } clearChildren() { this._zone.run(() => { this.units.toArray().forEach(unit => unit.reset()); }); } moveUnitTo(unit, point) { this.sortService.moveUnits(unit, point); } start(unit, point) { const units = this.units.toArray(); const from = units.indexOf(unit); this.sortService.start(units, point, from); this._activeItem = unit; } stop(unit) { this.clearChildren(); this.sortService.stop(unit).then((e) => { if (e.currentIndex !== null) this.dropCompleted.emit(e); }); this._activeItem = null; } } DragCollectionDirective.decorators = [ { type: Directive, args: [{ selector: '[dragCollection]', providers: [ { provide: DRAG_COLLECTION, useExisting: DragCollectionDirective }, EventService, SortService ], host: { '[class.collection]': 'true', '[class.in-action]': 'inAction', '[class.disabled]': 'disabled' } },] } ]; DragCollectionDirective.ctorParameters = () => [ { type: NgZone }, { type: SortService, decorators: [{ type: Self }] } ]; DragCollectionDirective.propDecorators = { _disabled: [{ type: Input, args: ['disabled',] }], dropCompleted: [{ type: Output }], units: [{ type: ContentChildren, args: [DragUnitDirective,] }] }; class NgReorderModule { } NgReorderModule.decorators = [ { type: NgModule, args: [{ declarations: [ DragCollectionDirective, DragUnitDirective, DragHandleDirective, DragRejectorDirective, ], imports: [], exports: [DragCollectionDirective, DragUnitDirective, DragHandleDirective, DragRejectorDirective] },] } ]; /* * Public API Surface of ng-reorder */ /** * Generated bundle index. Do not edit. */ export { DragCollectionDirective, DragHandleDirective, DragRejectorDirective, DragUnitDirective, NgReorderModule, createPoint, pointDif, pointFromPointerEvent, pointSum, reorderItems, DRAG_COLLECTION as ɵa, EventService as ɵc, SortService as ɵd, DRAG_UNIT_PARENT as ɵe }; //# sourceMappingURL=ng-reorder.js.map