<!DOCTYPE html>
<html>

  <head>
    <title>angular2 playground</title>
    <link rel="stylesheet" href="style.css" />
    <script src="https://code.angularjs.org/tools/system.js"></script>
    <script src="https://code.angularjs.org/tools/typescript.js"></script>
    <script src="config.js"></script>
    <script src="https://code.angularjs.org/2.0.0-alpha.46/angular2.dev.js"></script>
    <script>
    System.import('app')
      .catch(console.error.bind(console));
  </script>
  </head>

  <body>
    <my-app>
    loading...
  </my-app>
  </body>

</html>
body,html {
  padding: 0;
  margin: 0;
}

.testbox {
  background-color: red;
  position: absolute;
  cursor: default;
}

.testbox2 {
  background-color: green;
  position: absolute;
  width: 200px;
  height: 200px;
  left: 50px;
  top: 300px;
  cursor: default;
}

.testbox3 {
  background-color: yellow;
  position: absolute;
  width: 200px;
  height: 200px;
  left: 300px;
  top: 50px;
  cursor: default;
}

.resize-handle {
    position: absolute;
}

.resize-handle-e {
    right: -3px;
    top: 0;
    width: 6px;
    height: 100%;
    cursor: e-resize;
}

.resize-handle-n {
    left: 0;
    top: -3px;
    width: 100%;
    height: 6px;
    cursor: n-resize;
}

.resize-handle-w {
    left: -3px;
    top: 0;
    width: 6px;
    height: 100%;
    cursor: w-resize;
}

.resize-handle-s {
    left: 0;
    bottom: -3px;
    width: 100%;
    height: 6px;
    cursor: s-resize;
}

.resize-handle-ne {
    top: -3px;
    right: -3px;
    width: 10px;
    height: 10px;
    cursor: ne-resize;
}

.resize-handle-se {
    bottom: -3px;
    right: -3px;
    width: 10px;
    height: 10px;
    cursor: se-resize;
}

.resize-handle-sw {
    bottom: -3px;
    left: -3px;
    width: 10px;
    height: 10px;
    cursor: sw-resize;
}

.resize-handle-nw {
    top: -3px;
    left: -3px;
    width: 10px;
    height: 10px;
    cursor: nw-resize;
}
import { Component, View, NgStyle, bootstrap } from 'angular2/angular2'
import { Resizable } from './resizable.ts'
import { Draggable } from './draggable.ts'

@Component({
  selector: 'my-app'
})
@View({
  template: `
    <div class="testbox resizable draggable" [draggable]="{model:widget}" [resizable]="{model:widget}" [ng-style]="widget">
      draggable & resizable with model
      <pre>
  top    : {{widget.top}}
  left   : {{widget.left}}
  width  : {{widget.width}}
  height : {{widget.height}}
      </pre>
    </div>
    <div class="testbox2 resizable draggable" draggable resizable>
      draggable & resizable without model
    </div>
    <div class="testbox3 resizable draggable" draggable resizable 
      (drag-start)="onDragStart()" (drag)="onDrag()" (drag-stop)="onDragStop()"
      (resize-start)="onResizeStart()" (resize)="onResize()" (resize-stop)="onResizeStop()">
      <div>draggable & resizable event handling</div>
      <div>status : {{status}}</div>
      <div>drags : {{drags}}</div>
      <div>resizes : {{resizes}}</div>
    </div>
  `,
  directives: [ Draggable, Resizable, NgStyle ]
})
export class App {
  widget = {
    top: '50px',
    left: '50px',
    height: '200px',
    width: '200px'
  };
  
  status = "idle";
  drags: 0;
  resizes: 0;
  
  public onDragStart() {
    this.status = "dragging";
  }
  
  public onDrag() {
    this.drags++;
  }
  
  public onDragStop() {
    this.status = "idle";
  }
  
  public onResizeStart() {
    this.status = "resizing";
  }
  
  public onResize() {
    this.resizes++;
  }
  
  public onResizeStop() {
    this.status = "idle";
  }
  
}

bootstrap(App)
  .catch(err => console.error(err));
System.config({
  transpiler: 'typescript',
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  map: {
    app: "./src",
    '@reactivex/rxjs':'https://cdn.rawgit.com/ReactiveX/RxJS/5.0.0-alpha.8/src' 
  },
  packages: {
    app: {
      main: './app.ts',
      defaultExtension: 'ts'
    },
    '@reactivex/rxjs': {
      main: 'Rx.ts',
      defaultExtension: 'ts'
    }
  }
});
import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Renderer } from 'angular2/angular2';
import { Observable, Subscription, Subscriber } from '@reactivex/rxjs';

