<!DOCTYPE html>
<html>

  <head>
    <title>angular2 playground</title>
    <link data-require="bootstrap@3.3.5" data-semver="3.3.5" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="https://code.angularjs.org/tools/traceur-runtime.js"></script>
    <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.50/angular2.min.js"></script>
    <script>
    System.import('app')
      .catch(console.error.bind(console));
  </script>
  </head>

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

</html>


.form-control {
  height: 30px;
  font-size: 12px;
}

sb-criterion,
sb-segment,
sb-group {
  position: relative;
  display: block;
}

sb-criterion,
sb-segment {
  padding: 2px;
  margin: 2px;
  border: 1px solid #dddddd;
  border-radius: 4px;
}
sb-criterion > div,
sb-segment > div {
  background-color: #efefef;
  padding: 2px;
}

.recentAction {
  border-color: red !important;
}

drop-target {
  position: relative;
  display: block;
  height: 2px;
  left: -30px;
  width: 10%;
  z-index: 100;
}
drop-target > div {
  display: none !important;
  position: relative;
  top: -15px !important;
  height: 30px;
  font-size: 30px;
  color: grey;
}
drop-target.showMe > div {
  display: block !important;
}
drop-target.hoverMe > div {
  color: red;
}

.draggingMe drop-target > div {
  display: none !important;
}

sb-group {
  display: block;
  border: 1px solid #999999;
  margin: 2px;
  padding: 2px;
}

sb-group p {
  margin: 5px;
}

.panel {
  margin-bottom: 5px;
}

.panel-body {
  position: relative;
  padding: 5px 15px;
}
.panel-body > div {
  position: relative;
}

.btn-danger {
  float: right;
}
System.config({
  //use typescript for compilation
  transpiler: 'typescript',
  //typescript compiler options
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  //map tells the System loader where to look for things
  map: {
    app: "./src"
  },
  //packages defines our app package
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    }
  }
});
//main entry point
import {bootstrap} from 'angular2/angular2';
import {App} from './components/App';

bootstrap(App, [])
  .catch(err => console.error(err));

const enum GroupRelation { And, Or, None }


export class SbElement {
    id:number
    recentAction:boolean = false

    constructor(public position:number, public type:string) {
        this.id = Math.round(Math.random() * 1000000);
    }
}


export class SbSegment extends SbElement {
    constructor(position:number, public name:string) {
        super(position, 'segment');
    }
}

export class SbCriterion extends SbElement {
    constructor(position:number, public name:string) {
        super(position, 'criterion');
    }
}

export class SbGroup extends SbElement {
    constructor(position:number, public relation:GroupRelation, public children:Array<SbElement>) {
        super(position, 'group');
    }
}


export class SegmentBuilderService {
    rootGroup:SbGroup
    draggingElement:SbElement

    constructor() {
        this.rootGroup = new SbGroup(0, GroupRelation.And, [
            new SbCriterion(0, 'Mon Voisin Totoro'),
            new SbCriterion(1, 'Tueurs nés'),
            new SbSegment(2, 'Segment films anciens'),
            new SbGroup(3, GroupRelation.None, [
                new SbGroup(0, GroupRelation.Or, [
                    new SbSegment(0, 'Segment films espagnols'),
                    new SbCriterion(1, 'Spirit')
                ]),
                new SbCriterion(1, 'Le fils de l\'Homme'),
                new SbSegment(2, 'Segment films d\'animations'),
            ]),
            new SbGroup(4, GroupRelation.None, [
                new SbGroup(0, GroupRelation.And, [
                    new SbSegment(0, 'Segment films muets')
                ]),
                new SbCriterion(1, 'Mulan')
            ])
        ]);
    }

    public removeElement(parentGroup:SbGroup, elem:SbElement):SbElement {
        let index:number = parentGroup.children.indexOf(elem);
        if (index !== -1) {
            return parentGroup.children.splice(index, 1)[0];
        }
    }

    public addElement(group:SbGroup, type:string) {
        this._sortGroupByPosition(group);

        let pos: number = (group.children.length == 0) ? 0 : group.children[group.children.length - 1].position + 1;
        let elem: SbElement;

        switch(type) {
            case 'criterion':
                elem = new SbCriterion(pos, '');
                break;
            case 'segment':
                elem = new SbSegment(pos, '');
                break;
            case 'group':
                elem = new SbGroup(pos, GroupRelation.And, []);
                break;
            default:
                throw Error(`No type "${type}" existing !`);
                break;
        }

        group.children.push(elem);
        this._highlightRecentAction(elem);
    }

