Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Angular2 directives supporting nested drag and drop
import { Injectable } from '@angular/core';
@Injectable()
export class DndContext {
dropEffect = 'none';
isDragging = false;
itemType = undefined;
dragData: any = undefined;
// stopDragOver= new Map<string, () => void>();
stopDragOver: () => void = undefined;
current: { stopDragOver: () => void };
}
export interface DndDragEvent {
dragData?: any;
dropEffect?: string;
event: MouseEvent;
}
export interface DndDropEvent {
/**
* The original drop event sent by the browser
*/
event: DragEvent;
/**
* The position in the drop target at which the element would be dropped.
*/
index: number;
/**
* The transferred data
*/
data: any;
/**
* The type of element (if provided on the source element)
*/
type: string;
/**
* Indicates whether the element was dragged from an external source.
*/
external: boolean;
}
import {
Directive,
Input,
Output,
EventEmitter,
ElementRef,
ChangeDetectorRef,
HostBinding,
HostListener,
Renderer,
NgZone,
Injectable
} from '@angular/core';
import {
isPresent,
isBlank,
isString,
isFunction
} from '@angular/core/src/facade/lang';
import { DndContext, DndDragEvent } from './context';
export class DragImage {
constructor(
public imageElement: string | HTMLElement,
public x_offset: number = 0,
public y_offset: number = 0) {
if (isString(this.imageElement)) {
// Create real image from string source
let imgScr: string = <string>this.imageElement;
this.imageElement = new HTMLImageElement();
(<HTMLImageElement>this.imageElement).src = imgScr;
}
}
}
@Directive({
selector: '[tkDraggable]'
})
export class DraggableDirective {
/**
* Whether the object is draggable. Default is true.
*/
@Input()
@HostBinding('attr.draggable')
set dragEnabled(enabled: boolean) {
this.disabled = !enabled;
}
get dragEnabled(): boolean {
return !this.disabled;
}
/**
* The data to be transferred to the drop target.
* Can be any JS object.
*/
@Input() tkDraggable: any;
/**
* Drag effect
*/
@Input() dragEffect: 'copy' | 'move' | 'none';
/**
* Use this attribute if you have different kinds of items in your application and you want to
* limit which items can be dropped into which lists
*/
@Input() draggedItemType: string;
/**
* This class will be added to the element while the it is being dragged. It will affect
* both the element you see while dragging and the source element that stays at its position.
* Do not try to hide the source element with this class as it will about the drag operation.
*/
@Input() dragClass: string;
/**
* This class will be added to the source element after the drag operation is started, meaning
* it only affects the original element that is still at it's source position and
* not the "element" that the user is dragging.
*/
@Input() dragSourceClass: string;
@Input() dragImage: string | DragImage | Function;
/**
* Event raised when the element was moved. Usually you will remove your element
* from the original list by handling this event since this directive does not do
* that for you automatically. The original dragend event will be provided as output.
*/
@Output() onMoved: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
/**
* Same as the targetMoved event except that it is raised when the element is copied
* instead of moved. Payload will be the original dragend event.
*/
@Output() onCopied: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
/**
* Event raised if the element was clicked but not dragged. The original click event
* will be provided in the payload.
*/
@Output() onSelected: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
/**
* Event raised when the element is dragged. The original dragstart event
* will be privided as the output data.
*/
@Output() onDragStart: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
/**
* Event raised when the drag operation is ended. Output will be
* the original dragend event and the dropEffect
*/
@Output() onDragEnd: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
/**
* Event raised when the element was dragged but the operation was canceled
* and the element was not dropped. The original dragend event will be provided
* as output.
*/
@Output() onDragCanceled: EventEmitter<DndDragEvent> = new EventEmitter<DndDragEvent>();
private disabled: boolean = false;
private element: HTMLElement;
private defaultCursor: string;
constructor(
element: ElementRef,
private cdr: ChangeDetectorRef,
private renderer: Renderer,
private zone: NgZone,
private context: DndContext) {
this.element = element.nativeElement;
this.dragEnabled = true;
}
/**
* When the drag operation is started we have to prepare the dataTransfer object,
* which is the primary way we communicate with the target element
*/
@HostListener('dragstart', ['$event'])
handleDragStart(dragEvent: DragEvent) {
console.debug('DND: Drag start', dragEvent);
// Check whether the element is draggable, since dragstart might be triggered on a child.
if (this.dragEnabled === false) return true;
// Initialize global state
this.context.dropEffect = 'none';
this.context.isDragging = true;
this.context.dragData = this.tkDraggable;
this.context.itemType = this.draggedItemType || undefined;
// Add CSS classes.
const draggedElementClass = this.dragClass;
const sourceElementClass = this.dragSourceClass;
if (draggedElementClass) {
this.renderer.setElementClass(this.element, draggedElementClass, true);
}
if (sourceElementClass) {
setTimeout(() => {
this.zone.run(
() => this.renderer.setElementClass(
this.element, sourceElementClass, true));
}, 0);
}
// Try setting a proper drag image if triggered on a dnd-handle (won't work in IE).
if (dragEvent.dataTransfer) {
dragEvent.dataTransfer.setData('text', '');
// Change drag effect
dragEvent.dataTransfer.effectAllowed = this.dragEffect || '';
// Change drag image
const setDragImage = (image: any, xOffset: number = 0, yOffset: number = 0) =>
(dragEvent.dataTransfer as any).setDragImage(image, xOffset, yOffset);
if (isPresent(setDragImage) && isFunction(setDragImage)) {
if (isPresent(this.dragImage)) {
if (isString(this.dragImage)) {
setDragImage(
this.createImage(<string>this.dragImage));
} else if (isFunction(this.dragImage)) {
setDragImage((this.dragImage as Function)());
} else {
let img: DragImage = <DragImage>this.dragImage;
setDragImage(
img.imageElement, img.x_offset, img.y_offset);
}
} else {
setDragImage(this.element, 0, 0);
}
}
// // Change drag cursor
// const cursor = this.dragCursor || this.config.dragCursor;
// if (cursor) {
// this.context.dragCursor = this.element.style.cursor;
// this.element.style.cursor = cursor;
// } else {
// this.element.style.cursor = this._defaultCursor;
// }
}
// Raise the dragstart event
this.onDragStart.next(<DndDragEvent>{ event: dragEvent });
// Prevent triggering event in parent elements
if (isFunction(dragEvent.stopPropagation))
dragEvent.stopPropagation();
console.debug('DND: Drag start complete', dragEvent);
}
/**
* The dragend event is triggered when the element is dropped or when the drag operation
* is aborted (e.g. hitting the escape button). Depending on the executed action we will
* invoke the callbacks specified with the targetCopied and targetMoved events
*/
@HostListener('dragend', ['$event'])
handleDragEnd(dragEvent: DragEvent) {
console.debug('DND: Drag end', dragEvent);
/**
* Invoke callbacks. Usually we would use event.dataTransferEffect.dropEffect to determine
* the used effect, but Chrome has not implemented that field correctly. On Windows it is
* always set to 'none', while Chrome on Linux sometimes sets it to something else when it
* is supposed to send 'none' (drag operation aborted).
*/
const dropEffect = this.context.dropEffect;
switch (dropEffect) {
case 'move':
this.onMoved.emit({ event: dragEvent, dragData: this.context.dragData });
break;
case 'copy':
this.onCopied.emit({ event: dragEvent, dragData: this.context.dragData });
break;
case 'none':
this.onDragCanceled.emit({ event: dragEvent, dragData: this.context.dragData });
break;
default:
break;
}
this.onDragEnd.emit(<DndDragEvent>{
event: dragEvent,
dragData: this.context.dragData,
dropEffect
});
// Clean up
const draggingClass = this.dragClass;
const dragSourceClass = this.dragSourceClass;
if (draggingClass) {
this.renderer.setElementClass(this.element, draggingClass, false);
}
if (dragSourceClass) {
setTimeout(() => {
this.zone.run(
() => this.renderer.setElementClass(this.element, dragSourceClass, false));
}, 0);
}
this.context.isDragging = false;
// Prevent triggering event in parent elements
if (isFunction(dragEvent.stopPropagation))
dragEvent.stopPropagation();
console.debug('DND: Drag end complete');
}
@HostListener('click', ['$event'])
handleClick(event: MouseEvent) {
this.onSelected.emit({ event });
// Prevent triggering event in parent elements
event.stopPropagation();
}
/**
* Create Image element with specified url string
*/
private createImage(src: string) {
let img: HTMLImageElement = new HTMLImageElement();
img.src = src;
return img;
}
}
import {
Directive,
Input,
Output,
EventEmitter,
ElementRef,
ChangeDetectorRef,
HostBinding,
HostListener,
Renderer,
NgZone,
ViewContainerRef,
TemplateRef,
OnInit,
AfterViewInit,
ViewChild,
EmbeddedViewRef,
ComponentRef,
ViewRef,
Host,
Optional,
AfterContentInit,
forwardRef,
Inject
} from '@angular/core';
import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';
import {
isPresent,
isBlank,
isString,
isFunction
} from '@angular/core/src/facade/lang';
import { DndContext, DndDropEvent } from './context';
class DropTargetPlaceholderView {
viewRef: EmbeddedViewRef<Object>;
constructor(
public viewContainer: ViewContainerRef, private template: TemplateRef<Object>) { }
get index() {
return this.viewContainer.indexOf(this.viewRef);
}
create(): void { this.viewRef = this.viewContainer.createEmbeddedView(this.template); }
destroy(): void {
if (isPresent(this.viewRef)) {
this.viewContainer.clear();
this.viewRef = undefined;
}
}
contains(node: Node): boolean {
if (!isPresent(this.viewRef) || !isPresent(this.viewRef.rootNodes)) {
return false;
}
const nodes = this.viewRef.rootNodes as Node[];
return nodes.includes(node) || nodes.some(n => n.contains(node));
}
}
@Directive({
selector: '[tkDropTarget]'
})
export class DropTargetDirective {
/**
* Whether the object allows drop events. Default is false.
*/
@Input() disabled = false;
/**
* Optional array of allowed item types. When used, only items a matching type will be droppable
*/
@Input() allowedTypes: Array<string>;
/**
* Optional. When true, the list accepts drops from sources outside the current browser tab.
* Note that this will allow dropping arbitrary text into the list, thus it is highly
* recommended to provide a validator function via 'allowDrop'
* Furthermore, the elementType of external sources cannot be determinded, so do not rely
* on restrictions based on allowedTypes
*/
@Input() allowExternalSources: boolean = false;
/**
* When true, the positioning algorithm will use the left and right halfs of the list
* items instead of the upper and lower halfs.
*/
@Input() horizontal: boolean = false;
/**
* An optional function that is invoked when an element is dropped on the list
* that determines if the drop operation should be allowed.
*/
@Input() allowDrop: (event: DndDropEvent) => boolean;
/**
* CSS class to be set on the list element when an element is dragged over it.
*/
@Input() dragOverClass: string;
/**
* Event raised on successful drop
*/
@Output() onDrop: EventEmitter<DndDropEvent> = new EventEmitter<DndDropEvent>();
private placeHolder: DropTargetPlaceholderView;
constructor(
private element: ElementRef,
private renderer: Renderer,
private context: DndContext
// @Optional() @Host() @Inject(forwardRef(() => DropTargetDirective))
// private parent: DropTargetDirective,
// @Optional() @Host() @Inject(forwardRef(() => DropTargetPlaceholderDirective))
// private parent: DropTargetPlaceholderDirective,
) {
}
/**
* The dragenter event is fired when a dragged element or text selection enters a valid drop
* target. According to the spec, we either need to have a dropzone attribute or listen on
* dragenter events and call preventDefault(). It should be noted though that no browser seems
* to enforce this behaviour.
*/
@HostListener('dragenter', ['$event'])
private handleDragEnter(event: DragEvent) {
if (!this.isDropAllowed(event)) {
return true;
}
event.preventDefault();
}
/**
* The dragover event is triggered "every few hundred milliseconds" while an element
* is being dragged over our target, or over a child element.
*/
@HostListener('dragover', ['$event'])
private handleDragOver(event: DragEvent) {
if (!this.isDropAllowed(event)) {
// event.dataTransfer.effectAllowed = 'none';
event.dataTransfer.dropEffect = 'none';
return true;
}
const listNode = this.element.nativeElement as HTMLElement;
if (isPresent(this.placeHolder)) {
// Make sure the placeholder is shown,
// which is especially important if the list is empty.
if (!this.placeHolder.viewRef) {
this.placeHolder.create();
// In nested lists, the parent list might be showing a placeholder
// that we have to remove.
// if (isPresent(this.context.stopDragOver)) {
// const stopDragOver = this.context.stopDragOver;
// setTimeout(() => stopDragOver(), 0);
// }
// this.context.stopDragOver = this.stopDragOver;
if (isPresent(this.context.current)) {
this.context.current.stopDragOver();
}
this.context.current = this;
}
// const actions = [];
// this.context.stopDragOver.forEach((stopDragOver, id) => {
// if (id !== this.id && isPresent(stopDragOver)) {
// actions.push(stopDragOver);
// }
// });
// actions.forEach(action => action());
// this.context.stopDragOver[this.id] = this.stopDragOver;
if (event.target !== listNode) {
// Try to find the node direct directly below the list node.
var listItemNode = event.target as Node;
while (listItemNode.parentNode != listNode && isPresent(listItemNode.parentNode)) {
listItemNode = listItemNode.parentNode;
}
if (listItemNode.parentNode == listNode
&& !this.placeHolder.contains(listItemNode)
&& listItemNode instanceof HTMLElement) {
// If the mouse pointer is in the upper half of the list item element,
// we position the placeholder before the list item, otherwise after it.
const listItemElement = listItemNode as HTMLElement;
var rect = listItemElement.getBoundingClientRect();
const isFirstHalf = this.horizontal
? event.clientX < rect.left + rect.width / 2
: event.clientY < rect.top + rect.height / 2;
this.placeHolder.destroy();
listNode.insertBefore(this.placeHolderNode,
isFirstHalf ? listItemNode : listItemNode.nextSibling);
this.placeHolder.create();
}
}
}
// at this point we invoke the callback, which can still disallow the drop.
// We can't do this earlier because we need the index of the placeholder
const dragOverEvent = this.createDropEvent(event);
if (this.allowDrop && !this.allowDrop(dragOverEvent)) {
this.stopDragOver();
console.debug('DND: Drop disabled');
return true;
}
if (this.dragOverClass) {
this.renderer.setElementClass(this.element.nativeElement, this.dragOverClass, true);
}
event.preventDefault();
if (isFunction(event.stopPropagation))
event.stopPropagation();
// console.debug('DND: Drag over complete', event);
return false;
}
@HostListener('drop', ['$event'])
private handleDrop(event: DragEvent) {
console.debug('DND: Drop', event);
if (!this.isDropAllowed(event)) return true;
// The default behavior in Firefox is to interpret the dropped element as URL and
// forword to it. We want to prevent that even if our drop is aborted.
event.preventDefault();
const targetIndex = this.placeHolderIndex;
console.log(`Dropping item to index ${targetIndex}`, event.target);
const dropEvent = this.createDropEvent(event, targetIndex);
if (this.allowDrop && !this.allowDrop(dropEvent)) {
this.stopDragOver();
console.debug('DND: Drop disabled');
return true;
}
this.onDrop.next(dropEvent);
// In Chrome on Windows the dropEffect will always be none...
// We have to determine the actual effect manually from the allowed effects
if (event.dataTransfer.dropEffect === 'none') {
if (event.dataTransfer.effectAllowed === 'copy' ||
event.dataTransfer.effectAllowed === 'move') {
this.context.dropEffect = event.dataTransfer.effectAllowed;
} else {
this.context.dropEffect = event.ctrlKey ? 'copy' : 'move';
}
} else {
this.context.dropEffect = event.dataTransfer.dropEffect;
}
// Clean up
this.stopDragOver();
if (isFunction(event.stopPropagation))
event.stopPropagation();
return false;
}
/**
* We have to remove the placeholder when the element is no longer dragged over our list. The
* problem is that the dragleave event is not only fired when the element leaves our list,
* but also when it leaves a child element -- so practically it's fired all the time. As a
* workaround we wait a few milliseconds and then check if the dndDragover class was added
* again. If it is there, dragover must have been called in the meantime, i.e. the element
* is still dragging over the list. If you know a better way of doing this, please tell me!
*/
@HostListener('dragleave', ['$event'])
private handleDragLeave(event: DragEvent) {
// const document = this.renderer.selectRootElement('document') as HTMLDocument;
const document = getDOM().defaultDoc();
const target = document.elementFromPoint(event.clientX, event.clientY);
const container = this.element.nativeElement as HTMLElement;
if (!container.contains(target)) {
this.stopDragOver();
}
}
private get placeHolderNode() {
return this.placeHolder.viewContainer.element.nativeElement as Node;
}
private get placeHolderIndex() {
if (isPresent(this.placeHolder) && isPresent(this.placeHolder.viewRef)) {
const listNode = this.element.nativeElement as HTMLElement;
for (let i = 0; i < listNode.children.length; i++) {
const childNode = listNode.children.item(i);
if (this.placeHolder.contains(childNode))
return i;
}
}
return -1;
}
private createDropEvent(event: DragEvent, index: number = undefined) {
return {
event,
external: !this.context.isDragging,
index: index !== undefined ? index : this.placeHolderIndex,
data: this.context.isDragging
? this.context.dragData
: event.dataTransfer.getData(event.dataTransfer.types[0]),
type: this.context.isDragging ? this.context.itemType : undefined
};
}
public stopDragOver() {
if (isPresent(this.placeHolder)) {
this.placeHolder.destroy();
}
if (this.dragOverClass) {
this.renderer.setElementClass(this.element.nativeElement, this.dragOverClass, false);
}
console.debug('DND: Stopping drag over');
// this.context.stopDragOver.delete(this.id);
}
/**
* Checks various conditions that must be fulfilled for a drop to be allowed
*/
private isDropAllowed(event: DragEvent) {
// Disallow all drops if globally disabled
if (this.disabled) return false;
// Disallow drop from external source unless it's allowed explicitly.
if (!this.context.isDragging && !this.allowExternalSources) return false;
// Check mimetype. Usually we would use a custom drag type instead of Text, but IE doesn't
// support that.
// if (!this.hasTextMimetype(event.dataTransfer.types)) return false;
// Now check the dnd-allowed-types against the type of the incoming element. For drops from
// external sources we don't know the type, so it will need to be checked via dnd-drop.
if (this.allowedTypes && this.allowedTypes.length && this.context.isDragging) {
if (this.allowedTypes.indexOf(this.context.itemType) === -1) {
return false;
}
}
return true;
}
/** @internal */
_registerPlaceholder(template: TemplateRef<Object>, viewContainer: ViewContainerRef) {
if (isPresent(this.placeHolder)) {
console.error('A placeholder has already bean registered. Only one tkDropTargetPlaceholder may be used');
return;
}
console.debug('Registering drop-target placeholder');
this.placeHolder = new DropTargetPlaceholderView(viewContainer, template);
}
// /**
// * Checks whether the mouse pointer is in the first half of the given target element.
// *
// * In Chrome we can just use offsetY, but in Firefox we have to use layerY, which only
// * works if the child element has position relative. In IE the events are only triggered
// * on the listNode instead of the listNodeItem, therefore the mouse positions are
// * relative to the parent element of targetNode.
// */
// isMouseInFirstHalf(event: DragEvent, targetNode: Node, relativeToParent: boolean) {
// var mousePointer = this.horizontal ? (event.offsetX || event.layerX)
// : (event.offsetY || event.layerY);
// var targetSize = this.horizontal ? targetNode.offsetWidth : targetNode.offsetHeight;
// var targetPosition = this.horizontal ? targetNode.offsetLeft : targetNode.offsetTop;
// targetPosition = relativeToParent ? targetPosition : 0;
// return mousePointer < targetPosition + targetSize / 2;
// }
}
@Directive({
selector: '[tkDropTargetPlaceholder]'
})
export class DropTargetPlaceholderDirective {
constructor(
template: TemplateRef<Object>,
viewContainer: ViewContainerRef,
@Host() public dropTarget: DropTargetDirective
) {
dropTarget._registerPlaceholder(template, viewContainer);
}
}
@jimitndiaye

