From e2a48299788a44d8cdec4ecb8ad20fb986e5cd72 Mon Sep 17 00:00:00 2001 From: Alexey Zuev Date: Fri, 7 Jul 2017 15:48:18 +0400 Subject: [PATCH] Initial commit --- app/app.component.html | 12 +- app/app.component.ts | 51 ++- app/app.module.ts | 5 +- app/kanban/dragula.directive.ts | 53 +++ app/kanban/dragula.service.ts | 109 +++++ app/kanban/editable.component.ts | 49 ++ app/kanban/kanban-board.service.ts | 292 ++++++++++++ app/kanban/kanban.component.ts | 142 ++++++ app/kanban/kanban.module.ts | 24 + app/kanban/lane.component.ts | 41 ++ app/kanban/model.ts | 21 + app/kanban/notes.component.ts | 56 +++ images/drag-cursor.png | Bin 0 -> 184 bytes images/drag-pattern.png | Bin 0 -> 122 bytes package.json | 4 + style.css | 708 +++++++++++++++++++++++++++++ systemjs.config.js | 3 + 17 files changed, 1565 insertions(+), 5 deletions(-) create mode 100644 app/kanban/dragula.directive.ts create mode 100644 app/kanban/dragula.service.ts create mode 100644 app/kanban/editable.component.ts create mode 100644 app/kanban/kanban-board.service.ts create mode 100644 app/kanban/kanban.component.ts create mode 100644 app/kanban/kanban.module.ts create mode 100644 app/kanban/lane.component.ts create mode 100644 app/kanban/model.ts create mode 100644 app/kanban/notes.component.ts create mode 100644 images/drag-cursor.png create mode 100644 images/drag-pattern.png diff --git a/app/app.component.html b/app/app.component.html index fb8dbbe..33ac567 100644 --- a/app/app.component.html +++ b/app/app.component.html @@ -1 +1,11 @@ -

Hooray

+

Kanban board

