@ng-dnd/core
Version:
Drag and Drop for Angular
255 lines (254 loc) • 10.7 kB
TypeScript
/**
* @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>;
}