export class Position {
    constructor(public left?: number, public top?: number) { }
}

export class DragEvent {
    public cancelled: boolean = false;
    constructor(public mouseDown: UIEvent, public mouseMove: UIEvent, public position: Position, public offset: Position) {
    }
}

const makeInputObservable =
    (node: Node, eventName: string, useCapture?: boolean): Observable<UIEvent> =>
        Observable.fromEventPattern<MouseEvent>(
            (handler) => { node.addEventListener(eventName, <EventListener>handler, useCapture); },
            (handler) => { node.removeEventListener(eventName, <EventListener>handler, useCapture); }
        );

@Directive({
    selector: '[draggable]',
    inputs: ['config:draggable'],
    outputs: ['drag', 'dragStart', 'dragStop']
})
export class Draggable implements OnDestroy, OnInit {
    public drag: EventEmitter = new EventEmitter();
    public dragStart: EventEmitter = new EventEmitter();
    public dragStop: EventEmitter = new EventEmitter();

    private _dragSubscription: Subscription<DragEvent>;
    private _isDragging: boolean = false;
    private _axis: string;
    private _config: any;
    private _mouseDelay: number = 50;
    private _mouseDelayMet: boolean;
    private _mouseDelayTimer: number;
    private _mouseDistance: number = 5;
    private _mouseDistanceMet: boolean = false;
    private _containment: ClientRect = null;
    private _dragOffsetX: number;
    private _dragOffsetY: number;
    private _elementStartX: number;
    private _elementStartY: number;
    private _model: any;

    set config(value: any) {
        this._config = value;
        this.setConfig(this._config);
    }

    public constructor(private _element: ElementRef, private _renderer: Renderer) {
    }

    public setConfig(config: any): void {
        for (let key in config) {
            var value = config[key];
            switch (key) {
                case 'axis':
                    this._axis = value;
                    break;
                case 'delay':
                    this._mouseDelay = parseInt(value);
                    break;
                case 'distance':
                    this._mouseDistance = parseInt(value);
                    break;
                case 'containment':
                    this._containment = value;
                    break;
                case 'model':
                    this._model = value;
                    break;
            }
        }
    }

    private _generatePosition(event: MouseEvent): Position {
        var posX = (this._axis == 'y') ? this._elementStartX : this._elementStartX + this._dragOffsetX;
        var posY = (this._axis == 'x') ? this._elementStartY : this._elementStartY + this._dragOffsetY;
        return new Position(posX, posY);
    }

    private _start(): void {
        this._isDragging = false;
        this._mouseDelayMet = this._mouseDelay == 0;
        this._mouseDistanceMet = this._mouseDistance == 0;
        this._elementStartX = this._element.nativeElement.offsetLeft;
        this._elementStartY = this._element.nativeElement.offsetTop;
        if (!this._mouseDelayMet) {
            this._mouseDelayTimer = setTimeout(() => {
                this._mouseDelayMet = true;
            }, this._mouseDelay);
        }
    }

    private _update(mouseDownEvent: MouseEvent, mouseMoveEvent: MouseEvent): void {
        this._dragOffsetX = mouseMoveEvent.clientX - mouseDownEvent.clientX;
        this._dragOffsetY = mouseMoveEvent.clientY - mouseDownEvent.clientY;
        this._mouseDistanceMet = Math.abs(this._dragOffsetX) > this._mouseDistance || Math.abs(this._dragOffsetY) > this._mouseDistance
        if (!this._isDragging && this._mouseDelayMet && this._mouseDistanceMet) {
            this.dragStart.next(event);
            this._isDragging = true;
        }
    }

    public onInit(): void {
        var mouseDownObservable = Observable.fromEvent(this._element.nativeElement, 'mousedown').filter((md: MouseEvent) => md.which == 1);
        var mouseMoveObservable = Observable.fromEvent(this._element.nativeElement.ownerDocument, 'mousemove');
        var mouseUpObservable = Observable.fromEvent(this._element.nativeElement.ownerDocument, 'mouseup');
        var clickObservable = makeInputObservable(this._element.nativeElement.ownerDocument, 'click', true);
        var dragObservable = mouseDownObservable.flatMap<DragEvent>((mouseDownEvent: MouseEvent) => {
            mouseDownEvent.preventDefault();
            mouseDownEvent.stopPropagation();
            this._start();
            return mouseMoveObservable
                .map((mouseMoveEvent: MouseEvent) => {
                    this._update(mouseDownEvent, mouseMoveEvent);
                    return new DragEvent(mouseDownEvent, mouseMoveEvent, this._generatePosition(mouseMoveEvent), new Position(this._dragOffsetX, this._dragOffsetY));
                })
                .filter(() => this._isDragging)
                .takeUntil(mouseUpObservable
                    .map((mouseUpEvent) => {
                        clearInterval(this._mouseDelayTimer);
                        if (this._isDragging)
                            this.dragStop.next(mouseUpEvent);
                    })
                    .zip(clickObservable.map((clickEvent: MouseEvent) => {
                        if (this._isDragging) {
                            clickEvent.stopPropagation();
                            this._isDragging = false;
                        }
                    }))
                );
        });

        this._dragSubscription = dragObservable.subscribe((event) => {
            this.drag.next(event);
            setTimeout(() => {
                if (!event.cancelled) {
                    this._setStyle('top', event.position.top + 'px');
                    this._setStyle('left', event.position.left + 'px');
                }
            });
        });
    }

