UNPKG

ng2-dragula

Version:
557 lines (547 loc) 22.3 kB
import * as i0 from '@angular/core'; import { Injectable, Optional, EventEmitter, Directive, Input, Output, NgModule } from '@angular/core'; import { Subject, Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import * as dragulaExpt from 'dragula'; class Group { constructor(name, drake, options) { this.name = name; this.drake = drake; this.options = options; this.initEvents = false; } } var EventTypes; (function (EventTypes) { EventTypes["Cancel"] = "cancel"; EventTypes["Cloned"] = "cloned"; EventTypes["Drag"] = "drag"; EventTypes["DragEnd"] = "dragend"; EventTypes["Drop"] = "drop"; EventTypes["Out"] = "out"; EventTypes["Over"] = "over"; EventTypes["Remove"] = "remove"; EventTypes["Shadow"] = "shadow"; EventTypes["DropModel"] = "dropModel"; EventTypes["RemoveModel"] = "removeModel"; })(EventTypes || (EventTypes = {})); const AllEvents = Object.keys(EventTypes).map(k => EventTypes[k]); const dragula = dragulaExpt.default || dragulaExpt; class DrakeFactory { constructor(build = dragula) { this.build = build; } } const filterEvent = (eventType, filterDragType, projector) => (input) => { return input.pipe(filter(({ event, name }) => { return (event === eventType && (filterDragType === undefined || name === filterDragType)); }), map(({ name, args }) => projector(name, args))); }; const elContainerSourceProjector = (name, [el, container, source]) => ({ name, el, container, source }); class DragulaService { constructor(drakeFactory) { this.drakeFactory = drakeFactory; this.groups = {}; this.dispatch$ = new Subject(); this.elContainerSource = (eventType) => (groupName) => this.dispatch$.pipe(filterEvent(eventType, groupName, elContainerSourceProjector)); /* https://github.com/bevacqua/dragula#drakeon-events */ // eslint-disable-next-line @typescript-eslint/member-ordering this.cancel = this.elContainerSource(EventTypes.Cancel); // eslint-disable-next-line @typescript-eslint/member-ordering this.remove = this.elContainerSource(EventTypes.Remove); // eslint-disable-next-line @typescript-eslint/member-ordering this.shadow = this.elContainerSource(EventTypes.Shadow); // eslint-disable-next-line @typescript-eslint/member-ordering this.over = this.elContainerSource(EventTypes.Over); // eslint-disable-next-line @typescript-eslint/member-ordering this.out = this.elContainerSource(EventTypes.Out); this.drag = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.Drag, groupName, (name, [el, source]) => ({ name, el, source }))); this.dragend = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.DragEnd, groupName, (name, [el]) => ({ name, el, }))); this.drop = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.Drop, groupName, (name, [el, target, source, sibling]) => { return { name, el, target, source, sibling }; })); this.cloned = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.Cloned, groupName, (name, [clone, original, cloneType]) => { return { name, clone, original, cloneType }; })); this.dropModel = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.DropModel, groupName, (name, [el, target, source, sibling, item, sourceModel, targetModel, sourceIndex, targetIndex,]) => { return { name, el, target, source, sibling, item, sourceModel, targetModel, sourceIndex, targetIndex, }; })); this.removeModel = (groupName) => this.dispatch$.pipe(filterEvent(EventTypes.RemoveModel, groupName, (name, [el, container, source, item, sourceModel, sourceIndex]) => { return { name, el, container, source, item, sourceModel, sourceIndex, }; })); if (this.drakeFactory === null || this.drakeFactory === undefined) { this.drakeFactory = new DrakeFactory(); } } /** Public mainly for testing purposes. Prefer `createGroup()`. */ add(group) { const 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; } find(name) { return this.groups[name]; } destroy(name) { const 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` */ createGroup(name, options) { return this.add(new Group(name, this.drakeFactory.build([], options), options)); } handleModels({ name, drake, options }) { let dragElm; let dragIndex; let dropIndex; drake.on('remove', (el, container, source) => { 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]; this.dispatch$.next({ event: EventTypes.RemoveModel, name, args: [el, container, source, item, sourceModel, dragIndex], }); }); drake.on('drag', (el, source) => { if (!drake.models) { return; } dragElm = el; dragIndex = this.domIndexOf(el, source); }); drake.on('drop', (dropElm, target, source, sibling) => { 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)]; let item; 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 { const 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); // eslint-disable-next-line no-empty } catch (e) { } } } this.dispatch$.next({ event: EventTypes.DropModel, name, args: [ dropElm, target, source, sibling, item, sourceModel, targetModel, dragIndex, dropIndex, ], }); }); } setupEvents(group) { if (group.initEvents) { return; } group.initEvents = true; const name = group.name; // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; const emitter = (event) => { switch (event) { case EventTypes.Drag: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.Drop: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.DragEnd: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.Cancel: case EventTypes.Remove: case EventTypes.Shadow: case EventTypes.Over: case EventTypes.Out: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.Cloned: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.DropModel: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; case EventTypes.RemoveModel: group.drake.on(event, (...args) => { this.dispatch$.next({ event, name, args }); }); break; default: break; } }; AllEvents.forEach(emitter); } domIndexOf(child, parent) { if (parent) { return Array.prototype.indexOf.call(parent.children, child); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaService, deps: [{ token: DrakeFactory, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: DrakeFactory, decorators: [{ type: Optional }] }]; } }); class DragulaDirective { get container() { return this.el && this.el.nativeElement; } constructor(el, dragulaService) { this.el = el; this.dragulaService = dragulaService; this.dragulaModelChange = new EventEmitter(); } ngOnChanges(changes) { if (changes && changes.dragula) { const { previousValue: prev, currentValue: current, firstChange } = changes.dragula; const hadPreviousValue = !!prev; const hasNewValue = !!current; // something -> null => teardown only // something -> something => teardown, then setup // null -> something => setup only // // null -> null (precluded by fact of change being present) if (hadPreviousValue) { this.teardown(prev); } if (hasNewValue) { this.setup(); } } else if (changes && changes.dragulaModel) { // this code only runs when you're not changing the group name // because if you're changing the group name, you'll be doing setup or teardown // it also only runs if there is a group name to attach to. const { previousValue: prev, currentValue: current, firstChange } = changes.dragulaModel; const drake = this.group?.drake; if (this.dragula && drake) { drake.models = drake.models || []; const prevIndex = drake.models.indexOf(prev); if (prevIndex !== -1) { // delete the previous drake.models.splice(prevIndex, 1); // maybe insert a new one at the same spot if (current) { drake.models.splice(prevIndex, 0, current); } } else if (current) { // no previous one to remove; just push this one. drake.models.push(current); } } } } // call ngOnInit 'setup' because we want to call it in ngOnChanges // and it would otherwise run twice setup() { const checkModel = (group) => { if (this.dragulaModel) { if (group.drake?.models) { group.drake?.models?.push(this.dragulaModel); } else { if (group.drake) { group.drake.models = [this.dragulaModel]; } } } }; // find or create a group if (!this.dragula) { return; } let group = this.dragulaService.find(this.dragula); if (!group) { const options = {}; group = this.dragulaService.createGroup(this.dragula, options); } // ensure model and container element are pushed checkModel(group); group.drake?.containers.push(this.container); this.subscribe(this.dragula); this.group = group; } subscribe(name) { this.subs = new Subscription(); this.subs.add(this.dragulaService .dropModel(name) .subscribe(({ source, target, sourceModel, targetModel }) => { if (source === this.el.nativeElement) { this.dragulaModelChange.emit(sourceModel); } else if (target === this.el.nativeElement) { this.dragulaModelChange.emit(targetModel); } })); this.subs.add(this.dragulaService .removeModel(name) .subscribe(({ source, sourceModel }) => { if (source === this.el.nativeElement) { this.dragulaModelChange.emit(sourceModel); } })); } teardown(groupName) { if (this.subs) { this.subs.unsubscribe(); } const group = this.dragulaService.find(groupName); if (group) { const itemToRemove = group.drake?.containers.indexOf(this.el.nativeElement); if (itemToRemove !== -1) { group.drake?.containers.splice(itemToRemove, 1); } if (this.dragulaModel && group.drake && group.drake.models) { const modelIndex = group.drake.models.indexOf(this.dragulaModel); if (modelIndex !== -1) { group.drake.models.splice(modelIndex, 1); } } } } ngOnDestroy() { if (!this.dragula) { return; } this.teardown(this.dragula); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaDirective, deps: [{ token: i0.ElementRef }, { token: DragulaService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.5", type: DragulaDirective, selector: "[dragula]", inputs: { dragula: "dragula", dragulaModel: "dragulaModel" }, outputs: { dragulaModelChange: "dragulaModelChange" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaDirective, decorators: [{ type: Directive, args: [{ selector: '[dragula]' }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: DragulaService }]; }, propDecorators: { dragula: [{ type: Input }], dragulaModel: [{ type: Input }], dragulaModelChange: [{ type: Output }] } }); class DragulaModule { static forRoot() { return { ngModule: DragulaModule, providers: [DragulaService] }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.1.5", ngImport: i0, type: DragulaModule, declarations: [DragulaDirective], exports: [DragulaDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaModule, providers: [DragulaService] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.5", ngImport: i0, type: DragulaModule, decorators: [{ type: NgModule, args: [{ exports: [DragulaDirective], declarations: [DragulaDirective], providers: [DragulaService] }] }] }); const MockDrakeFactory = new DrakeFactory((containers, options) => { return new MockDrake(containers, options); }); /** You can use MockDrake to simulate Drake events. * * The three methods that actually do anything are `on(event, listener)`, * `destroy()`, and a new method, `emit()`. Use `emit()` to manually emit Drake * events, and if you injected MockDrake properly with MockDrakeFactory or * mocked the DragulaService.find() method, then you can make ng2-dragula think * drags and drops are happening. * * Caveats: * * 1. YOU MUST MAKE THE DOM CHANGES YOURSELF. * 2. REPEAT: YOU MUST MAKE THE DOM CHANGES YOURSELF. * That means `source.removeChild(el)`, and `target.insertBefore(el)`. * 3. None of the other methods do anything. * That's ok, because ng2-dragula doesn't use them. */ class MockDrake { /** * @param containers A list of container elements. * @param options These will NOT be used. At all. * @param models Nonstandard, but useful for testing using `new MockDrake()` directly. * Note, default value is undefined, like a real Drake. Don't change that. */ constructor(containers = [], options = {}, models) { this.containers = containers; this.options = options; this.models = models; // Basic but fully functional event emitter shim this.emitter$ = new Subject(); this.subs = new Subscription(); /* Doesn't represent anything meaningful. */ this.dragging = false; } on(event, callback) { this.subs.add(this.emitter$ .pipe(filter(({ eventType }) => eventType === event)) .subscribe(({ eventType, args }) => { if (eventType === EventTypes.Drag) { const argument = Array.from(args); const el = argument[0]; const source = argument[1]; //@ts-ignore callback(el, source); return; } if (eventType === EventTypes.Drop) { const argument = Array.from(args); const el = argument[0]; const target = argument[1]; const source = argument[2]; const sibling = argument[3]; //@ts-ignore callback(el, target, source, sibling); return; } if (eventType === EventTypes.Remove) { const argument = Array.from(args); const el = argument[0]; const container = argument[1]; const source = argument[2]; //@ts-ignore callback(el, container, source); return; } callback(args); })); } /* Does nothing useful. */ start(item) { this.dragging = true; } /* Does nothing useful. */ end() { this.dragging = false; } cancel(revert) { this.dragging = false; } /* Does nothing useful. */ canMove(item) { return this.options.accepts ? this.options.accepts(item) : false; } /* Does nothing useful. */ remove() { this.dragging = false; } destroy() { this.subs.unsubscribe(); } /** * This is the most useful method. You can use it to manually fire events that would normally * be fired by a real drake. * * You're likely most interested in firing `drag`, `remove` and `drop`, the three events * DragulaService uses to implement [dragulaModel]. * * See https://github.com/bevacqua/dragula#drakeon-events for what you should emit (and in what order). * * (Note also, firing dropModel and removeModel won't work. You would have to mock DragulaService for that.) */ emit(eventType, ...args) { this.emitter$.next({ eventType, args }); } } /** * Generated bundle index. Do not edit. */ export { DragulaDirective, DragulaModule, DragulaService, DrakeFactory, EventTypes, Group, MockDrake, MockDrakeFactory, dragula }; //# sourceMappingURL=ng2-dragula.mjs.map