Add falling animation

This commit is contained in:
schmelczerandras 2019-09-01 18:13:21 +02:00
parent 938f3def1f
commit db6a31dd85
20 changed files with 211 additions and 152 deletions

View file

@ -35,7 +35,7 @@
"with": "src/environments/environment.prod.ts"
}
],
"optimization": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
@ -47,8 +47,8 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
"maximumWarning": "20mb",
"maximumError": "50mb"
}
]
}

View file

@ -6,9 +6,7 @@
<p>
You are trying to remove
<span [ngStyle]="{ color: modalService.active.input.baseColor | color }">{{
modalService.active.input.name ? modalService.active.input.name : 'an unnamed tower'
}}</span
<span [ngStyle]="{ color: tower.baseColor | color }">{{ tower.name ? tower.name : 'an unnamed tower' }}</span
>.
</p>

View file

@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
import { Tower } from '../../../../model/tower';
@Component({
selector: 'app-remove-tower',
@ -8,4 +9,6 @@ import { ModalService } from '../../../../services/modal.service';
})
export class RemoveTowerComponent {
constructor(public modalService: ModalService) {}
tower: Tower = this.modalService.active.input;
}

View file

@ -9,11 +9,11 @@
[beforeText]="'Hide create tower button'"
[afterText]="'Show create tower button'"
[default]="!page.userData.hideCreateTowerButton"
(value)="page?.setHideCreateTowerButton(!$event)"
(value)="page.setHideCreateTowerButton(!$event)"
></app-toggle>
</div>
<!-- wrapper for easier styling -->
<p *ngIf="page.towers?.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
<p *ngIf="page.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
<button (click)="deletePage()">Delete current page</button>

View file