    private _setStyle(styleName: string, styleValue: string) {
        if (this._model) {
            this._model[styleName] = styleValue
        } else {
            this._renderer.setElementStyle(this._element, styleName, styleValue);
        }
    }

    public onDestroy(): void {
        this._dragSubscription.unsubscribe();
    }
}
import { Component, View, ElementRef, CORE_DIRECTIVES, EventEmitter, Renderer } from 'angular2/angular2';
import { DragEvent, Draggable } from "./draggable";

@Component({
    selector: '[resizable]',
    inputs: ['config:resizable'],
    outputs: ['resize', 'resizeStart', 'resizeStop']
})
@View({
    template: `
    <ng-content></ng-content>
    <div *ng-for="#handle of handles" [ng-class]="'resize-handle resize-handle-' + handle" draggable (drag)="onDrag($event, handle)" (drag-start)="onDragStart($event)" (drag-stop)="onDragStop($event)"></div>
    `,
    directives: [Draggable, CORE_DIRECTIVES]
})
export class Resizable {
    public resize: EventEmitter = new EventEmitter();
    public resizeStart: EventEmitter = new EventEmitter();
    public resizeStop: EventEmitter = new EventEmitter();

    private _handles: Array<string> = ['s', 'e', 'se'];
    private _originalWidth: number;
    private _originalHeight: number;
    private _originalLeft: number;
    private _originalTop: number;
    private _minWidth: number = 0;
    private _minHeight: number = 0;
    private _model: any;
    private _config: any;

    set config(value: any) {
        this._config = value;
        this.setConfig(this._config);
    }

    get handles(): Array<string> {
        return this._handles;
    }

    public setConfig(config: any): void {
        for (let key in config) {
            var value = config[key];
            switch (key) {
                case 'model':
                    this._model = value;
                    break;
                case 'handles':
                    this._handles = value;
                    break;
                case 'minWidth':
                    this._minWidth = parseFloat(value);
                    break;
                case 'minHeight':
                    this._minHeight = parseFloat(value);
                    break;
            }
        }
    }

    public constructor(private _element: ElementRef, private _renderer: Renderer) {
    }

    private _setStyle(styleName: string, styleValue: string) {
        if (this._model) {
            this._model[styleName] = styleValue
        } else {
            this._renderer.setElementStyle(this._element, styleName, styleValue);
        }
    }

    public onDrag(dragEvent: DragEvent, handle: string) {
        if (handle.indexOf('e') != -1) {
            this._setStyle('width', Math.max(this._minWidth, this._originalWidth + dragEvent.offset.left) + 'px');
        }
        if (handle.indexOf('s') != -1)
            this._setStyle('height', Math.max(this._minHeight, this._originalHeight + dragEvent.offset.top) + 'px');
        if (handle.indexOf('w') != -1) {
            this._setStyle('left', (this._originalLeft + dragEvent.offset.left) + 'px');
            this._setStyle('width', Math.max(this._minWidth, this._originalWidth - dragEvent.offset.left) + 'px');
        }
        if (handle.indexOf('n') != -1) {
            this._setStyle('top', (this._originalTop + dragEvent.offset.top) + 'px');
            this._setStyle('height', Math.max(this._minHeight, this._originalHeight - dragEvent.offset.top) + 'px');
        }
        dragEvent.cancelled = true;
        this.resize.next(dragEvent);
    }

    public onDragStart(event: Event) {
        this._originalWidth = this._element.nativeElement.offsetWidth;
        this._originalHeight = this._element.nativeElement.offsetHeight;
        this._originalLeft = this._element.nativeElement.offsetLeft;
        this._originalTop = this._element.nativeElement.offsetTop;
        this.resizeStart.next(event);
    }

    public onDragStop(event: Event) {
        this.resizeStop.next(event);
    }
}