This comment has been minimized.

Copy link
Owner Author

jimitndiaye commented Apr 20, 2017

Here's a snippet of production code using the drag/drop directives:

<ul tkDropTarget [allowedTypes]="container.allowedTypes" 
                           [allowExternalSources]="false" 
                           [horizontal]="container.horizontal"
                           [dragOverClass]="'dndDragOver'" 
                           (allowDrop)="validateChildComponent($event, container.id)" 
                           (onDrop)="addComponent($event, container.id)"
                           [ngClass]="{horizontal: container.horizontal}" 
                           class="container">
            <li *ngFor="let component of container.components"
                class="container-item"
                [tkDraggable]="component"
                [draggedItemType]="component.type"
                [dragEffect]="'move'"
                [dragClass]="'dndDragging'"
                [dragSourceClass]="'dndDraggingSource'">
                      <ion-toolbar>
                          <ion-title>{{getComponentName(component.type)}}</ion-title>
                          <ion-buttons right>
                              <button primary
                                      title="Settings "
                                      (click)="configureComponent(component)">
                                  <ion-icon name="options"></ion-icon>
                              </button>
                              <button danger
                                      title="Delete "
                                      (click)="removeComponent(component.id)">
                                  <ion-icon name="close-circle"></ion-icon>
                              </button>
                          </ion-buttons>
                      </ion-toolbar>
                      <template [tkComponentOutlet]="component"
                                (componentChanged)="logComponentChanged($event)"></template>
                  </li>
                  <li class="dndPlaceholder container-item "
                      text-center
                      text-capitalize
                      *tkDropTargetPlaceholder>Drop here</li>
              </ul>