+ diff --git a/app/app.component.ts b/app/app.component.ts index 2156b7f..57f986e 100644 --- a/app/app.component.ts +++ b/app/app.component.ts @@ -1,8 +1,53 @@ import { Component } from '@angular/core'; +import { Lane } from './kanban/model'; +import { KanbanBoardService } from './kanban/kanban-board.service'; +import { Observable } from 'rxjs/Observable'; + @Component({ - selector: 'my-app', - templateUrl: `./app.component.html` + selector: 'my-app', + templateUrl: `./app.component.html` }) -export class AppComponent {} +export class AppComponent { + lanes$: Observable; + + constructor(private kanbanBoardService: KanbanBoardService) {} + + ngOnInit() { + this.lanes$ = this.kanbanBoardService.lanes; + this.kanbanBoardService.load(); + } + + onLaneMoved(event: { fromIndex: number, toIndex: number }) { + this.kanbanBoardService.moveLane(event.fromIndex, event.toIndex); + } + + onLaneEdited(event: { id: string, name: string, editing: boolean }) { + this.kanbanBoardService.editLane(event); + } + + onNoteCreated(id: string) { + this.kanbanBoardService.attachNote(id) + } + + onNoteEdited(event: { laneId: string, id: string, text: string, editing: boolean }) { + this.kanbanBoardService.editNote(event); + } + + onNoteMoved(event: { fromLaneId: string, fromIndex: number, toLaneId: string, toIndex?: number }) { + this.kanbanBoardService.moveNote(event); + } + + onNoteDeleted(event: { laneId: string, id: string }) { + this.kanbanBoardService.deleteNote(event); + } + + onNotesStatusChanged(event: { laneId: string, id: string, status: any}) { + this.kanbanBoardService.changeNoteStatus(event); + } + + reset() { + this.kanbanBoardService.reset(); + } +} diff --git a/app/app.module.ts b/app/app.module.ts index ffe1c3f..f2bd7c1 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -2,14 +2,17 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; +import { KanbanModule } from './kanban/kanban.module'; +import { KanbanBoardService } from './kanban/kanban-board.service'; @NgModule({ imports: [ - BrowserModule + BrowserModule, KanbanModule ], declarations: [ AppComponent ], + providers: [KanbanBoardService], bootstrap: [ AppComponent ] }) export class AppModule { } diff --git a/app/kanban/dragula.directive.ts b/app/kanban/dragula.directive.ts new file mode 100644 index 0000000..f97c51e --- /dev/null +++ b/app/kanban/dragula.directive.ts @@ -0,0 +1,53 @@ +import { Directive, Input, ElementRef, OnInit, OnChanges, SimpleChange } from '@angular/core'; +import { DragulaService } from './dragula.service'; +import * as dragula from 'dragula'; + +@Directive({selector: '[dragula]'}) +export class DragulaDirective implements OnInit, OnChanges { + @Input() public dragula: string; + @Input() public dragulaModel: any; + @Input() public dragulaOptions: any; + private container: any; + private drake: any; + + public constructor(el: ElementRef, private dragulaService: DragulaService) { + this.dragulaService = dragulaService; + this.container = el.nativeElement; + } + + public ngOnInit(): void { + let bag = this.dragulaService.find(this.dragula); + if (bag) { + this.drake = bag.drake; + this.checkModel(); + this.drake.containers.push(this.container); + } else { + this.drake = dragula([this.container], Object.assign({}, this.dragulaOptions)); + this.checkModel(); + this.dragulaService.add(this.dragula, this.drake); + } + } + + private checkModel() { + if (!this.dragulaModel) { + return; + } + + if (this.drake.models) { + this.drake.models.push(this.dragulaModel); + } else { + this.drake.models = [this.dragulaModel]; + } + } + + public ngOnChanges(changes: {dragulaModel?: SimpleChange}): void { + if (this.drake && changes && changes.dragulaModel) { + if (this.drake.models) { + let modelIndex = this.drake.models.indexOf(changes.dragulaModel.previousValue); + this.drake.models.splice(modelIndex, 1, changes.dragulaModel.currentValue); + } else { + this.drake.models = [changes.dragulaModel.currentValue]; + } + } + } +} diff --git a/app/kanban/dragula.service.ts b/app/kanban/dragula.service.ts new file mode 100644 index 0000000..9a6801d --- /dev/null +++ b/app/kanban/dragula.service.ts @@ -0,0 +1,109 @@ +import { Injectable, EventEmitter } from '@angular/core'; + +import * as dragula from 'dragula'; + +@Injectable() +export class DragulaService { + public cancel: EventEmitter = new EventEmitter(); + public cloned: EventEmitter = new EventEmitter(); + public drag: EventEmitter = new EventEmitter(); + public dragend: EventEmitter = new EventEmitter(); + public drop: EventEmitter = new EventEmitter(); + public out: EventEmitter = new EventEmitter(); + public over: EventEmitter = new EventEmitter(); + public remove: EventEmitter = new EventEmitter(); + public shadow: EventEmitter = new EventEmitter(); + public dropModel: EventEmitter = new EventEmitter(); + private events: string[] = [ + 'cancel', 'cloned', 'drag', 'dragend', 'drop', 'out', 'over', + 'remove', 'shadow', 'dropModel' + ]; + private bags: any[] = []; + + public add(name: string, drake: any): any { + let bag = this.find(name); + if (bag) { + throw new Error('Bag named: "' + name + '" already exists.'); + } + bag = {name, drake}; + this.bags.push(bag); + if (drake.models) { // models to sync with (must have same structure as containers) + this.handleModels(name, drake); + } + if (!bag.initEvents) { + this.setupEvents(bag); + } + return bag; + } + + public find(name: string): any { + for (let bag of this.bags) { + if (bag.name === name) { + return bag; + } + } + } + + public destroy(name: string): void { + let bag = this.find(name); + let i = this.bags.indexOf(bag); + this.bags.splice(i, 1); + bag.drake.destroy(); + } + + public setOptions(name: string, options: any): void { + let bag = this.add(name, dragula(options)); + this.handleModels(name, bag.drake); + } + + private handleModels(name: string, drake: any): void { + let dragElm: any; + let dragIndex: number; + let dropIndex: number; + let sourceModel: any; + drake.on('drag', (el: any, source: any) => { + dragElm = el; + dragIndex = this.domIndexOf(el, source); + }); + drake.on('drop', (dropElm: any, target: any, source: any) => { + if (!drake.models || !target) { + return; + } + dropIndex = this.domIndexOf(dropElm, target); + sourceModel = drake.models[drake.containers.indexOf(source)]; + + if (target === source) { + sourceModel.splice(dropIndex, 0, sourceModel.splice(dragIndex, 1)[0]); + } else { + let notCopy = dragElm === dropElm; + let targetModel = drake.models[drake.containers.indexOf(target)]; + let dropElmModel = notCopy ? sourceModel[dragIndex] : JSON.parse(JSON.stringify(sourceModel[dragIndex])); + + if (notCopy) { + sourceModel.splice(dragIndex, 1); + } + targetModel.splice(dropIndex, 0, dropElmModel); + target.removeChild(dropElm); // element must be removed for ngFor to apply correctly + } + this.dropModel.emit([name, dropElm, target, source, dragIndex, dropIndex]); + }); + } + + private setupEvents(bag: any): void { + bag.initEvents = true; + let that: any = this; + let emitter = (type: any) => { + function replicate(): void { + let args = Array.prototype.slice.call(arguments); + that[type].emit([bag.name].concat(args)); + } + + bag.drake.on(type, replicate); + }; + this.events.forEach(emitter); + } + + private domIndexOf(child: any, parent: any): any { + return Array.prototype.indexOf.call(parent.children, child); + } +} diff --git a/app/kanban/editable.component.ts b/app/kanban/editable.component.ts new file mode 100644 index 0000000..af6969d --- /dev/null +++ b/app/kanban/editable.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'editable', + template: ` + + + +
{{value}}
+ × +
+
+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditableComponent { + @Input() value: string; + @Input() editing: boolean; + @Input() onDelete: boolean; + + @Output() valueClicked: EventEmitter = new EventEmitter(); + + @Output() edited: EventEmitter = new EventEmitter(); + + @ViewChild('autofocus') autofocus: ElementRef; + + handleValueClick() { + this.valueClicked.emit(); + } + + handleFinishEdit(e) { + if ((e.type === 'keypress')) { + return; + } + + const value = e.target.value; + this.edited.emit(value); + } + + ngOnChanges() { + if (this.editing) { + setTimeout(() => this.autofocus.nativeElement.focus()) + } + } + +} diff --git a/app/kanban/kanban-board.service.ts b/app/kanban/kanban-board.service.ts new file mode 100644 index 0000000..244686e --- /dev/null +++ b/app/kanban/kanban-board.service.ts @@ -0,0 +1,292 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { UUID } from 'angular2-uuid'; + +import { Lane, Note, NoteStatus } from './model'; + +import * as localforage from 'localforage'; + + +const defaultState: Lane[] = [ + { + id: UUID.UUID(), + name: 'Todo', + editing: false, + notes: [ + { + id: UUID.UUID(), + status: NoteStatus.InProgress, + text: 'Navbar and Office Navigation Bar Enhancements', + editing: false + }, + { + id: UUID.UUID(), + status: NoteStatus.Overdue, + text: 'PictureEdit - Trim Image by Mask/Shape', + editing: false + }, + { + id: UUID.UUID(), + status: NoteStatus.InProgress, + text: 'Calendar Control - Touch Mode', + editing: false + } + ] + }, + { + id: UUID.UUID(), + name: 'In Progress', + editing: false, + notes: [ + { + id: UUID.UUID(), + status: NoteStatus.InProgress, + text: 'Stub Glyphs for BarItems in Ribbon & BarManager', + editing: false + } + ] + }, + { + id: UUID.UUID(), + name: 'Review', + editing: false, + notes: [] + }, + { + id: UUID.UUID(), + name: 'Done', + editing: false, + notes: [ + { + id: UUID.UUID(), + status: NoteStatus.Completed, + text: 'MVVM Core Enhancements', + editing: false + }, + { + id: UUID.UUID(), + status: NoteStatus.InProgress, + text: 'Filtering UI Enhancements', + editing: false + }, + { + id: UUID.UUID(), + status: NoteStatus.InProgress, + text: 'Master Detail Mode - Single Vertical Scrollbar for Multiple Views', + editing: false + } + ] + } +]; + +const storage: LocalForage = localforage.createInstance({ + name: 'kanban', +}); + + +@Injectable() +export class KanbanBoardService { + private _lanes: BehaviorSubject = new BehaviorSubject([]); + private dataStore: { lanes: Lane[] } = {lanes: []}; + + get lanes(): Observable { + return this._lanes.asObservable(); + } + + load() { + storage.getItem('state').then((data: Lane[]) => { + this.dataStore.lanes = data || defaultState; + this.dispatch(); + }); + this.lanes.subscribe((state) => storage.setItem('state', state)); + } + + moveLane(fromIndex: number, toIndex: number) { + let newArr = [...this.dataStore.lanes]; + /* let movedLane = newArr[fromIndex]; + newArr.splice(fromIndex, 1); + newArr.splice(toIndex, 0, movedLane); + */ + this.dataStore.lanes = newArr; + this.dispatch(); + } + + editLane(action: EditLaneAction) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== action.id) { + return lane; + } + + return Object.assign({}, lane, { + name: action.name, + editing: action.editing + }); + }); + this.dispatch(); + } + + attachNote(id: string) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== id) { + return lane; + } + + const note: Note = { + id: UUID.UUID(), + editing: true, + text: '', + status: NoteStatus.InProgress + }; + + return Object.assign({}, lane, {notes: [...lane.notes, note]}); + }); + + this.dispatch(); + } + + editNote(action: EditNoteAction) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== action.laneId) { + return lane; + } + + lane.notes = lane.notes.map(note => { + if (note.id !== action.id) { + return note; + } + + return Object.assign({}, note, { + text: action.text, + editing: action.editing + }); + }); + return lane; + }); + + this.dispatch(); + } + + moveNote(action: MoveNoteAction) { + if (action.fromLaneId === action.toLaneId) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== action.fromLaneId) { + return lane; + } + + let newArr = [...lane.notes]; + /* let movedNote = newArr[action.fromIndex]; + newArr.splice(action.fromIndex, 1); + newArr.splice(action.toIndex, 0, movedNote);*/ + + return Object.assign({}, lane, {notes: newArr}); + }); + } else { + let movedNote; + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id === action.fromLaneId) { + let newArr = [...lane.notes]; + // movedNote = newArr.splice(action.fromIndex, 1)[0]; + + return Object.assign({}, lane, {notes: newArr}); + } + + return lane; + }); + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id === action.toLaneId) { + let newArr = [...lane.notes]; + /* if(action.toIndex === null) { + newArr.push(movedNote); + } else { + newArr.splice(action.toIndex, 0, movedNote); + }*/ + + return Object.assign({}, lane, {notes: newArr}); + } + + return lane; + }); + } + + this.dispatch(); + } + + deleteNote(action: DeleteNoteAction) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== action.laneId) { + return lane; + } + let newArr = lane.notes.filter(x => x.id !== action.id); + + return Object.assign({}, lane, {notes: newArr}); + }); + + this.dispatch(); + } + + changeNoteStatus(action: ChangeNoteStatusAction) { + this.dataStore.lanes = this.dataStore.lanes.map((lane: Lane) => { + if (lane.id !== action.laneId) { + return lane; + } + + lane.notes = lane.notes.map(note => { + if (note.id !== action.id) { + return note; + } + + return Object.assign({}, note, { + status: +action.status + }); + }); + return lane; + }); + + this.dispatch(); + } + + dispatch() { + this._lanes.next(Object.assign({}, this.dataStore).lanes); + } + + reset() { + storage.clear(); + window.location.reload(); + } +} + + +export interface MoveLaneAction { + fromIndex: number; + toIndex: number; +} + +export interface MoveNoteAction { + fromLaneId: string; + fromIndex: number; + toLaneId: string; + toIndex?: number +} + +export interface EditLaneAction { + id: string; + name: string; + editing: boolean; +} + +export interface EditNoteAction { + laneId: string; + id: string; + text: string; + editing: boolean; +} + +export interface DeleteNoteAction { + laneId: string; + id: string; +} + +export interface ChangeNoteStatusAction extends DeleteNoteAction { + status: any; +} \ No newline at end of file diff --git a/app/kanban/kanban.component.ts b/app/kanban/kanban.component.ts new file mode 100644 index 0000000..ea62186 --- /dev/null +++ b/app/kanban/kanban.component.ts @@ -0,0 +1,142 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { Lane } from './model'; +import 'rxjs/add/operator/first'; +import { DragulaService } from './dragula.service'; + +@Component({ + selector: 'kanban', + template: ` +
+ + +
+ `, + host: { class: 'kanban' }, + providers: [DragulaService], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class KanbanComponent implements OnDestroy { + @Input() lanes: Lane[]; + + @Output() laneMoved: EventEmitter<{ fromIndex: number, toIndex: number }> = new EventEmitter(); + @Output() laneEdited: EventEmitter<{ id: string, name: string, editing: boolean }> = new EventEmitter(); + + @Output() createNote: EventEmitter = new EventEmitter(); + @Output() noteEdited: EventEmitter<{ laneId: string, id: string, text: string, editing: boolean }> = new EventEmitter(); + @Output() noteMoved: EventEmitter<{ fromLaneId: string, fromIndex: number, toLaneId: string, toIndex?: number }> = new EventEmitter(); + @Output() noteDeleted: EventEmitter<{ laneId: string, id: string }> = new EventEmitter(); + @Output() noteStatusChanged: EventEmitter = new EventEmitter(); + + drakeName = 'lanesContainer'; + + dragulaOptions = { + moves: (el, container, handle) => { + return handle.classList.contains('lane__drag'); + } + }; + + trackById(i: number, item: Lane) { + return item.id; + } + + constructor(private dragulaService: DragulaService) { + this.dragulaService.dropModel.subscribe(([bagName, el, target, source, dragIndex, dropIndex]) => { + if(bagName === this.drakeName) { + this.laneMoved.emit({ fromIndex: dragIndex, toIndex: dropIndex }) + } + if(bagName === 'notesContainer') { + this.noteMoved.emit({ fromLaneId: source.dataset.key, fromIndex: dragIndex, toLaneId: target.dataset.key, toIndex: dropIndex }) + } + }); + } + + ngAfterViewInit() { + /* let drake = dragula([this.lanesContainer.nativeElement], { + moves: (el, container, handle) => { + return handle.classList.contains('lane__drag'); + } + }); + drake.on('drop', (el, target, source, sibling) => { + const fromIndex = el.dataset.index; + let toIndex; + if (sibling) { + if (fromIndex < sibling.dataset.index) { + toIndex = sibling.dataset.index - 1; + } else { + toIndex = sibling.dataset.index; + } + } else { + toIndex = this.lanes.length - 1; + } + console.log('from ' + fromIndex + ' to ' + toIndex); + + this.laneMoved.emit({ fromIndex, toIndex}) + });*/ + + /* this.drakeNotes = dragula(); + this.drakeNotes.on('drop', (el, target, source, sibling) => { + if(!target) return; + const fromIndex = el.dataset.index; + let toIndex; + + const fromLaneId = source.dataset.key; + const toLaneId = target.dataset.key; + + if(fromLaneId === toLaneId) { + if (sibling) { + if (fromIndex < sibling.dataset.index) { + toIndex = sibling.dataset.index - 1; + } else { + toIndex = sibling.dataset.index; + } + } else { + toIndex = this.lanes.length - 1; + } + } else { + toIndex = sibling ? sibling.dataset.index : null; + } + + // el.parentNode.removeChild(el); + this.noteMoved.emit({ fromLaneId, fromIndex, toLaneId, toIndex }) + });*/ + } + + onEditLane(id: string, currentName: string, newName: string) { + const name = newName !== undefined ? newName : currentName; + const editing = newName === undefined; + + this.laneEdited.emit({ id, name, editing }) + } + + onCreateNote(id: string) { + this.createNote.emit(id); + } + + onEditNote(laneId: string, event: { id: string, currentText: string, text: string }) { + const text = event.text !== undefined ? event.text : event.currentText; + const editing = event.text === undefined; + + this.noteEdited.emit({ laneId, id: event.id, text, editing }); + } + + onDeleteNote(laneId: string, event: any) { + this.noteDeleted.emit({laneId, id: event}); + } + + onNoteStatusChanged(laneId: string, { id, status }) { + this.noteStatusChanged.emit({ laneId, id, status }); + } + + ngOnDestroy() { + this.dragulaService.destroy(this.drakeName); + this.dragulaService.destroy('notesContainer'); + } +} diff --git a/app/kanban/kanban.module.ts b/app/kanban/kanban.module.ts new file mode 100644 index 0000000..fa91925 --- /dev/null +++ b/app/kanban/kanban.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { KanbanComponent } from './kanban.component'; +import { LaneComponent } from './lane.component'; +import { EditableComponent } from './editable.component'; +import { NotesComponent } from './notes.component'; +import { DragulaDirective } from './dragula.directive'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + KanbanComponent, + LaneComponent, + EditableComponent, + NotesComponent, + DragulaDirective + ], + exports: [KanbanComponent] +}) +export class KanbanModule { +} diff --git a/app/kanban/lane.component.ts b/app/kanban/lane.component.ts new file mode 100644 index 0000000..fab239b --- /dev/null +++ b/app/kanban/lane.component.ts @@ -0,0 +1,41 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'lane', + template: ` +

+
{{ lane.name }}
+ +

+ + `, + host: { class: 'lane' }, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LaneComponent { + @Input() lane: any; + + @Output() createNote: EventEmitter = new EventEmitter(); + @Output() editLane: EventEmitter = new EventEmitter(); + @Output() editNote: EventEmitter = new EventEmitter(); + @Output() deleteNote: EventEmitter = new EventEmitter(); + @Output() noteStatusChanged: EventEmitter = new EventEmitter(); + + onEditLane(name: string) { + this.editLane.emit(name); + } + + onEditNote(event: any) { + this.editNote.emit(event) + } + + onDeleteNote(event: any) { + this.deleteNote.emit(event); + } +} diff --git a/app/kanban/model.ts b/app/kanban/model.ts new file mode 100644 index 0000000..a75fb70 --- /dev/null +++ b/app/kanban/model.ts @@ -0,0 +1,21 @@ + + +export enum NoteStatus { + InProgress, + Overdue, + Completed +} + +export class Note { + id: string; + editing: boolean; + text: string; + status: NoteStatus +} + +export interface Lane { + id: string; + name: string; + editing: boolean; + notes: Note[]; +} diff --git a/app/kanban/notes.component.ts b/app/kanban/notes.component.ts new file mode 100644 index 0000000..9cb91eb --- /dev/null +++ b/app/kanban/notes.component.ts @@ -0,0 +1,56 @@ +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Note } from './model'; + +@Component({ + selector: 'notes', + template: ` +
    +
  • + + +
    + + +
    + × +
  • +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotesComponent { + @Input() laneId: string; + @Input() notes: Note[]; + + drakeName = 'notesContainer'; + + dragulaOptions = { + moves: (el, container, handle) => { + return handle.classList.contains('note__drag'); + } + }; + + @Output() editNote: EventEmitter = new EventEmitter(); + @Output() noteStatusChanged: EventEmitter = new EventEmitter(); + @Output() deleteNote: EventEmitter = new EventEmitter(); + + @ViewChild('notesContainer') notesContainer: ElementRef; + + onEditNote(id: string, currentText: string, newText: string) { + this.editNote.emit({ id, currentText, text: newText }) + } + + handleDelete(id: string) { + this.deleteNote.emit(id); + } +} diff --git a/images/drag-cursor.png b/images/drag-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..d7a86de1c93155a7552ccf829ecb1f52c8d1ace3 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^DIm$_x=7r7Gp_} zUoeBivm0qZPOPVkV~EG`x0e=*HW=`EN^*MHdZzcfJbP0l+XkKpYuY7 literal 0 HcmV?d00001 diff --git a/images/drag-pattern.png b/images/drag-pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..f08fded4fddd263b59c49259f00db5c439254d68 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp@KrGJ03?$c{&=mqwJOMr-u0VRtnl+m?Zu