@supermemo/ng2-dragula
Version:
Simple drag and drop with dragula
249 lines (224 loc) • 7.66 kB
text/typescript
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 });
()
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 (() 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);
}
}