Make app work again

This commit is contained in:
schmelczerandras 2019-09-01 10:44:17 +02:00
parent a9ad628488
commit 938f3def1f
30 changed files with 236 additions and 155 deletions

View file

@ -22,6 +22,7 @@ import { CreateBlockComponent } from './components/modal/modals/create-block/cre
import { RemoveBlockComponent } from './components/modal/modals/remove-block/remove-block.component'; import { RemoveBlockComponent } from './components/modal/modals/remove-block/remove-block.component';
import { ToggleComponent } from './components/shared/toggle/toggle.component'; import { ToggleComponent } from './components/shared/toggle/toggle.component';
import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component'; import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component';
import { ColorPipe } from './pipes/color.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -41,7 +42,8 @@ import { TasksComponent } from './components/pages/page/tower/tasks/tasks.compon
CreateBlockComponent, CreateBlockComponent,
RemoveBlockComponent, RemoveBlockComponent,
ToggleComponent, ToggleComponent,
TasksComponent TasksComponent,
ColorPipe
], ],
imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule], imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule],
providers: [], providers: [],

View file

@ -24,7 +24,7 @@
<app-toggle <app-toggle
[beforeText]="'This task hasn\'t been finished yet'" [beforeText]="'This task hasn\'t been finished yet'"
[afterText]="'Goal already accomplished'" [afterText]="'Goal already accomplished'"
[default]="!modalService.active.input.isTask" [default]="isDone"
(value)="isDone = $event" (value)="isDone = $event"
></app-toggle> ></app-toggle>
</div> </div>

View file

@ -9,7 +9,7 @@ import { ModalService } from '../../../../services/modal.service';
export class CreateBlockComponent { export class CreateBlockComponent {
selected: string; selected: string;
description: string = null; description: string = null;
isDone: boolean; isDone: boolean = !this.modalService.active.input.isTask;
constructor(public modalService: ModalService) {} constructor(public modalService: ModalService) {}

View file

@ -8,7 +8,7 @@
<app-select-add <app-select-add
class="select" class="select"
[options]="modalService.active.input.options" [options]="modalService.active.input.options"
[default]="modalService.active.input.options[0]" [default]="modalService.active.input.default"
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
[placeholder]="'Tag this item…'" [placeholder]="'Tag this item…'"

View file

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

View file

@ -1 +1 @@
<div [ngStyle]="{ 'background-color': block.color }" (click)="handleClick()"></div> <div [ngStyle]="{ 'background-color': block.color | color }" (click)="handleClick()"></div>

View file

@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Block } from '../../../../../model/block'; import { Block } from '../../../../../model/block';
import { ModalService } from '../../../../../services/modal.service'; import { ModalService } from '../../../../../services/modal.service';
import { Tower } from '../../../../../model/tower'; import { ColoredBlock, Tower } from '../../../../../model/tower';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
@ -9,7 +9,7 @@ import { Tower } from '../../../../../model/tower';
styleUrls: ['./block.component.scss'] styleUrls: ['./block.component.scss']
}) })
export class BlockComponent { export class BlockComponent {
@Input() block: Block; @Input() block: ColoredBlock;
@Input() tower: Tower; @Input() tower: Tower;
constructor(private modalService: ModalService) {} constructor(private modalService: ModalService) {}
@ -22,9 +22,12 @@ export class BlockComponent {
description: this.block.description, description: this.block.description,
isDone: this.block.isDone isDone: this.block.isDone
}); });
this.block.tag = selected; console.log(description);
this.block.description = description; this.block.changeProperties({
this.block.isDone = isDone; tag: selected,
description,
isDone
});
} catch { } catch {
// pass // pass
} }

View file

@ -6,8 +6,8 @@
{{ tasks.length == 0 ? '&#8203;' : tasks.length == 1 ? 'task' : 'tasks' }} {{ tasks.length == 0 ? '&#8203;' : tasks.length == 1 ? 'task' : 'tasks' }}
</p> </p>
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }"> <div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
<div class="task-container" *ngFor="let task of tasks" [ngStyle]="{ color: toHslString(task.color) }"> <div class="task-container" *ngFor="let task of tasks" [ngStyle]="{ color: task.color | color }">
<div [ngStyle]="{ 'background-color': toHslString(task.color) }"></div> <div [ngStyle]="{ 'background-color': task.color | color }"></div>
<p (click)="handleClick(task)" [innerText]="task.description ? task.description : 'unknown'"></p> <p (click)="handleClick(task)" [innerText]="task.description ? task.description : 'unknown'"></p>
</div> </div>
</div> </div>