This was written using Angular 2.1.0. I'm not quite up to date any changes in syntax in more recent versions of Angular, if any. It is for a WYSIWYG designer allowing drag and drop of nested components. We needed to support arbitrarily deep nesting while allowing drag and drop to and from containers at any level. The key things to note are:

  • The tkDropTarget directive is used on the enclosing ul to mark it as a drop zone.
  • The horizontal property on that directive is a boolean used to indicate whether the children are arranged horizontally or vertically - this is used to calculate placeholder placement. In this instance it is bound to a property. If not specified, horizontal is assumed to be false, i.e items are arranged vertically.
  • The allowedTypes property is used to filter which items are allowed to be dropped to in this zone.
  • The dragOverClass is an optional property that allows you to toggle a CSS class when an item is being dragged over the drop zone e.g toggle a border color etc.
  • I use the allowDrop and onDrop events validate drop items and process dropped items respectively.
  • Within the enclosing ul I use an ngFor to list a bunch of draggable li using the tkDraggable directive. These represent the direct children of the container. Marking them as draggable allows you to use drag and drop to reposition items within the container or even move them from one container to another.
  • dragClass is used to toggle a CSS class on the dragged item
  • dragSourceClass is used to toggle a CSS class on the dragged item's original position
  • The each li contains a template (tkComponentOutlet is very similar to ngComponentOutlet) for embedding a component which may itself be a container, thus resulting in nested lists. In my case, any nested containers reuse this same template above resulting in a form of recursion.
  • Finally the ul has an li marked with tkDropTargetPlaceholder which basically defines the template for the placeholder element shown during drag and drop

