UNPKG

@ng-dnd/core

Version:

Drag and Drop for Angular

255 lines (254 loc) 10.7 kB
/** * @module 1-Top-Level */ /** a second comment */ import { DropTargetMonitor } from './target-monitor'; import { DragSourceMonitor } from './source-monitor'; import { TypeOrTypeArray } from './type-ish'; import { DragLayerMonitor } from './layer-monitor'; import { DragSourceOptions, DragPreviewOptions } from './connectors'; import { Observable, TeardownLogic, Subscription, SubscriptionLike } from 'rxjs'; /** * A base type to represent a DOM connection. */ export interface ConnectionBase<TMonitor> extends SubscriptionLike { /** * A connection maintains a subscription to `dnd-core`'s drag state * changes. This function is how you are notified of those changes. * * This function is essentially RxJS `Observable.map` with one small * optimization: it runs the output of * the function you provide through `distinctUntilChanged`, and checks * reference equality (`===`) for scalars and `shallowEqual` for Objects. * * Because of #2, you can happily emulate `react-dnd`-style code like: * * ```typescript * collected$ = this.target.listen(monitor => ({ * isDragging: monitor.isDragging(), * isOver: monitor.isOver(), * canDrop: monitor.canDrop() * })); * ``` * * ... in which case you probably want to use the `*ngIf` as pattern for * grouping subscriptions into one bound template variable: * * ```html * <ng-container *ngIf="collected$ | async as c"> * <p>{{c.isDragging ? 'dragging': null}}<p> * ... * </ng-container> * ``` * * You can also subscribe one-by-one, with `isDragging$ = listen(m => m.isDragging())`. */ listen<O>(mapTo: (monitor: TMonitor) => O): Observable<O>; /** * This method **MUST** be called, however you choose to, when `ngOnDestroy()` fires. * If you don't, you will leave subscriptions hanging around that will fire * callbacks on components that no longer exist. */ unsubscribe(): void; /** * Same as RxJS Subscription.add(). * Useful, for example, for writing wrappers for the {@link DndService} methods, * which might internally listen()/subscribe to {@link DropTargetSpec#hover} and provide * a convenient callback after you hover without dropping or exiting for a specified * duration. That would require the following pattern: * * ```typescript * function wrapper(dndService, types, spec, callback) { * let subj = new Subject(); * let dt = dndService.dropTarget(types, { * ...spec, * hover: monitor => { * subj.next(); * spec.hover?.(monitor); * } * }); * // runs the callback until the returned connection * // is destroyed via unsubscribe() * dt.add(subj.pipe( ... ).subscribe(callback)) * return dt; * } * ``` */ add(teardown: TeardownLogic): void; } /** * Represents one drop target and its behaviour, that can listen to the state * and connect to a DOM element. * * To create one, refer to {@link DndService#dropTarget}. */ export interface DropTarget<Item = unknown, DropResult = unknown> extends ConnectionBase<DropTargetMonitor<Item, DropResult>> { /** * Use this method to have a dynamically typed target. If no type has * previously been set, it creates the subscription and allows the * `[dragSource]` DOM element to be connected. If you do not need to * dynamically update the type, you can set it once via the * {@link DropTargetSpec#types} property. * * See {@link DragSource#setType} for an example of how to set * a dynamic type, for it is very similar here. */ setTypes(type: TypeOrTypeArray): void; /** * This function allows you to connect a DOM node to your `DropTarget`. * You will not usually need to call this directly; * it is more easily handled by the directives. * * The subscription returned is automatically unsubscribed when the connection is made. * This may be immediate if the `DropTarget` already has a type. */ connectDropTarget(elementOrNode: Node): Subscription; /** * Returns the drop target ID that can be used to simulate the drag and drop events * with the testing backend. */ getHandlerId(): any; } /** * Like {@link DropTarget}, it can be used just for subscribing to * drag state information related to a particular item type or list of types. * You do not have to connect it to a DOM element if that's all you want. * * To create one, refer to {@link DndService#dragSource}. */ export interface DragSource<Item, DropResult = unknown> extends ConnectionBase<DragSourceMonitor<Item, DropResult>> { /** * Use this method to have a dynamically typed source. If no type has * previously been set, it creates the subscription and allows the * `[dragSource]` DOM element to be connected. If you do not need to * dynamically update the type, you can set it once via the * {@link DragSourceSpec#type} property. * * If you wish to have a dynamic type based on an `@Input()` property, for * example, you must call `setType()` in either of your component's * `ngOnInit` or `ngOnChanges` methods: * * ```typescript * @Input() type: string; * @Input() model: { parentId: number; name: string; }; * target = this.dnd.dragSource(null, { * // ... * }); * ngOnChanges() { * // use what your parent component told you to * this.target.setType(this.type); * // or create groupings on the fly * this.target.setType("PARENT_" + this.model.parentId.toString()); * } * ``` * * It may be more convenient or easier to understand if you write: * * ```typescript * @Input() set type(t) { * this.source.setType(t); * } * source = this.dnd.dragSource(null, { * beginDrag: () => ({ ... }) * }); * ``` * */ setType(type: string | symbol): void; /** * This function allows you to connect a DOM node to your `DragSource`. * You will not usually need to call this directly; * it is more easily handled by the directives. * * The subscription returned is automatically unsubscribed when the connection is made. * This may be immediate if the `DragSource` already has a type. */ connectDragSource(elementOrNode: Node, options?: DragSourceOptions): Subscription; /** * This function allows you to connect a DOM node to your `DragSource` as a **preview**. * You will not usually need to call this directly; * it is more easily handled by the directives. * * You might use an `ElementRef.nativeElement`, or even an * [`Image`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image). * * ```ts * const img = new Image(); * img.onload = this.source.connectDragPreview(img); * img.src = '...'; * ``` * * The subscription returned is automatically unsubscribed when the connection is made. * This may be immediate if the `DragSource` already has a type. */ connectDragPreview(elementOrNode: Node, options?: DragPreviewOptions): Subscription; /** * Returns the drag source ID that can be used to simulate the drag and drop events * with the testing backend. */ getHandlerId(): any; } /** * For many use cases, the default rendering of the HTML5 backend should suffice. * However, its drag preview has certain limitations. For example, it has to be an * existing node screenshot or an image, and it cannot change midflight. * * Sometimes you might want to perform the custom rendering. This also becomes * necessary if you're using a custom backend. `DragLayer` lets you perform the * rendering of the drag preview yourself. * * A drag layer is a special subscriber to the global drag state. It is called * a 'layer', not just a subscriber, because it is typically used to render custom * elements that follow the mouse, above all other elements. The data flows like * so: * * ``` * drag start => global state => drag source => no preview * => drag layer => preview rendered on the spot * drag moved => global state => drag layer => preview moves * drag ends => global state => drag layer => preview erased * ``` * * To use a drag layer as designed: * * 1. Create a drag layer: `DndService.dragLayer`. Make sure to unsubscribe from * it in `ngOnDestroy()`. * * 2. Listen to global drag state changes with `DragLayer.listen`. These are all available on `DragLayerMonitor`: * whether something is being dragged, what type it is, where the drag started, where the dragged element is now. * * 3. If dragging, render a custom preview under the current mouse position, * depending on the item type, in a `position: fixed` 'layer'. You may like to * use an `*ngSwitch` on the type, rather than one drag layer per type, simply * to reduce code duplication. * * * You can see an example of a drag layer over on the `Examples` page. * * One piece of advice for using drag layers effectively is to separate 'smart' and * 'dumb' components. If you have one component purely for visuals, which does all * input through `@Input()` and all interactivity through `@Output()` events, then * you can reuse it to display a drag preview based on either data in the item from * `DragSourceSpec.beginDrag`, or supplied by a 'smart' component which pulls * data from somewhere else using only an `id`. This practice is even more * important if you are using, or planning on using, anything other than the HTML5 * backend, because no other backend provides automatic previews. In those cases * you must handle every draggable `type` in a drag layer to have any previews at * all. * * Or, you could just use [@ng-dnd/multi-backend](../@ng-dnd/multi-backend/). * */ export interface DragLayer<Item = any> extends ConnectionBase<DragLayerMonitor<Item>> { /** * For listen functions in general, see {@link ConnectionBase#listen}. * * This listen function is called any time the global drag state * changes, including the coordinate changes, so that your component can * provide a timely updated custom drag preview. You can ask the monitor for * the client coordinates of the dragged item. Read the {@link DragLayerMonitor} * docs to see all the different possibile coordinates you might subscribe to. * */ listen<O>(mapTo: (monitor: DragLayerMonitor<Item>) => O): Observable<O>; }