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 0000000..d7a86de Binary files /dev/null and b/images/drag-cursor.png differ diff --git a/images/drag-pattern.png b/images/drag-pattern.png new file mode 100644 index 0000000..f08fded Binary files /dev/null and b/images/drag-pattern.png differ diff --git a/package.json b/package.json index 23f470a..7a32047 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,13 @@ "@angular/platform-browser": "4.2.5", "@angular/platform-browser-dynamic": "4.2.5", "@angular/router": "4.2.5", + "angular2-uuid": "^1.1.1", "core-js": "^2.4.1", + "dragula": "^3.7.2", + "localforage": "^1.5.0", "rxjs": "^5.1.0", "systemjs": "0.19.41", + "uuid": "^3.1.0", "zone.js": "^0.8.11" }, "devDependencies": { diff --git a/style.css b/style.css index e69de29..46b16cb 100644 --- a/style.css +++ b/style.css @@ -0,0 +1,708 @@ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + */ +html { + font-family: sans-serif; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ +} + +/** + * Remove default margin. + */ +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ +audio, +canvas, +progress, +video { + display: inline-block; + /* 1 */ + vertical-align: baseline; + /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ +/** + * Remove the gray background color from active links in IE 10. + */ +a { + background-color: transparent; +} + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ +/** + * Remove border when inside `a` element in IE 8/9/10. + */ +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ +/** + * Address margin not present in IE 8/9 and Safari. + */ +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ +hr { + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ +button, +input, +optgroup, +select, +textarea { + color: inherit; + /* 1 */ + font: inherit; + /* 2 */ + margin: 0; + /* 3 */ +} + +textarea { + resize: vertical; +} +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + box-sizing: content-box; + /* 2 */ +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ +legend { + border: 0; + /* 1 */ + padding: 0; + /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ +/** + * Remove most spacing between table cells. + */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} + +* { + box-sizing: border-box; +} + +body { + background-attachment: fixed; + background-color: #e8e8e8; + color: #5a5050; + font-family: 'Source Sans Pro', sans-serif; + font-size: 16px; + font-weight: 400; + height: 100%; +} + +html { + height: 100%; +} + +a { + color: #a6206a; + text-decoration: none; + transition: color ease-in-out .4s; +} + +a:hover { + color: #7b184f; +} + +p { + font-size: 1em; + font-weight: 300; +} + +h1, +h2 { + margin: 0; +} + +h2 { + font-size: 1.2em; +} + +img { + max-width: 100%; +} + +button { + background-color: #727272; + border: 0; + border-radius: 2px; + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2); + color: #fff; + padding: .5em 1em; + transition: all .3s ease-out; +} + +button:focus { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + outline: 0; +} + +input, +.input, +textarea { + border: 0; + font-weight: normal; + width: 100%; + padding: .6em 0.3em; + background: transparent; +} + +textarea:focus, +input:focus { + outline: none; +} + +.kanban { + flex: 1 1 auto; + margin: 0 auto; + width: 95%; + display: flex; + position: relative; + +} + +my-app { + height: 100%; + display: flex; + flex-direction: column; +} + +.kanban-title { + font-weight: 400; + margin-top: 20px; + text-align: center; +} + +.add-lane { + background-color: #467075; + margin-bottom: .4em; +} + +.add-lane:hover, .add-lane:focus { + background-color: #467075; +} + +.reset-store { + background-color: rgba(0, 188, 212, 0); + box-shadow: none; + color: #F44336; + font-weight: 600; + font-size: 15px; +} + +.reset-store:hover, .reset-store:focus { + text-decoration: underline; +} + +.lanes { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-pack: center; + justify-content: center; + margin: 0 -15px; + flex: 1 1 auto; +} + + +.lane { + -ms-flex-positive: 1; + flex-grow: 1; + margin-top: 1.5em; + position: relative; + padding: 0 15px; + display: flex; + flex-direction: column; + width: 25%; +} + + + +.lane__name { + padding: 0 .5em; + margin-bottom: 5px; +} + +.lane__editable { + font-weight: bold; +} +.lane__name .editing { + box-shadow: inset 0 -2px #f7d969; +} + +.lane__delete { + background-color: #fff; + border: 1px solid #f3f3f4; + box-shadow: none; + color: #5a5050; + height: 1em; + line-height: 1em; + padding: 0; + position: absolute; + right: .5em; + top: -.5em; + width: 1em; +} + +.lane__drag { + background-color: transparent; + background-image: url("images/drag-cursor.png"); + background-position: center; + cursor: move; + height: 1em; + position: absolute; + right: 20px; + top: 10px; + width: 1em; +} + +.lane__drag:focus { + box-shadow: none; +} + + + +.add-note { + background-color: #4caf50; + line-height: 28px; + padding: 0 10px; + display: inline-block; + font-size: 15px; +} + + + +.notes-list { + list-style-type: none; + padding: 0; + display: block; + min-height: 30px; +} + + +notes { + flex: 1 1 auto; + overflow-y: auto; + padding: 0 5px; +} +.note { + background-color: #fff; + border-left: 5px solid #f06562; + margin: .725em 0; + padding: 15px 1.6em; + position: relative; + list-style: none; +} + +.note__drag { + position: absolute; + background: url("images/drag-pattern.png") no-repeat; + left: 5px; + top: 50%; + width: 13px; + height: 23px; + cursor: move; + margin-top: -11.5px; +} + +.note-status { + margin-top: 10px; + text-align: right; +} + +.note--0 { + border-left-color: #e6c730; +} + +.note--1 { + border-left-color: #f06562; +} + +.note--2 { + border-left-color: #96c244; +} + +.note input, +.note textarea, +.note .input{ + border-bottom: 2px solid transparent; + cursor: text; + color: #4070d6; + font-weight: bold; +} + + +.note .editing { + border-bottom: 2px solid #ccc; +} + +.note .delete { + color: rgba(90, 80, 80, 0.3); + cursor: pointer; + font-size: 2rem; + font-weight: 600; + margin-left: 5px; + position: absolute; + right: 15px; + top: 1px; + vertical-align: sub; +} + + +.gu-mirror { + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + opacity: 0.8; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; + filter: alpha(opacity=80); +} +.gu-hide { + display: none !important; +} +.gu-unselectable { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; +} +.gu-transit { + opacity: 0.2; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; + filter: alpha(opacity=20); +} + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + /* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px;*/ +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + background: #c6c6c6; +} \ No newline at end of file diff --git a/systemjs.config.js b/systemjs.config.js index a80dc6c..cb9dbbf 100644 --- a/systemjs.config.js +++ b/systemjs.config.js @@ -27,6 +27,9 @@ '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js', '@angular/material': 'npm:@angular/material/bundles/material.umd.js', + 'dragula': 'npm:dragula/dist/dragula.js', + 'localforage': 'npm:localforage/dist/localforage.js', + 'angular2-uuid': 'npm:angular2-uuid/index.js', // other libraries 'rxjs': 'npm:rxjs' },