If you have any further questions on individual directive properties there are comments in the original gist for every input property.

The relevant CSS classes for the snippet above are as follows:

/***************************** Dropzone Styling *****************************/

/**
 * The drop target should always have a min-height,
 * otherwise you can't drop to it when it's empty
 */
ul.container[tkDropTarget] {
    min-height: 42px;
    width: 100%;
    height: 100%;
    margin: 0px;
    padding-left: 0px;
    // background-color: grey;
   // margin-top: 10px;
    // margin-bottom: 10px;
}

ul.container[tkDropTarget] li{
    // background-color: #fff;
    border: 1px solid #ddd;
    display: block;
    padding: 0px;
}

/**
 * Reduce opacity of elements during the drag operation. This allows the user
 * to see where he is dropping his element, even if the element is huge. The
 * .dndDragging class is automatically set during the drag operation.
 */
ul.container[tkDropTarget] .dndDragging {
    opacity: 0.7;
}

/**
 * Put a border around the drop zone to highlight it during the drag operation
 */
ul.container.dndDragOver  {
    border: 1px solid blue;
}

/**
 * The dndDraggingSource class will be applied to the source element of a drag
 * operation. It makes sense to hide it to give the user the feeling that he's
 * actually moving it. Note that the source element has also .dndDragging class.
 */
ul.container[tkDropTarget] .dndDraggingSource {
    display: none;
}

/**
 * An element with .dndPlaceholder class will be added as child of the drop target
 * while the user is dragging over it.
 */
ul.container[tkDropTarget] .dndPlaceholder {
    background-color: #ddd;
    min-height: 42px;
    display: block;
    position: relative;
}

.container .container-item {
    margin: 10px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.