Make scrolling blocks usable

This commit is contained in:
Andras Schmelczer 2019-09-15 21:32:38 +02:00
parent fc0d64fce7
commit 32704c5561
10 changed files with 185 additions and 99 deletions

View file

@ -1,50 +1,63 @@
<section #container> <section #container>
<div class="card transparent"></div> <div class="card placeholder"></div>
<div <div
*ngFor="let i of range({ min: 1, max: blocks.length + 1 })" *ngFor="let i of range({ max: blocks.length })"
(click)="$event.stopPropagation(); scrollToChild(i)" (click)="$event.stopPropagation(); scrollToChild(i + 1)"
class="card {{ i === activeChild ? 'active' : '' }}" class="card {{ i + 1 === activeChild ? 'active' : '' }} {{
i + 2 === activeChild || i === activeChild ? 'near-active' : ''
}}"
> >
<div class="mask"></div> <div class="mask"></div>
<div class="header"> <div class="header">
<div class="exit" (click)="modalService.cancel()"></div> <div class="exit" (click)="modalService.cancel()"></div>
<div class="block" [ngStyle]="{ 'background-color': tower.getColorOfBlock(editedValues[i]) | color }"></div>
<h1>View item</h1> <h1>View item</h1>
</div> </div>
<div class="select-add-container"> <div class="select-add-container">
<app-select-add <app-select-add
class="select" class="select"
[disabled]="!editMode"
[options]="tower.tags" [options]="tower.tags"
[default]="blocks[i - 1].tag" [default]="editedValues[i].tag"
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
[placeholder]="'Tag this item…'" [placeholder]="'Tag this item…'"
(value)="blocks[i - 1].changeKeys({ tag: $event })" (value)="editedValues[i].tag = $event"
></app-select-add> ></app-select-add>
</div> </div>
<textarea <textarea
[disabled]="!editMode"
placeholder="Write a description here…" placeholder="Write a description here…"
[value]="blocks[i - 1].description" [(ngModel)]="editedValues[i].description"
(change)="blocks[i - 1].changeKeys({ description: $event.target.value })"
></textarea> ></textarea>
<div> <div>
<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]="blocks[i - 1].isDone" [default]="blocks[i].isDone"
(value)="blocks[i - 1].changeKeys({ isDone: $event })" [disabled]="!editMode"
(value)="editedValues[i].isDone = $event"
></app-toggle> ></app-toggle>
</div> </div>
<div class="edit {{ editMode ? 'active' : '' }}" (click)="editMode = !editMode"> <div class="bottom">
<img src="assets/pen.svg" alt="edit" /> <button class="{{ editMode && i + 1 === activeChild ? '' : 'hidden' }}" (click)="submitChange()"></button>
<div class="edit {{ editMode && i + 1 === activeChild ? 'active' : '' }}" (click)="editMode = !editMode">
<img src="assets/pen.svg" alt="edit" />
</div>
</div> </div>
</div> </div>
<div (click)="$event.stopPropagation()" class="card {{ false ? 'active' : '' }}"> <div
(click)="$event.stopPropagation(); scrollToChild(blocks.length + 1)"
class="card {{ blocks.length + 1 === activeChild ? 'active' : '' }} {{
blocks.length === activeChild ? 'near-active' : ''
}} "
>
<div class="mask"></div> <div class="mask"></div>
<div class="header"> <div class="header">
@ -60,22 +73,24 @@
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
[placeholder]="'Tag this item…'" [placeholder]="'Tag this item…'"
(value)="editedValues.tag = $event" (value)="top(editedValues).tag = $event"
></app-select-add> ></app-select-add>
</div> </div>
<textarea placeholder="Write a description here…" [(ngModel)]="editedValues.description"></textarea> <textarea placeholder="Write a description here…" [(ngModel)]="top(editedValues).description"></textarea>
<div> <div>
<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]="onlyDone" [default]="onlyDone"
(value)="editedValues.isDone = $event" (value)="top(editedValues).isDone = $event"
></app-toggle> ></app-toggle>
</div> </div>
<button (click)="submitAdd()" [disabled]="!editedValues.tag">Create</button> <div class="bottom">
<button (click)="submitAdd()" [disabled]="!top(editedValues).tag">Create and exit</button>
</div>
</div> </div>
<div class="card transparent"></div> <div class="card placeholder"></div>
</section> </section>