View file

@ -3,8 +3,8 @@ import { Block } from '../../../../../model/block';
import { Tower } from '../../../../../model/tower'; import { Tower } from '../../../../../model/tower';
import { ModalService } from '../../../../../services/modal.service'; import { ModalService } from '../../../../../services/modal.service';
import { CancelService } from '../../../../../services/cancel.service'; import { CancelService } from '../../../../../services/cancel.service';
import { toHslString } from '../../../../../utils/color'; import { IColor } from '../../../../../interfaces/color';
import { IColor } from '../../../../../interfaces/persistance/color'; import { IBlock } from '../../../../../interfaces/persistance/block';
@Component({ @Component({
selector: 'app-tasks', selector: 'app-tasks',
@ -12,8 +12,6 @@ import { IColor } from '../../../../../interfaces/persistance/color';
styleUrls: ['./tasks.component.scss'] styleUrls: ['./tasks.component.scss']
}) })
export class TasksComponent implements OnInit { export class TasksComponent implements OnInit {
readonly toHslString = toHslString;
@Input() tasks: Array<Block & { color: IColor }>; @Input() tasks: Array<Block & { color: IColor }>;
@Input() tower: Tower; @Input() tower: Tower;
@ -48,12 +46,16 @@ export class TasksComponent implements OnInit {
isDone: block.isDone isDone: block.isDone
}); });
block.tag = selected; const change: Partial<IBlock> = {
block.description = description; tag: selected,
description,
isDone
};
if (!block.isDone && isDone) { if (!block.isDone && isDone) {
block.created = new Date(); change.created = new Date();
} }
block.isDone = isDone;
block.changeProperties(change);
} catch { } catch {
// pass // pass
} }

View file

@ -18,7 +18,7 @@
id="tower-name" id="tower-name"
type="text" type="text"
placeholder="name…" placeholder="name…"
[(ngModel)]="tower.name" [(ngModel)]="towerName"
[ngStyle]="{ color: toHslString(tower?.baseColor) }" [ngStyle]="{ color: tower?.baseColor | color }"
/> />
</div> </div>

View file