    public moveElement(elemToMove:SbElement, toGroup:SbGroup, destIndex:number) {
        let fromGroup: SbGroup = this._getParentElement(this.rootGroup, elemToMove);

        if(fromGroup === toGroup) {
            console.log('move inside same group');
            this._moveElementInsideGroup(elemToMove, toGroup, destIndex);
        }
        else {
            console.log('move to another group');
            this._moveElementInAnotherGroup(elemToMove, fromGroup, toGroup, destIndex);
        }
    }

    private _moveElementInsideGroup(elemToMove:SbElement, group:SbGroup, destIndex:number):void {
        // si le nouveau index est le même ou le même plus 1
        // alors l'element ne bouge pas!
        let currIndex = group.children.indexOf(elemToMove);

        if(destIndex == currIndex || destIndex == currIndex+1) {
            throw Error('Try to move an element on the same position.. don\'t move it!');
        }
        else {
            let newPosition: number;

            switch(true) {
                // si nouvel index au début du groupe > position = position du premier element moins un
                case (destIndex == 0):
                    console.log('déplacement au début: destIndex=', destIndex, ' / currIndex=', currIndex);
                    newPosition = group.children[0].position - 1;
                    break;

                // si nouvel index à la fin du groupe > position = position du dernier element plus un
                case (destIndex == group.children.length):
                    console.log('déplacement à la fin: destIndex=', destIndex, ' / currIndex=', currIndex);
                    newPosition = group.children[group.children.length - 1].position + 1;
                    break;

                default:
                    if(destIndex < currIndex)  console.log('déplacement en amont: destIndex=', destIndex, ' / currIndex=', currIndex);
                    else                       console.log('déplacement en aval: destIndex=', destIndex, ' / currIndex=', currIndex);
                    newPosition = (group.children[destIndex - 1].position + group.children[destIndex].position)/2;
                    break;
            }

            elemToMove.position = newPosition;
            this._sortGroupByPosition(group);
            this._highlightRecentAction(elemToMove);
        }
    }

    private _moveElementInAnotherGroup(elemToMove:SbElement, fromGroup:SbGroup, toGroup:SbGroup, destIndex:number):void {
        // si on deplace un groupe dans un autre groupe (que son actuel)
        // on doit vérifier que le groupe de destination n'est pas dans le groupe déplacé:
        if(elemToMove instanceof SbGroup) {
            // on remonte du groupe de destination au groupe racine en vérifiant que l'on
            // ne tombe pas sur le groupe que l'on déplace
            let parentGroup: SbGroup = toGroup;
            while (parentGroup != this.rootGroup) {
                if(parentGroup === elemToMove) {
                    throw Error('Try to move a group inside itself.. Weird!');
                }
                parentGroup = this._getParentElement(this.rootGroup, parentGroup);
            }
        }

        let elem: SbElement = this.removeElement(fromGroup, elemToMove);
        let newPosition: number;

        switch(true) {
            case (toGroup.children.length == 0):
                newPosition = 0;
                break;

            case (destIndex == 0):
                console.log('ajout au debut du groupe');
                newPosition = toGroup.children[0].position - 1;
                break;

            case (destIndex == toGroup.children.length):
                console.log('ajout à la fin du groupe');
                newPosition = toGroup.children[toGroup.children.length - 1].position + 1;
                break;

            default:
                console.log('ajout à l\'intérieur du groupe');
                newPosition = (toGroup.children[destIndex - 1].position + toGroup.children[destIndex].position)/2;
                break;
        }

        elem.position = newPosition;
        toGroup.children.push(elem);
        this._sortGroupByPosition(toGroup);
        this._highlightRecentAction(elem);
    }

    private _sortGroupByPosition(group:SbGroup):void {
        group.children.sort((a, b) => a.position - b.position);
    }

    private _highlightRecentAction(element:SbElement):void {
        element.recentAction = true;
        setTimeout(() => element.recentAction = false, 2000);
    }

    private _getParentElement(group:SbGroup, element:SbElement):SbGroup {
        let parentElem: SbGroup;

        group.children.some((elem:SbElement) => {
            if (elem === element) {
                parentElem = group;
                return true;
            }
            if (elem instanceof SbGroup && !parentElem) {
                parentElem = this._getParentElement(elem, element);
            }
        });

        return parentElem;
    }

}
import {Directive, ElementRef} from 'angular2/angular2';

import {SbElement, SegmentBuilderService} from './../../services/SegmentBuilderService';


@Directive({
    selector: '[drag]',
    inputs: [
        'drag'
    ],
    host: {
        'draggable': 'true',
        '[class.draggingMe]': 'isDraggingMe',
        '(dragstart)': 'onDragStart($event)',
        '(dragend)': 'onDragEnd($event)'
    }
})
export class DragDirective {
    drag:SbElement
    isDraggingMe:boolean = false

    constructor(private segmentBuilder:SegmentBuilderService, private elRef: ElementRef) {}

    onInit() {}