View file

@ -37,6 +37,7 @@
max-width: 400px; max-width: 400px;
@media (max-width: $mobile-width) { @media (max-width: $mobile-width) {
width: 300px; width: 300px;
opacity: 1 !important;
} }
box-sizing: border-box; box-sizing: border-box;
@ -44,13 +45,17 @@
margin: calc(var(--large-padding) / 2); margin: calc(var(--large-padding) / 2);
position: relative; position: relative;
&.near-active {
cursor: pointer;
}
.mask { .mask {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 100; z-index: 10000;
@include card(); @include card();
} }
@ -59,68 +64,100 @@
margin-left: var(--large-padding); margin-left: var(--large-padding);
} }
&.transparent { &.placeholder {
opacity: 0; opacity: 0;
width: 1000px;
max-width: 1000px;
} }
@include inner-spacing(var(--large-padding)); @include inner-spacing(var(--large-padding));
.header { .header {
@include center-child(); @include center-child();
position: relative;
.exit { .exit {
position: absolute; position: absolute;
left: var(--large-padding); left: 0;
@include exit(); @include exit();
} }
.block {
@include square(12px);
margin-right: 10px;
}
} }
.edit { .bottom {
height: 32px;
@media (max-width: $mobile-width) {
height: 24px;
}
position: relative; position: relative;
display: inline-block;
float: right;
opacity: 0.25;
cursor: pointer;
img { button {
@include square(16px); margin: 0;
}
transition: opacity $short-animation-time;
&:before {
content: '';
display: block;
position: absolute; position: absolute;
bottom: calc(-1 * #{$line-height}); left: 50%;
left: 0; top: 50%;
height: $line-height; transform: translateY(-50%) translateX(-50%);
background-color: $text-color;
width: 0; transition: opacity $short-animation-time;
transition: width $long-animation-time;
&.hidden {
opacity: 0;
}
} }
@media (min-width: $mobile-width) { .edit {
&:hover { position: absolute;
opacity: 0.5; right: 0;
top: 50%;
transform: translateY(-50%);
opacity: 0.25;
cursor: pointer;
img {
@include square(16px);
} }
&:hover {
transition: opacity $short-animation-time;
&:before {
content: '';
display: block;
position: absolute;
bottom: calc(-1 * #{$line-height});
left: 0;
height: $line-height;
background-color: $text-color;
width: 0;
transition: width $long-animation-time;
}
@media (min-width: $mobile-width) {
&:hover {
opacity: 0.5;
}
&:hover {
&:before {
width: 100% !important;
}
}
}
&.active {
&:before { &:before {
width: 100% !important; width: 100% !important;
} }
} }
}
&.active { &.active {
&:before { opacity: 1;
width: 100% !important;
} }
} }
&.active {
opacity: 1;
}
} }
} }

View file