@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
import { DataService } from '../../../../services/data.service';
import { Page } from '../../../../model/page';
@ -8,8 +8,10 @@ import { Page } from '../../../../model/page';
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent {
constructor(public modalService: ModalService, public dataService: DataService) {
export class SettingsComponent implements OnInit {
constructor(public modalService: ModalService, public dataService: DataService) {}
ngOnInit() {
this.modalService.active.input.subscribe(p => (this.page = p));
}

View file

@ -1,21 +1,21 @@
<button *ngIf="page && page.towers.length < 5 && !page.userData?.hideCreateTowerButton" (click)="page.addTower()">
Create tower
</button>
<section class="towers" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropDrag($event)">
<app-tower
*ngFor="let tower of page?.towers"
[tower]="tower"
[dateRange]="dateRange"
*ngFor="let tower of towers"
[tower$]="tower.asObservable()"
[dateRange$]="dateRange"
cdkDrag
(cdkDragStarted)="startDrag(page.towers.indexOf(tower))"
(cdkDragStarted)="startDrag(towers.indexOf(tower))"
></app-tower>
<div *ngIf="(page$ | async)?.towers.length < 5 && !(page$ | async)?.userData?.hideCreateTowerButton">
<img src="assets/plus-sign.svg" alt="add tower" class="add-tower" (click)="page.addTower()" />
</div>
</section>
<img
[ngClass]="isDragging ? 'active' : ''"
src="assets/trash.svg"
alt="trashcan"
class="trash"
(pointerenter)="trashEnter()"
(pointerleave)="trashExit()"
(pointerup)="removeTower()"
@ -26,7 +26,6 @@
*ngIf="dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER"
[values]="dates"
[labels]="dateLabels"
(lowerBound)="startDate = $event"
(upperBound)="endDate = $event"
(range)="dateRange.next($event)"
></app-double-slider>
</div>

View file

@ -24,13 +24,31 @@
transition: box-shadow $short-animation-time;
max-width: 800px;
&.cdk-drop-list-dragging {
*:not(.cdk-drag-placeholder) {
transition: transform $long-animation-time cubic-bezier(0, 0, 0.2, 1);
}
}
max-width: 800px;
div {
@include center-child();
img.add-tower {
height: 48px;
@media (max-width: $mobile-width) {
height: 32px;
}
opacity: 0.33;
transition: opacity $long-animation-time;
cursor: pointer;
&:hover {
opacity: 1;
}
}
}
& > * {
max-width: 200px;
@ -65,7 +83,7 @@
}
}
img {
img.trash {
@include square(48px);
padding: 16px;

View file

@ -1,58 +1,62 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Page } from '../../../model/page';
import { ModalService } from '../../../services/modal.service';
import { DataService } from '../../../services/data.service';
import { Observable } from 'rxjs/internal/Observable';
import { Range } from '../../../interfaces/range';
import { Subject } from 'rxjs/internal/Subject';
import { Tower } from '../../../model/tower';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
@Component({
selector: 'app-page',
templateUrl: './page.component.html',
styleUrls: ['./page.component.scss']
})
export class PageComponent {
private _page: Page;
@Input() set page(value: Page) {
if (!value) {
return;
}
export class PageComponent implements OnInit {
@Input() page$: Observable<Page>;
private page: Page;
this._page = value;
this.updateDates();
}
towers: Array<BehaviorSubject<Tower>> = [];
@Output() isDragHappening: EventEmitter<boolean> = new EventEmitter();
get page(): Page {
return this._page;
}
readonly MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER = 3;
readonly MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER = 6;
isDragging = false;
draggedTowerIndex: number;
nearTrashcan = false;
dates: Date[] = [];
startDate: Date;
endDate: Date;
dateRange: Subject<Range<Date>> = new Subject<Range<Date>>();
get dateLabels(): string[] {
return this.dates.map(d => d.toLocaleDateString());
}
get dateRange(): { from: Date; to: Date } {
return this.dates.length >= this.MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER
? {
from: this.startDate,
to: this.endDate
}
: {
from: new Date(0, 0),
to: new Date(10000, 0)
};
}
constructor(private modalService: ModalService, public dataService: DataService) {}
ngOnInit(): void {
this.page$.subscribe(value => {
if (value) {
this.towers = value.towers.map((t, index) => {
if (index < this.towers.length) {
if (this.towers[index].getValue() !== t) {
this.towers[index].next(t);
}
return this.towers[index];
}
return new BehaviorSubject(t);
});
this.page = value;
this.dates = value.towers
.reduce((all, t) => [...t.blocks.map(b => b.created), ...all], [])
.sort((d1, d2) => d1.getTime() - d2.getTime());
}
});
}
dropDrag(event: any) {
this.page.moveTower(event);
this.isDragging = false;
@ -88,10 +92,4 @@ export class PageComponent {
// pass
}
}
private updateDates() {
this.dates = this.page.towers
.reduce((all, t) => [...t.blocks.map(b => b.created), ...all], [])
.sort((d1, d2) => d1.getTime() - d2.getTime());
}
}

View file

@ -1,4 +1,4 @@
<div class="container {{ tasks.length > 0 ? 'show-hover' : '' }}">
<div *ngIf="tasks" class="container {{ tasks.length > 0 ? 'show-hover' : '' }}">
<p class="header" (click)="isOpen = !isOpen">
<strong>
{{ tasks.length == 0 ? '' : tasks.length }}

View file

@ -1,14 +1,20 @@
<div class="tower">
<div class="tower" *ngIf="tower$ | async">
<div class="container">
<div class="tasks-container">
<app-tasks [tasks]="tasks" [tower]="tower"></app-tasks>
<app-tasks [tasks]="tasks" [tower]="tower$ | async"></app-tasks>
</div>
<img src="assets/plus-sign.svg" alt="add item" (click)="addBlock()" />
<div class="block-container-container">
<div class="block-container falling">
<app-block *ngFor="let block of drawableBlocks" [block]="block" [tower]="tower"></app-block>
<div class="block-container" *ngIf="blocks.length > 0">
<app-block
[ngClass]="block.cssClass"
[ngStyle]="block.style"
*ngFor="let block of drawableBlocks"
[block]="block"
[tower]="tower$ | async"
></app-block>
</div>
</div>
</div>
@ -19,6 +25,6 @@
type="text"
placeholder="name…"
[(ngModel)]="towerName"
[ngStyle]="{ color: tower?.baseColor | color }"
[ngStyle]="{ color: (tower$ | async)?.baseColor | color }"
/>
</div>

View file

@ -121,23 +121,17 @@
bottom: 0;
width: 100%;
transform: scaleY(-1);
&.falling > *:last-child {
animation: falling 1.5s cubic-bezier(0.5, 0, 1, 0) forwards;
@keyframes falling {
0% {
opacity: 0;
* {
transform: translateY(500%);
}
50% {
opacity: 1;
.descend {
transition: transform 1.5s cubic-bezier(0.5, 0, 1, 0), opacity 500ms cubic-bezier(0.5, 0, 1, 0);
}
100% {
transform: translateY(0);
}
}
.ascend {
transition: transform 1.5s cubic-bezier(0.5, 0, 1, 0), opacity 500ms cubic-bezier(0.5, 0, 1, 0) 1s;
}
}
}

View file

@ -1,51 +1,97 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { ColoredBlock, Tower } from '../../../../model/tower';
import { ModalService } from '../../../../services/modal.service';
import { Observable } from 'rxjs/internal/Observable';
import { Range } from '../../../../interfaces/range';
import { top } from '../../../../utils/top';
type StyledBlock = ColoredBlock & { style: { [p: string]: string }; shouldDraw: boolean; cssClass: string };
@Component({
selector: 'app-tower',
templateUrl: './tower.component.html',
styleUrls: ['./tower.component.scss']
})
export class TowerComponent {
export class TowerComponent implements OnInit {
@Input() dateRange$: Observable<Range<Date>>;
private dateRange: Range<Date>;
@Input() tower$: Observable<Tower>;
private tower: Tower;
get towerName(): string {
return this.tower.name;
return this.tower ? this.tower.name : 'Loading…';
}
set towerName(value: string) {
this.tower.changeName(value);
}
@Input() set dateRange(value: { from: Date; to: Date }) {
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
return;
}
this._dateRange = value;
}
get dateRange(): { from: Date; to: Date } {
return this._dateRange;
tasks: Array<ColoredBlock>;
blocks: Array<StyledBlock> = [];
get drawableBlocks(): Array<StyledBlock> {
return this.blocks.filter(b => b.shouldDraw);
}
public constructor(private modalService: ModalService) {}
get drawableBlocks(): Array<ColoredBlock> {
return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
);
ngOnInit() {
this.tower$.subscribe(value => {
if (value) {
this.blocks = value.coloredBlocks
.filter(b => b.isDone)
.map(b => {
let classedBlock = b as StyledBlock;
classedBlock.shouldDraw = true;
classedBlock.style = { transform: 'translateY(0)', opacity: '1' };
classedBlock.cssClass = '';
return classedBlock;
});
if (this.tower) {
let difference = this.tower.blocks.map((b, index) => {
return b === value.blocks[index];
});
if (
(difference.every(i => i) && this.tower.blocks.length < value.blocks.length) ||
this.tower.blocks.filter(b => b.isDone).length + 1 === value.blocks.filter(b => b.isDone).length
) {
const lastBlock = top(this.blocks);
if (lastBlock) {
lastBlock.style = { opacity: '0' };
lastBlock.cssClass = 'descend';
setTimeout(() => (lastBlock.style = { transform: 'translateY(0)', opacity: '1' }), 0);
}
}
}
get tasks(): Array<ColoredBlock> {
return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone
);
this.tasks = value.coloredBlocks.filter(block => !block.isDone);
this.tower = value;
}
});
this.dateRange$.subscribe(dateRange => {
this.initData(dateRange);
this.dateRange = dateRange;
});
}
@Input() tower: Tower;
initData(newDateRange: Range<Date>) {
for (const block of this.blocks) {
block.shouldDraw = newDateRange.from <= block.created;
_dateRange: { from: Date; to: Date };
isFalling = true;
if ((block.cssClass === '' || block.cssClass === 'descend') && newDateRange.to < block.created) {
block.cssClass = 'ascend';
block.style = { transform: 'translateY(500%)', opacity: '0' };
}
if (block.shouldDraw && block.cssClass === 'ascend' && block.created < newDateRange.to) {
block.cssClass = 'descend';
block.style = { transform: 'translateY(0)', opacity: '1' };
}
}
}
public async addBlock() {
try {

View file

@ -2,8 +2,8 @@
<!-- wrapper for easier styling -->
<app-select-add
[options]="pageNames"
[default]="selectedPageName"
(value)="selectPage($event)"
[default]="(selectedPage$ | async).name"
(value)="selectedPageName = $event"
[placeholder]="'Add a new page…'"
></app-select-add>
</div>
@ -11,7 +11,7 @@
<div class="page-container">
<!-- wrapper for easier styling -->
<app-page *ngIf="selectedPage" [page]="selectedPage" (isDragHappening)="isDragHappening = $event"></app-page>
<app-page [page$]="selectedPage$" (isDragHappening)="isDragHappening = $event"></app-page>
</div>
<!-- wrapper for easier styling -->

View file

@ -27,19 +27,6 @@ export class PagesComponent {
return [];
}
get selectedPage(): Page {
try {
return this.pages[this.pageNames.indexOf(this.selectedPageName)];
} catch {
return null;
}
}
private _selectedPageName: string;
get selectedPageName(): string {
return this._selectedPageName;
}
set selectedPageName(value: string) {
window.localStorage.setItem(
USER_DATA_KEY,
@ -47,7 +34,8 @@ export class PagesComponent {
selectedPage: value
})
);
this._selectedPageName = value;
const index = this.pageNames.indexOf(value);
this._selectedPage.next(index >= 0 ? this.pages[0] : null);
}
private readonly _selectedPage: BehaviorSubject<Page> = new BehaviorSubject(null);
@ -56,28 +44,22 @@ export class PagesComponent {
constructor(public dataService: DataService, private modalService: ModalService) {
const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
if (userData !== null && userData.selectedPage !== undefined) {
this._selectedPageName = userData.selectedPage;
this.selectedPageName = userData.selectedPage;
}
this.dataService.safeChildren$.subscribe(pages => {
this.dataService.children$.subscribe(pages => {
if (pages) {
this.pages = pages;
if (!this.selectedPage) {
this.selectedPageName = this.pages.length > 0 ? this.pages[0].name : null;
if (!this._selectedPage.getValue() && this.pages.length > 0) {
this.selectedPageName = this.pages[0].name;
} else if (this._selectedPage.getValue()) {
// To trigger update on new page.
this.selectedPageName = this._selectedPage.getValue().name;
}
this._selectedPage.next(this.selectedPage);
}
});
}
async selectPage(selected: string) {
if (!this.pageNames.includes(selected)) {
this.dataService.addPage(selected);
}
this.selectedPageName = selected;
this._selectedPage.next(this.selectedPage);
}
async openSettings() {
try {
await this.modalService.showSettings(this.selectedPage$);

View file

@ -1,5 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { range } from '../../../utils/range';
import { Range } from '../../../interfaces/range';
@Component({
selector: 'app-double-slider',
@ -49,8 +50,7 @@ export class DoubleSliderComponent {
private _values: any[];
@Output() lowerBound: EventEmitter<any> = new EventEmitter();
@Output() upperBound: EventEmitter<any> = new EventEmitter();
@Output() range: EventEmitter<Range<any>> = new EventEmitter();
drawnLabels: string[];
@ -93,12 +93,16 @@ export class DoubleSliderComponent {
}
private emitValue() {
if (this.oneValue < this.otherValue) {
this.lowerBound.emit(this.values[this.indexFromValue(this.oneValue)]);
this.upperBound.emit(this.values[this.indexFromValue(this.otherValue)]);
} else {
this.lowerBound.emit(this.values[this.indexFromValue(this.otherValue)]);
this.upperBound.emit(this.values[this.indexFromValue(this.oneValue)]);
}
const range = {
from:
this.oneValue < this.otherValue
? this.values[this.indexFromValue(this.oneValue)]
: this.values[this.indexFromValue(this.otherValue)],
to:
this.oneValue < this.otherValue
? this.values[this.indexFromValue(this.otherValue)]
: this.values[this.indexFromValue(this.oneValue)]
};
this.range.emit(range);
}
}

View file

@ -59,12 +59,12 @@ export class SelectAddComponent {
this.select(this.newOption);
this.newOption = '';
}
this.toggle();
}
select(option: string) {
this.selected = option;
this.value.emit(this.selected);
this.toggle();
}
toggle() {

View file

@ -1,4 +1,5 @@
import { ITower } from './tower';
import { Range } from '../range';
export interface IPage {
name: string;
@ -6,9 +7,6 @@ export interface IPage {
userData: {
hideCreateTowerButton?: boolean;
defaultDateRange?: {
from: Date;
to: Date;
};
defaultDateRange?: Range<Date>;
};
}

View file

@ -0,0 +1,4 @@
export interface Range<T> {
from: T;
to: T;
}

View file

@ -41,11 +41,17 @@ export class DataService extends Root<Page> {
childrenConstructor: null
}
};
for (let page of pages) {
new Page(this, page);
}
setTimeout(() => {
this.children$.subscribe(value => {
this.log();
});
}, 0);
this.children$.subscribe(value => {
this._safeChildren.next(value);
this.save(0);
});
@ -60,11 +66,12 @@ export class DataService extends Root<Page> {
}
addPage(name: string) {
new Page(this, {
const page = new Page(this, {
name,
userData: {},
towers: []
});
page.addTower();
}
removePage(page: Page) {

View file

@ -49,13 +49,13 @@ export class StoreService<T> {
isDone: false
},
{
created: new Date(2020, 2, 15),
created: new Date(2019, 3, 15),
tag: 'go to school',
description: 'done it',
isDone: true
},
{
created: new Date(2021, 2, 15),
created: new Date(2019, 3, 15, 19),
tag: 'go to school',
isDone: false
}