    onDragStart(e:DragEvent) {
        this.segmentBuilder.draggingElement = this.drag;

        this.isDraggingMe = true;

        this.elRef.nativeElement.style.opacity = .5;
        this.elRef.nativeElement.style.outline = '1px solid red';

        e.dataTransfer.setData('text/plain', 'go');
        e.dataTransfer.effectAllowed = 'move';
        e.stopPropagation();
    }

    onDragEnd(e:DragEvent) {
        setTimeout((): void => {
            this.segmentBuilder.draggingElement = null;
            this.isDraggingMe = false;
        });

        this.elRef.nativeElement.style.opacity = 1;
        this.elRef.nativeElement.style.outline = 0;

        e.stopPropagation();
    }
}
import {Component, View, ElementRef, FORM_DIRECTIVES} from 'angular2/angular2';

import {SbGroup, SbCriterion, SegmentBuilderService} from './../../services/SegmentBuilderService';


@Component({
    selector: 'drop-target',
    properties: [
        'group',
        'index',
        'hidewhenids'
    ],
    host: {
        '[class.showMe]': '_showMe()',
        '[class.hoverMe]': 'isHover',
        '(dragenter)': 'onDragEnter($event)',
        '(dragleave)': 'onDragLeave($event)',
        '(dragover)': 'onDragOver($event)',
        '(dragout)': 'onDragOut($event)',
        '(drop)': 'onDrop($event)'
    }
})
@View({
    directives: [],
    template: `
        <div class="glyphicon glyphicon-arrow-right"></div>
    `
})

export class DropTargetComponent {
    group:SbGroup
    index:number
    hidewhenids:Array<number> = []
    isHover:boolean = false

    constructor(private segmentBuilder:SegmentBuilderService) {}

    onInit() {
        console.log(this.hidewhenids);
    }

    private _showMe() {
        return !!this.segmentBuilder.draggingElement && this.hidewhenids.indexOf(this.segmentBuilder.draggingElement.id) == -1;
    }

    onDragEnter(e:Event) {
        e.preventDefault();
        e.stopPropagation();
        this.isHover = true;
    }

    onDragLeave(e:Event) {
        e.stopPropagation();
        this.isHover = false;
    }

    onDragOver(e:Event) {
        e.preventDefault();
        e.stopPropagation();
    }

    onDragOut(e:Event) {
        e.preventDefault();
        e.stopPropagation();
    }

    onDrop(e:Event) {
        this.isHover = false;
        this.segmentBuilder.moveElement(this.segmentBuilder.draggingElement, this.group, this.index);
    }
}
import {Component, View, NgFor, FORM_DIRECTIVES} from 'angular2/angular2';

import {SbGroup, SbCriterion, SegmentBuilderService} from './../../services/SegmentBuilderService';


@Component({
    selector: 'sb-criterion',
    properties: [
        'parentgroup',
        'criterion'
    ],
    host: {
        '[class.recentAction]': 'criterion.recentAction'
    }
})
@View({
    directives: [FORM_DIRECTIVES],
    template: `
        <div class="form-inline">
            <span class="badge">[{{ criterion.position }}] Criterion ({{ criterion.id }})</span> - 
            <input class="form-control" type="text" [(ng-model)]="criterion.name" />
            <button class="btn btn-sm btn-danger" (click)="segmentBuilder.removeElement(parentgroup, criterion)"><span class="glyphicon glyphicon-trash"></span></button>
        </div>
    `
})

export class SbCriterionComponent {
    parentgroup:SbGroup
    criterion:SbCriterion

    constructor(private segmentBuilder:SegmentBuilderService) {}
}
import {Component, View, NgIf, NgFor, ElementRef} from 'angular2/angular2';

import {SbCriterionComponent} from './SbCriterionComponent';
import {SbSegmentComponent} from './SbSegmentComponent';
import {DragDirective} from './DragDirective';
import {DropTargetComponent} from './DropTargetComponent';

import {SbGroup, SegmentBuilderService} from './../../services/SegmentBuilderService';