@ -6,9 +6,7 @@ import { Block } from '../../../../model/block';
import { IBlock } from '../../../../interfaces/persistance/block'; import { IBlock } from '../../../../interfaces/persistance/block';
import { CancelService } from '../../../../services/cancel.service'; import { CancelService } from '../../../../services/cancel.service';
import { range } from 'src/app/utils/range'; import { range } from 'src/app/utils/range';
import { el } from '@angular/platform-browser/testing/src/browser_util'; import { top } from 'src/app/utils/top';
const SWIPE_LIMIT = 0.35;
@Component({ @Component({
selector: 'app-blocks', selector: 'app-blocks',
@ -17,10 +15,11 @@ const SWIPE_LIMIT = 0.35;
}) })
export class BlocksComponent implements OnInit, OnDestroy { export class BlocksComponent implements OnInit, OnDestroy {
readonly range = range; readonly range = range;
readonly top = top;
tower: Tower; tower: Tower;
editedValues: Partial<IBlock>; editedValues: Array<Partial<IBlock>>;
endOfScrollToken = 0; endOfScrollToken = 0;
editMode = false; editMode = false;
@ -76,13 +75,16 @@ export class BlocksComponent implements OnInit, OnDestroy {
this.onlyDone = onlyDone; this.onlyDone = onlyDone;
this.subscription = tower$.subscribe(value => { this.subscription = tower$.subscribe(value => {
this.tower = value; this.tower = value;
setTimeout(() => this.scrollToChild(this.blocks.indexOf(startBlock) + 1, true)); this.editedValues = this.blocks.map(({ isDone, description, tag }) => ({ isDone, description, tag }));
}); this.editedValues.push({
isDone: this.onlyDone,
description: ''
});
this.editedValues = { setTimeout(() =>
isDone: onlyDone, this.scrollToChild(startBlock ? this.blocks.indexOf(startBlock) + 1 : this.blocks.length + 1, true)
description: '' );
}; });
} }
animateScroll() { animateScroll() {
@ -106,8 +108,9 @@ export class BlocksComponent implements OnInit, OnDestroy {
animate(cardStyle, maskStyle, t: number) { animate(cardStyle, maskStyle, t: number) {
t = Math.min(1, Math.max(0, t)); t = Math.min(1, Math.max(0, t));
cardStyle.opacity = (1 - t / 1.5).toString();
maskStyle.opacity = Math.pow(t, 0.5).toString(); maskStyle.opacity = Math.pow(t, 0.5).toString();
maskStyle.display = t === 0 ? 'none' : 'block'; maskStyle.display = t <= 0.1 ? 'none' : 'block';
} }
adjustPosition() { adjustPosition() {
@ -141,8 +144,13 @@ export class BlocksComponent implements OnInit, OnDestroy {
} }
submitAdd() { submitAdd() {
this.editedValues.created = new Date(); top(this.editedValues).created = new Date();
this.tower.addBlock(this.editedValues as IBlock); this.tower.addBlock(top(this.editedValues) as IBlock);
this.modalService.submit();
}
submitChange() {
this.blocks[this.activeChild - 1].changeKeys(this.editedValues[this.activeChild - 1]);
this.modalService.submit(); this.modalService.submit();
} }

View file

@ -1,5 +1,8 @@
<div class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }}" (click)="$event.stopPropagation()"> <div
<div #top class="top" (click)="!editMode && toggle()"> class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }} {{ disabled ? 'disabled' : '' }}"
(click)="$event.stopPropagation()"
>
<div #top class="top" (click)="!editMode && !disabled && toggle()">
<p [innerHTML]="selected ? selected : placeholder" *ngIf="!editMode || !selected; else editableSelected"></p> <p [innerHTML]="selected ? selected : placeholder" *ngIf="!editMode || !selected; else editableSelected"></p>
<ng-template #editableSelected> <ng-template #editableSelected>
<input type="text" [value]="selected" (change)="changeOption(selected, $event)" /> <input type="text" [value]="selected" (change)="changeOption(selected, $event)" />

View file

@ -5,6 +5,12 @@ $inner-padding: var(--medium-padding);
width: 100%; width: 100%;
position: relative; position: relative;
&.disabled {
.top {
cursor: not-allowed !important;
}
}
.top, .top,
.bottom { .bottom {
padding: $inner-padding; padding: $inner-padding;

View file

@ -13,6 +13,7 @@ export class SelectAddComponent {
@Input() alwaysDropShadow = false; @Input() alwaysDropShadow = false;
@Input() onlyShadowBorder = false; @Input() onlyShadowBorder = false;
@Input() editable = false; @Input() editable = false;
@Input() disabled = false;
@Input() set default(value: string) { @Input() set default(value: string) {
this.selected = value; this.selected = value;

View file

@ -1,5 +1,7 @@
<span [className]="!on ? 'active' : ''" (click)="on = false" [innerText]="beforeText"></span> <span [className]="!on ? 'active' : ''" (click)="on = false" [innerText]="beforeText"></span>
<input type="checkbox" [(ngModel)]="on" [className]="on ? 'on' : ''" /> <label class="{{ disabled ? 'disabled' : '' }}">
<input type="checkbox" [disabled]="disabled" [(ngModel)]="on" [className]="on ? 'on' : ''" />
</label>
<span [className]="on ? 'active' : ''" (click)="on = true" [innerText]="afterText"></span> <span [className]="on ? 'active' : ''" (click)="on = true" [innerText]="afterText"></span>

View file

@ -24,46 +24,54 @@
} }
} }
input[type='checkbox'] { label {
-webkit-appearance: none; display: block;
-moz-appearance: none;
width: 2 * $size; input[type='checkbox'] {
height: $size; -webkit-appearance: none;
-moz-appearance: none;
border-radius: 1000px; width: 2 * $size;
box-shadow: $shadow-border; height: $size;
position: relative;
cursor: pointer;
&:after {
content: '';
position: absolute;
display: block;
left: 0;
@include square($size);
border-radius: 1000px; border-radius: 1000px;
background-color: $text-color; box-shadow: $shadow-border;
transition: box-shadow $long-animation-time, left $long-animation-time, transform $long-animation-time; position: relative;
} cursor: pointer;
@media (min-width: $mobile-width) { &:after {
&:hover:after { content: '';
box-shadow: $shadow; position: absolute;
transform: translateX(2px); display: block;
left: 0;
@include square($size);
border-radius: 1000px;
background-color: $text-color;
transition: box-shadow $long-animation-time, left $long-animation-time, transform $long-animation-time;
} }
&.on:hover:after { &.on:after {
transform: translateX(-2px); left: $size;
} }
} }
&.on:after { &:not(.disabled) {
left: $size; input[type='checkbox'] {
@media (min-width: $mobile-width) {
&:hover:after {
box-shadow: $shadow;
transform: translateX(2px);
}
&.on:hover:after {
transform: translateX(-2px);
}
}
}
} }
} }
} }

View file

@ -9,6 +9,8 @@ export class ToggleComponent {
@Input() beforeText: string; @Input() beforeText: string;
@Input() afterText: string; @Input() afterText: string;
@Input() disabled = false;
@Output() value: EventEmitter<boolean> = new EventEmitter(); @Output() value: EventEmitter<boolean> = new EventEmitter();
@Input() set default(value: boolean) { @Input() set default(value: boolean) {

View file

@ -48,6 +48,10 @@ export class Tower extends Serializable implements ITower, TowerState {
this.changeKeys({ name }); this.changeKeys({ name });
} }
getColorOfBlock(block: Block): IColor {
return lighten((hash(block.tag) - 0.5) * 50, this.baseColor);
}
protected onAfterClone() { protected onAfterClone() {
this.blocks.sort((a, b) => { this.blocks.sort((a, b) => {
return a.created.getTime() - b.created.getTime(); return a.created.getTime() - b.created.getTime();
@ -55,7 +59,7 @@ export class Tower extends Serializable implements ITower, TowerState {
this.coloredBlocks = this.blocks.map(b => { this.coloredBlocks = this.blocks.map(b => {
const coloredBlock = b as ColoredBlock; const coloredBlock = b as ColoredBlock;
coloredBlock.color = lighten((hash(coloredBlock.tag) - 0.5) * 50, this.baseColor); coloredBlock.color = this.getColorOfBlock(b);
return coloredBlock; return coloredBlock;
}); });