@ -1,9 +1,6 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Tower } from '../../../../model/tower'; import { ColoredBlock, Tower } from '../../../../model/tower';
import { ModalService } from '../../../../services/modal.service'; import { ModalService } from '../../../../services/modal.service';
import { Block } from '../../../../model/block';
import { IColor } from '../../../../interfaces/persistance/color';
import { toHslString } from '../../../../utils/color';
@Component({ @Component({
selector: 'app-tower', selector: 'app-tower',
@ -11,7 +8,13 @@ import { toHslString } from '../../../../utils/color';
styleUrls: ['./tower.component.scss'] styleUrls: ['./tower.component.scss']
}) })
export class TowerComponent { export class TowerComponent {
readonly toHslString = toHslString; get towerName(): string {
return this.tower.name;
}
set towerName(value: string) {
this.tower.changeName(value);
}
@Input() set dateRange(value: { from: Date; to: Date }) { @Input() set dateRange(value: { from: Date; to: Date }) {
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) { if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
@ -26,13 +29,13 @@ export class TowerComponent {
public constructor(private modalService: ModalService) {} public constructor(private modalService: ModalService) {}
get drawableBlocks(): Array<Block & { color: IColor }> { get drawableBlocks(): Array<ColoredBlock> {
return this.tower.coloredBlocks.filter( return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
); );
} }
get tasks(): Array<Block & { color: IColor }> { get tasks(): Array<ColoredBlock> {
return this.tower.coloredBlocks.filter( return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone
); );

View file

@ -0,0 +1,5 @@
export interface IColor {
h: number;
s: number;
l: number;
}

View file

@ -1,7 +1,4 @@
import { Typed } from './typed'; export interface IBlock {
export interface IBlock extends Typed {
type: 'Block';
created: Date; created: Date;
tag: string; tag: string;
isDone: boolean; isDone: boolean;

View file

@ -1,8 +0,0 @@
import { Typed } from './typed';
export interface IColor extends Typed {
type: 'Color';
h: number;
s: number;
l: number;
}

View file

@ -1,8 +1,6 @@
import { ITower } from './tower'; import { ITower } from './tower';
import { Typed } from './typed';
export interface IPage extends Typed { export interface IPage {
type: 'Page';
name: string; name: string;
towers: ITower[]; towers: ITower[];

View file

@ -1,9 +1,7 @@
import { IBlock } from './block'; import { IBlock } from './block';
import { IColor } from './color'; import { IColor } from '../color';
import { Typed } from './typed';
export interface ITower extends Typed { export interface ITower {
type: 'Tower';
name: string; name: string;
blocks: IBlock[]; blocks: IBlock[];
baseColor: IColor; baseColor: IColor;

View file

@ -1,3 +0,0 @@
export interface Typed {
type: string;
}

View file

@ -1,4 +0,0 @@
export interface Vector {
x: number;
y: number;
}

View file

@ -1,20 +1,30 @@
import { Serializable } from './serializable'; import { Serializable } from './serializable';
import { IBlock } from '../interfaces/persistance/block'; import { IBlock } from '../interfaces/persistance/block';
import { Node } from '../storage/node'; import { Node } from '../store/node';
export class Block extends Serializable implements IBlock { export class Block extends Serializable implements IBlock {
constructor(parent: Node, props: IBlock) { constructor(parent: Node, props: IBlock) {
super(parent, props); super(parent, props);
this.onAfterClone();
}
protected onAfterClone(): void {
if (this.created.constructor.name !== 'Date') { if (this.created.constructor.name !== 'Date') {
this.created = new Date(this.created); this.created = new Date(this.created);
} }
// TODO: remove.
if (this.isDone === null || this.isDone === undefined) {
this.isDone = false;
}
}
changeProperties(values: Partial<IBlock>) {
this.changeKeys(values);
} }
// Only here to prevent ts warnings.
type: 'Block';
created: Date; created: Date;
isDone: boolean; isDone: boolean;
description: string; readonly description: string;
tag: string; readonly tag: string;
} }

View file

@ -1,21 +1,19 @@
import { Serializable } from './serializable'; import { Serializable } from './serializable';
import { IPage } from '../interfaces/persistance/page'; import { IPage } from '../interfaces/persistance/page';
import { Tower } from './tower'; import { Tower } from './tower';
import { Node } from '../storage/node'; import { Node } from '../store/node';
export class Page extends Serializable implements IPage { export class Page extends Serializable implements IPage {
constructor(parent: Node, props: IPage) { constructor(parent: Node, props: IPage) {
super(parent, props); super(parent, props);
} }
// Only here to prevent ts warnings. readonly name: string;
name: string;
get towers(): Array<Tower> { get towers(): Array<Tower> {
return this.children as Array<Tower>; return this.children as Array<Tower>;
} }
type: 'Page';
userData: { readonly userData: {
hideCreateTowerButton: boolean; hideCreateTowerButton: boolean;
defaultDateRange: { defaultDateRange: {
from: Date; from: Date;
@ -38,10 +36,14 @@ export class Page extends Serializable implements IPage {
return; return;
} }
this.map(page => { const towers = [...this.towers];
const tower = page.towers[previousIndex]; const tower = towers[previousIndex];
page.towers.splice(previousIndex, 1); towers.splice(previousIndex, 1);
page.towers.splice(currentIndex, 0, tower); towers.splice(currentIndex, 0, tower);
this.changeValue({
oldValue: this.towers,
newValue: towers
}); });
} }
@ -52,10 +54,9 @@ export class Page extends Serializable implements IPage {
} while (30 <= hue && hue <= 200); } while (30 <= hue && hue <= 200);
new Tower(this, { new Tower(this, {
type: 'Tower',
name, name,
blocks: [], blocks: [],
baseColor: { h: hue, s: 100, l: 50, type: 'Color' } baseColor: { h: hue, s: 100, l: 50 }
}); });
} }

View file

@ -1,8 +1,7 @@
import { Cloneable } from '../storage/cloneable'; import { Cloneable } from '../store/cloneable';
import { Node } from '../storage/node'; import { Node } from '../store/node';
export class Serializable extends Cloneable { export class Serializable extends Cloneable {
type: string;
private static propertyList: any = {}; private static propertyList: any = {};
static childrenMap: { static childrenMap: {
[type: string]: { [type: string]: {
@ -11,7 +10,11 @@ export class Serializable extends Cloneable {
}; };
}; };
constructor(parent: Node, properties: any) { protected onAfterClone(): void {
// pass
}
protected constructor(parent: Node, properties: any) {
super(parent); super(parent);
const type = this.constructor.name; const type = this.constructor.name;

View file

@ -3,44 +3,39 @@ import { lighten } from '../utils/color';
import { Block } from './block'; import { Block } from './block';
import { Serializable } from './serializable'; import { Serializable } from './serializable';
import { hash } from '../utils/hash'; import { hash } from '../utils/hash';
import { Node } from '../storage/node'; import { Node } from '../store/node';
import { IColor } from '../interfaces/persistance/color'; import { IColor } from '../interfaces/color';
export type ColoredBlock = Block & { color: IColor };
export class Tower extends Serializable implements ITower { export class Tower extends Serializable implements ITower {
constructor(parent: Node, props: ITower) {
super(parent, props);
this.blocks.sort((a, b) => a.created.getTime() - b.created.getTime());
this.calculateTagList();
}
tags: string[]; tags: string[];
// Only here to prevent ts warnings.
name: string; name: string;
type: 'Tower';
get blocks(): Array<Block> { get blocks(): Array<Block> {
return this.children as Array<Block>; return this.children as Array<Block>;
} }
baseColor: IColor;
get coloredBlocks(): Array<Block & { color: IColor }> { coloredBlocks: Array<ColoredBlock>;
return this.children.map(b => {
const coloredBlock = b as Block & { color: IColor }; readonly baseColor: IColor;
constructor(parent: Node, props: ITower) {
super(parent, props);
this.onAfterClone();
}
protected onAfterClone(): void {
this.blocks.sort((a, b) => {
return a.created.getTime() - b.created.getTime();
});
this.coloredBlocks = this.blocks.map(b => {
const coloredBlock = b as ColoredBlock;
coloredBlock.color = lighten((hash(coloredBlock.tag) - 0.5) * 50, this.baseColor); coloredBlock.color = lighten((hash(coloredBlock.tag) - 0.5) * 50, this.baseColor);
return coloredBlock; return coloredBlock;
}); });
}
addBlock(props: { tag: string; description: string; isDone: boolean }) {
new Block(this, {
created: new Date(),
...props,
type: 'Block'
});
}
private calculateTagList() {
this.tags = []; this.tags = [];
for (const block of this.blocks) { for (const block of this.blocks) {
if (!this.tags.includes(block.tag)) { if (!this.tags.includes(block.tag)) {
@ -48,4 +43,17 @@ export class Tower extends Serializable implements ITower {
} }
} }
} }
addBlock(props: { tag: string; description: string; isDone: boolean }) {
new Block(this, {
created: new Date(),
...props
});
}
changeName(newName: string) {
// For optimization purposes.
this.name = newName;
this.mutatedUpdate();
}
} }

View file

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { IColor } from '../interfaces/color';
import { toHslString } from '../utils/color';
@Pipe({
name: 'color'
})
export class ColorPipe implements PipeTransform {
transform(color: IColor, args?: any): string {
return toHslString(color);
}
}

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { StoreService } from './store.service'; import { StoreService } from './store.service';
import { Page } from '../model/page'; import { Page } from '../model/page';
import { Root } from '../storage/root'; import { Root } from '../store/root';
import { Serializable } from '../model/serializable'; import { Serializable } from '../model/serializable';
import { Tower } from '../model/tower'; import { Tower } from '../model/tower';
import { Block } from '../model/block'; import { Block } from '../model/block';
@ -47,15 +47,22 @@ export class DataService extends Root<Page> {
this.children$.subscribe(value => { this.children$.subscribe(value => {
this.log(); this.log();
this._safeChildren.next(value); this._safeChildren.next(value);
this.storeService.scheduleSave(this.pages); this.save(0);
}); });
} }
mutatedUpdate() {
this.save(2500);
}
save(timeout: number) {
this.storeService.scheduleSave(this.pages, timeout);
}
addPage(name: string) { addPage(name: string) {
new Page(this, { new Page(this, {
name, name,
userData: {}, userData: {},
type: 'Page',
towers: [] towers: []
}); });
} }

View file

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Page } from '../model/page'; import { Page } from '../model/page';
import { IPage } from '../interfaces/persistance/page'; import { IPage } from '../interfaces/persistance/page';
const LOCAL_STORAGE_KEY = 'life-towers.data.v.3'; const LOCAL_STORAGE_KEY = 'life-towers.data.v.2';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -15,84 +15,77 @@ export class StoreService<T> {
{ {
name: 'Work & life', name: 'Work & life',
userData: {}, userData: {},
type: 'Page',
towers: [ towers: [
{ {
name: 'work', name: 'work',
baseColor: { h: 0, s: 100, l: 50, type: 'Color' }, baseColor: { h: 0, s: 100, l: 50 },
type: 'Tower',
blocks: [ blocks: [
{ {
created: new Date(2015, 2, 13), created: new Date(2015, 2, 13),
tag: 'a', tag: 'a',
description: 'done it', description: 'done it',
isDone: true, isDone: true
type: 'Block'
}, },
{ {
created: new Date(2016, 2, 15), created: new Date(2016, 2, 15),
tag: 'go to school', tag: 'go to school',
description: 'done it', description: 'done it',
type: 'Block' isDone: false
}, },
{ {
created: new Date(2017, 2, 15), created: new Date(2017, 2, 15),
tag: 'go to work', tag: 'go to work',
isDone: true, isDone: true
type: 'Block'
}, },
{ {
created: new Date(2018, 2, 13), created: new Date(2018, 2, 13),
tag: 'go to work', tag: 'go to work',
description: 'done it', description: 'done it',
isDone: true, isDone: true
type: 'Block'
}, },
{ {
created: new Date(2019, 3, 13), created: new Date(2019, 3, 13),
tag: 'go to work', tag: 'go to work',
type: 'Block' isDone: false
}, },
{ {
created: new Date(2020, 2, 15), created: new Date(2020, 2, 15),
tag: 'go to school', tag: 'go to school',
description: 'done it', description: 'done it',
isDone: true, isDone: true
type: 'Block'
}, },
{ {
created: new Date(2021, 2, 15), created: new Date(2021, 2, 15),
tag: 'go to school', tag: 'go to school',
type: 'Block' isDone: false
} }
] ]
}, },
{ {
baseColor: { h: 180, s: 100, l: 50, type: 'Color' }, baseColor: { h: 180, s: 100, l: 50 },
name: 'life', name: 'life',
type: 'Tower',
blocks: [ blocks: [
{ {
created: new Date(2019, 3, 13), created: new Date(2019, 3, 13),
tag: 'go home', tag: 'go home',
description: 'done it', description: 'done it',
type: 'Block' isDone: false
}, },
{ {
created: new Date(2019, 4, 13), created: new Date(2019, 4, 13),
tag: 'go home', tag: 'go home',
type: 'Block' isDone: false
}, },
{ {
created: new Date(2019, 4, 15), created: new Date(2019, 4, 15),
tag: 'go to work', tag: 'go to work',
description: 'done it', description: 'done it',
type: 'Block' isDone: false
}, },
{ {
created: new Date(2019, 4, 15, 14), created: new Date(2019, 4, 15, 14),
tag: 'go to work', tag: 'go to work',
type: 'Block' isDone: false
} }
] ]
} }
@ -105,14 +98,14 @@ export class StoreService<T> {
this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T; this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
} }
scheduleSave(data: T) { scheduleSave(data: T, timeout: number) {
this.dataToSave = data; this.dataToSave = data;
if (!this.saveScheduled) { if (!this.saveScheduled) {
this.saveScheduled = true; this.saveScheduled = true;
setTimeout(() => { setTimeout(() => {
this.saveScheduled = false; this.saveScheduled = false;
this.save(this.dataToSave).catch(); this.save(this.dataToSave).catch();
}, 0); }, timeout);
} }
} }

View file

@ -1,11 +1,13 @@
import { InnerNode } from './inner-node'; import { InnerNode } from './inner-node';
import { Node } from './node'; import { Node } from './node';
export class Cloneable extends InnerNode { export abstract class Cloneable extends InnerNode {
constructor(parent: Node) { protected constructor(parent: Node) {
super(parent); super(parent);
} }
protected abstract onAfterClone(): void;
protected cloneWithMap(map: (node: this) => void): this { protected cloneWithMap(map: (node: this) => void): this {
const insides = Object.getOwnPropertyDescriptors(this); const insides = Object.getOwnPropertyDescriptors(this);
@ -20,8 +22,8 @@ export class Cloneable extends InnerNode {
return value.bind(proxy); return value.bind(proxy);
} }
return value; return value;
} else if (this.hasOwnProperty(prop)) { } else if (target.prototype.hasOwnProperty(prop)) {
const value = this[prop]; const value = target.prototype[prop];
if (typeof value === 'function') { if (typeof value === 'function') {
return value.bind(proxy); return value.bind(proxy);
} }
@ -34,28 +36,44 @@ export class Cloneable extends InnerNode {
}); });
map(<any>insidesProxy); map(<any>insidesProxy);
(<any>insidesProxy.__target__).id.value = Node.id++; return this.cloneFromInsides(<any>insidesProxy.__target__);
(<any>insidesProxy.__target__).copyCount.value++;
Node.sumCopyCount++;
return Object.create(Object.getPrototypeOf(this), <any>insidesProxy.__target__);
} }
protected cloneWithAdd({ value, propertyName }: { value: any; propertyName: string }): this { protected cloneWithAdd({ propertyName, value }: { value: any; propertyName: string }): this {
if (this[propertyName] === value) {
return this;
}
const insides = Object.getOwnPropertyDescriptors(this); const insides = Object.getOwnPropertyDescriptors(this);
insides[propertyName].value = value; insides[propertyName].value = value;
insides.id.value = Node.id++; return this.cloneFromInsides(insides);
insides.copyCount.value++; }
Node.sumCopyCount++;
return Object.create(Object.getPrototypeOf(this), insides); protected cloneWithChangedKeys(props: { [propertyName: string]: any }): this {
const insides = Object.getOwnPropertyDescriptors(this);
for (let key in props) {
if (props.hasOwnProperty(key)) {
if (insides.hasOwnProperty(key)) {
insides[key].value = props[key];
} else {
// @ts-ignore
insides[key] = {
value: props[key]
};
}
}
}
return this.cloneFromInsides(insides);
} }
protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this { protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
if (oldValue === newValue) {
return this;
}
const insides = Object.getOwnPropertyDescriptors(this); const insides = Object.getOwnPropertyDescriptors(this);
insides.id.value = Node.id++;
insides.copyCount.value++;
Node.sumCopyCount++;
let wasMatch = false; let wasMatch = false;
for (let name in insides) { for (let name in insides) {
@ -69,6 +87,16 @@ export class Cloneable extends InnerNode {
throw new TypeError(`Object has no property with value: ${oldValue.toString()}`); throw new TypeError(`Object has no property with value: ${oldValue.toString()}`);
} }
return Object.create(Object.getPrototypeOf(this), insides); return this.cloneFromInsides(insides);
}
private cloneFromInsides(insides): this {
insides.id.value = Node.id++;
insides.copyCount.value++;
Node.sumCopyCount++;
const clone = Object.create(Object.getPrototypeOf(this), insides);
clone.onAfterClone();
return clone;
} }
} }

View file

@ -1,4 +1,5 @@
import { Node } from './node'; import { Node } from './node';
import { observableToBeFn } from 'rxjs/internal/testing/TestScheduler';
export abstract class InnerNode extends Node { export abstract class InnerNode extends Node {
parent: Node; parent: Node;
@ -17,21 +18,31 @@ export abstract class InnerNode extends Node {
protected constructor(parent: Node) { protected constructor(parent: Node) {
super(); super();
parent.addChild({ new Promise(r => r()).then(() =>
value: this parent.addChild({
}); value: this
})
);
}
mutatedUpdate() {
this.parent.mutatedUpdate();
} }
map(map: (a: this) => void) { map(map: (a: this) => void) {
return this.update((self: this) => this.cloneWithMap.call(self, map)); return this.update((self: this) => this.cloneWithMap.call(self, map));
} }
changeKey(update: { propertyName: string; value: any }): this { changeKey({ propertyName, value }: { propertyName: string; value: any }): this {
return this.update((self: this) => this.cloneWithAdd.call(self, update)); return this.update((self: this) => this.cloneWithAdd.call(self, { propertyName, value }));
} }
changeValue(update: { oldValue: any; newValue: any }): this { changeKeys(props: { [propertyName: string]: any }): this {
return this.update((self: this) => this.cloneWithModify.call(self, update)); return this.update((self: this) => this.cloneWithChangedKeys.call(self, props));
}
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
return this.update((self: this) => this.cloneWithModify.call(self, { oldValue, newValue }));
} }
addChild(update: { value: InnerNode }) { addChild(update: { value: InnerNode }) {
@ -44,6 +55,7 @@ export abstract class InnerNode extends Node {
protected abstract cloneWithMap(map: (a: this) => void): this; protected abstract cloneWithMap(map: (a: this) => void): this;
protected abstract cloneWithAdd(update: { value: any; propertyName: string }): this; protected abstract cloneWithAdd(update: { value: any; propertyName: string }): this;
protected abstract cloneWithChangedKeys(props: { [propertyName: string]: any }): this;
protected abstract cloneWithModify(update: { oldValue: any; newValue: any }): this; protected abstract cloneWithModify(update: { oldValue: any; newValue: any }): this;
private update(cloneMethod: (self: this) => this): this { private update(cloneMethod: (self: this) => this): this {
@ -52,6 +64,10 @@ export abstract class InnerNode extends Node {
} }
const clone = cloneMethod(this); const clone = cloneMethod(this);
if (clone === this) {
return this;
}
for (let child of clone.children) { for (let child of clone.children) {
child.parent = clone; child.parent = clone;
} }

View file

@ -14,6 +14,8 @@ export abstract class Node {
Node.sumCopyCount++; Node.sumCopyCount++;
} }
abstract mutatedUpdate(): void;
addChild(update: { value: InnerNode }) { addChild(update: { value: InnerNode }) {
this.changeValue({ this.changeValue({
oldValue: this.children, oldValue: this.children,
@ -22,6 +24,10 @@ export abstract class Node {
} }
changeChild({ oldValue, newValue }: { oldValue: InnerNode; newValue: InnerNode }) { changeChild({ oldValue, newValue }: { oldValue: InnerNode; newValue: InnerNode }) {
if (oldValue === newValue) {
return;
}
this.changeValue({ this.changeValue({
oldValue: this.children, oldValue: this.children,
newValue: this.children.map(c => (c === oldValue ? newValue : c)) newValue: this.children.map(c => (c === oldValue ? newValue : c))

View file

@ -15,6 +15,10 @@ export class Root<T extends InnerNode> extends Node {
this._children.next(value); this._children.next(value);
} }
mutatedUpdate() {
// pass
}
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) { changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
if (this.children !== oldValue) { if (this.children !== oldValue) {
throw new TypeError('Only children can be changed.'); throw new TypeError('Only children can be changed.');

View file

@ -1,4 +1,4 @@
import { IColor } from '../interfaces/persistance/color'; import { IColor } from '../interfaces/color';
export const lighten = (by: number, { h, s, l }: IColor): IColor => { export const lighten = (by: number, { h, s, l }: IColor): IColor => {
let newL = l + by; let newL = l + by;
@ -8,7 +8,7 @@ export const lighten = (by: number, { h, s, l }: IColor): IColor => {
newL = 0; newL = 0;
} }
return { h, s, l: newL, type: 'Color' }; return { h, s, l: newL };
}; };
export const toHslString = ({ h, s, l }: IColor): string => { export const toHslString = ({ h, s, l }: IColor): string => {