@Component({
    selector: 'sb-group',
    properties: [
        'parentgroup',
        'group',
        'level'
    ],
    host: {
        '[class]': '\'panel panel-default level-\' + level',
        //'[class.recentAction]': 'group.recentAction'
    }
})
@View({
    directives: [NgIf, NgFor, SbCriterionComponent, SbSegmentComponent, SbGroupComponent, DragDirective, DropTargetComponent],
    template: `
        <div class="panel-heading">
            <button class="btn btn-sm btn-default" (click)="isOpen = !isOpen"><span class="glyphicon glyphicon-{{ isOpen ? 'triangle-bottom' : 'triangle-right' }}"></span></button>
            <span class="badge">[{{ group.position }}] Group ({{ group.id }})</span>
            <button class="btn btn-sm btn-primary" (click)="segmentBuilder.addElement(group, 'group')">+ Group</button>
            <button class="btn btn-sm btn-primary" (click)="segmentBuilder.addElement(group, 'criterion')">+ Criterion</button>
            <button class="btn btn-sm btn-primary" (click)="segmentBuilder.addElement(group, 'segment')">+ Segment</button>
            <button class="btn btn-sm btn-danger" *ng-if="parentgroup" (click)="segmentBuilder.removeElement(parentgroup, group)"><span class="glyphicon glyphicon-trash"></span></button>
        </div>
        <div class="panel-body" [hidden]="!isOpen">
          
            <drop-target *ng-if="!group.children.length"
                  index="0" [group]="group" [level]="level"></drop-target>

            <drop-target *ng-if="group.children.length"
                  index="0" [group]="group" [level]="level"  [hidewhenids]="[group.children[0].id]"></drop-target>

            <div *ng-for="#m of group.children; #i = index">
                <sb-criterion *ng-if="m.type == 'criterion'" 
                      [parentgroup]="group" [criterion]="m" [drag]="m"></sb-criterion>

                <sb-segment *ng-if="m.type == 'segment'" 
                      [parentgroup]="group" [segment]="m" [drag]="m"></sb-segment>

                <sb-group *ng-if="m.type == 'group'" 
                      [parentgroup]="group" [group]="m" [level]="level + 1" [drag]="m"></sb-group>

                <drop-target *ng-if="i < group.children.length - 1"
                      [index]="i + 1" [group]="group" [level]="level" [hidewhenids]="[group.children[i].id, group.children[i + 1].id]"></drop-target>

                <drop-target *ng-if="i == group.children.length - 1"
                      [index]="i + 1" [group]="group" [level]="level" [hidewhenids]="[group.children[i].id]"></drop-target>
            </div>
        </div>
    `
})

export class SbGroupComponent {
    parentgroup:SbGroup
    group:SbGroup
    level:number
    isOpen:boolean = true

    constructor(private segmentBuilder:SegmentBuilderService) {}

    onInit() {
        this.level = Number(this.level);
    }
}

/*  [ng-switch]="m.type"
<sb-criterion [ng-switch-when]="'criterion'" [criterion]="m"></sb-criterion>
<sb-segment [ng-switch-when]="'segment'" [segment]="m"></sb-segment>
<sb-group [ng-switch-when]="'group'" [group]="m"></sb-group>

<div *ng-for="#m of group.children; #i = index">

<template ng-for #m [ng-for-of]="group.children" #i="index">
</template>
*/
import {Component, View, CORE_DIRECTIVES, FORM_DIRECTIVES} from 'angular2/angular2';

import {SbGroup, SbSegment, SegmentBuilderService} from './../../services/SegmentBuilderService';


@Component({
    selector: 'sb-segment',
    properties: [
        'parentgroup',
        'segment'
    ],
    host: {
        '[class.recentAction]': 'segment.recentAction'
    }
})
@View({
    directives: [CORE_DIRECTIVES, FORM_DIRECTIVES],
    template: `
        <div class="form-inline">
            <span class="badge">[{{ segment.position }}] Segment ({{ segment.id }})</span> - 
            <input class="form-control" type="text" [(ng-model)]="segment.name" />
            <button class="btn btn-sm btn-danger" (click)="segmentBuilder.removeElement(parentgroup, segment)"><span class="glyphicon glyphicon-trash"></span></button>
        </div>
    `
})

export class SbSegmentComponent {
    parentgroup:SbGroup
    segment:SbSegment

    constructor(private segmentBuilder:SegmentBuilderService) {}
}
import {Component, View, NgFor} from 'angular2/angular2';
import {SbGroupComponent} from './SbGroupComponent';

import {SbGroup, SegmentBuilderService} from './../../services/SegmentBuilderService';

@Component({
    selector: 'segment-builder',
    providers: [SegmentBuilderService]
})
@View({
    directives: [SbGroupComponent, NgFor],
    template: `
        <div class="container-fluid">
            <h2>Segment Builder:</h2>
            <sb-group [group]="mainGroup" level="0"></sb-group>
        </div>
    `
})

export class SegmentBuilderComponent {
    mainGroup:SbGroup

    constructor(segmentBuilder:SegmentBuilderService) {
        this.mainGroup = segmentBuilder.rootGroup;
    }
}
//our root app component
import {Component, View, CORE_DIRECTIVES} from 'angular2/angular2'
import {SegmentBuilderComponent} from './segmentBuilder/SegmentBuilderComponent'

@Component({
  selector: 'my-app'
})
@View({
  template: `
    <div>
      <segment-builder></segment-builder>
    </div>
  `,
  directives: [SegmentBuilderComponent]
})
export class App {
  constructor() {}
}