UNPKG

@supermemo/ng2-dragula

Version:

Simple drag and drop with dragula

249 lines (224 loc) 7.66 kB
import { Injectable, Optional } from '@angular/core'; import { Group } from '../Group'; import { DragulaOptions } from '../DragulaOptions'; import { Subject, Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { EventTypes, AllEvents } from '../EventTypes'; import { DrakeFactory } from '../DrakeFactory'; type FilterProjector<T extends { name: string; }> = (name: string, args: any[]) => T; type Dispatch = { event: EventTypes; name: string; args: any[]; }; const filterEvent = <T extends { name: string; }>( eventType: EventTypes, filterDragType: string | undefined, projector: FilterProjector<T> ) => (input: Observable<Dispatch>): Observable<T> => { return input.pipe( filter(({ event, name }) => { return event === eventType && (filterDragType === undefined || name === filterDragType); }), map(({ name, args }) => projector(name, args)) ); } const elContainerSourceProjector = (name: string, [el, container, source]: [Element, Element, Element]) => ({ name, el, container, source }); @Injectable() export class DragulaService { /* https://github.com/bevacqua/dragula#drakeon-events */ private dispatch$ = new Subject<Dispatch>(); public drag = (groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.Drag, groupName, (name, [el, source]: [Element, Element]) => ({ name, el, source }) ) ); public dragend = (groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.DragEnd, groupName, (name, [el]: [Element]) => ({ name, el }) ) ); public drop = (groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.Drop, groupName, (name, [ el, target, source, sibling ]: [Element, Element, Element, Element]) => { return { name, el, target, source, sibling }; }) ); private elContainerSource = (eventType: EventTypes) => (groupName?: string) => this.dispatch$.pipe( filterEvent(eventType, groupName, elContainerSourceProjector) ); public cancel = this.elContainerSource(EventTypes.Cancel); public remove = this.elContainerSource(EventTypes.Remove); public shadow = this.elContainerSource(EventTypes.Shadow); public over = this.elContainerSource(EventTypes.Over); public out = this.elContainerSource(EventTypes.Out); public cloned = (groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.Cloned, groupName, (name, [ clone, original, cloneType ]: [Element, Element, 'mirror' | 'copy']) => { return { name, clone, original, cloneType } }) ); public dropModel = <T = any>(groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.DropModel, groupName, (name, [ el, target, source, sibling, item, sourceModel, targetModel, sourceIndex, targetIndex ]: [Element, Element, Element, Element, T, T[], T[], number, number]) => { return { name, el, target, source, sibling, item, sourceModel, targetModel, sourceIndex, targetIndex } }) ); public removeModel = <T = any>(groupName?: string) => this.dispatch$.pipe( filterEvent( EventTypes.RemoveModel, groupName, (name, [ el, container, source, item, sourceModel, sourceIndex ]: [Element, Element, Element, T, T[], number]) => { return { name, el, container, source, item, sourceModel, sourceIndex } } ) ); private groups: { [k: string]: Group } = {}; constructor (@Optional() private drakeFactory: DrakeFactory = null) { if (this.drakeFactory === null) { this.drakeFactory = new DrakeFactory(); } } /** Public mainly for testing purposes. Prefer `createGroup()`. */ public add(group: Group): Group { let existingGroup = this.find(group.name); if (existingGroup) { throw new Error('Group named: "' + group.name + '" already exists.'); } this.groups[group.name] = group; this.handleModels(group); this.setupEvents(group); return group; } public find(name: string): Group { return this.groups[name]; } public destroy(name: string): void { let group = this.find(name); if (!group) { return; } group.drake && group.drake.destroy(); delete this.groups[name]; } /** * Creates a group with the specified name and options. * * Note: formerly known as `setOptions` */ public createGroup<T = any>(name: string, options: DragulaOptions<T>): Group { return this.add(new Group( name, this.drakeFactory.build([], options), options )); } private handleModels({ name, drake, options }: Group): void { let dragElm: any; let dragIndex: number; let dropIndex: number; drake.on('remove', (el: any, container: any, source: any) => { if (!drake.models) { return; } let sourceModel = drake.models[drake.containers.indexOf(source)]; sourceModel = sourceModel.slice(0); // clone it const item = sourceModel.splice(dragIndex, 1)[0]; // console.log('REMOVE'); // console.log(sourceModel); this.dispatch$.next({ event: EventTypes.RemoveModel, name, args: [ el, container, source, item, sourceModel, dragIndex ] }); }); drake.on('drag', (el: any, source: any) => { if (!drake.models) { return; } dragElm = el; dragIndex = this.domIndexOf(el, source); }); drake.on('drop', (dropElm: any, target: Element, source: Element, sibling?: Element) => { if (!drake.models || !target) { return; } dropIndex = this.domIndexOf(dropElm, target); let sourceModel = drake.models[drake.containers.indexOf(source)]; let targetModel = drake.models[drake.containers.indexOf(target)]; // console.log('DROP'); // console.log(sourceModel); let item: any; if (target === source) { sourceModel = sourceModel.slice(0) item = sourceModel.splice(dragIndex, 1)[0]; sourceModel.splice(dropIndex, 0, item); // this was true before we cloned and updated sourceModel, // but targetModel still has the old value targetModel = sourceModel; } else { let isCopying = dragElm !== dropElm; item = sourceModel[dragIndex]; if (isCopying) { if (!options.copyItem) { throw new Error("If you have enabled `copy` on a group, you must provide a `copyItem` function.") } item = options.copyItem(item); } if (!isCopying) { sourceModel = sourceModel.slice(0) sourceModel.splice(dragIndex, 1); } targetModel = targetModel.slice(0) targetModel.splice(dropIndex, 0, item); if (isCopying) { try { target.removeChild(dropElm); } catch (e) {} } } this.dispatch$.next({ event: EventTypes.DropModel, name, args: [ dropElm, target, source, sibling, item, sourceModel, targetModel, dragIndex, dropIndex ] }); }); } private setupEvents(group: Group): void { if (group.initEvents) { return; } group.initEvents = true; const name = group.name; let that: any = this; let emitter = (event: EventTypes) => { group.drake.on(event, (...args: any[]) => { this.dispatch$.next({ event, name, args }); }); }; AllEvents.forEach(emitter); } private domIndexOf(child: any, parent: any): any { return Array.prototype.indexOf.call(parent.children, child); } }