Reformat code

This commit is contained in:
schmelczerandras 2019-08-24 15:25:43 +02:00
parent 6e27539eca
commit 420cd788c4
94 changed files with 10592 additions and 2608 deletions

View file

@ -0,0 +1,9 @@
<main class="{{ modalService.active ? 'active' : '' }}" [ngSwitch]="modalService.active?.type">
<app-create-block *ngSwitchCase="ModalType.createBlock"></app-create-block>
<app-edit-block *ngSwitchCase="ModalType.editBlock"></app-edit-block>
<app-remove-block *ngSwitchCase="ModalType.removeBlock"></app-remove-block>
<app-remove-tower *ngSwitchCase="ModalType.removeTower"></app-remove-tower>
<app-settings *ngSwitchCase="ModalType.settings"></app-settings>
<app-get-started *ngSwitchCase="ModalType.getStarted"></app-get-started>
<app-remove-page *ngSwitchCase="ModalType.removePage"></app-remove-page>
</main>

View file

@ -0,0 +1,29 @@
@import '../../../styles';
main {
position: absolute;
top: 0;
left: 0;
z-index: 10000;
width: 100%;
height: 100%;
@include center-child();
padding: var(--large-padding);
box-sizing: border-box;
background: $background-gradient;
transition: opacity 300ms;
&:not(.active) {
opacity: 0;
pointer-events: none;
}
button {
margin-top: var(--medium-padding);
}
}

View file

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { ModalService, ModalType } from '../../services/modal.service';
@Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.scss']
})
export class ModalComponent {
// Needed for accessing the enum from html.
ModalType = ModalType;
constructor(public modalService: ModalService) {
window.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.modalService.cancel();
}
});
}
}

View file

@ -0,0 +1,33 @@
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>Create an item</h1>
</div>
<div class="select-add-container">
<!-- wrapper for easier styling -->
<app-select-add
class="select"
[options]="modalService.active.input"
[default]="modalService.active.input[0]"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
[placeholder]="'Tag this item…'"
(value)="selected = $event"
></app-select-add>
</div>
<!-- wrapper for easier styling -->
<textarea placeholder="Write a description here…" [(ngModel)]="description"></textarea>
<div>
<!-- wrapper for easier styling -->
<app-toggle
[beforeText]="'This task hasn\'t been finished yet'"
[afterText]="'Goal already accomplished'"
[default]="false"
(value)="isDone = $event"
></app-toggle>
</div>
<!-- wrapper for easier styling -->
<button (click)="submit()">Create</button>

View file

@ -0,0 +1,29 @@
@import '../../../../../styles';
:host {
@include card();
box-shadow: $shadow;
width: 66vw;
max-width: 400px;
@media (max-width: $mobile-width) {
width: 300px;
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
@include inner-spacing(var(--large-padding));
.header {
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
@include exit();
}
}
}

View file

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
@Component({
selector: 'app-create-block',
templateUrl: './create-block.component.html',
styleUrls: ['./create-block.component.scss']
})
export class CreateBlockComponent {
selected: string;
description: string = null;
isDone: boolean;
constructor(public modalService: ModalService) {}
submit() {
this.modalService.submit({
selected: this.selected,
description: this.description,
isDone: this.isDone
});
}
}

View file

@ -0,0 +1,33 @@
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>View item</h1>
</div>
<div class="select-add-container">
<!-- wrapper for easier styling -->
<app-select-add
class="select"
[options]="modalService.active.input.options"
[default]="modalService.active.input.options[0]"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
[placeholder]="'Tag this item…'"
(value)="selected = $event"
></app-select-add>
</div>
<!-- wrapper for easier styling -->
<textarea placeholder="Write a description here…" [(ngModel)]="modalService.active.input.description"></textarea>
<div>
<!-- wrapper for easier styling -->
<app-toggle
[beforeText]="'This task hasn\'t been finished yet'"
[afterText]="'Goal already accomplished'"
[default]="modalService.active.input.isDone"
(value)="modalService.active.input.isDone = $event"
></app-toggle>
</div>
<!-- wrapper for easier styling -->
<button (click)="submit()">Modify</button>

View file

@ -0,0 +1,29 @@
@import '../../../../../styles';
:host {
@include card();
box-shadow: $shadow;
width: 66vw;
max-width: 400px;
@media (max-width: $mobile-width) {
width: 300px;
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
@include inner-spacing(var(--large-padding));
.header {
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
@include exit();
}
}
}

View file

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
@Component({
selector: 'app-edit-block',
templateUrl: './edit-block.component.html',
styleUrls: ['./edit-block.component.scss']
})
export class EditBlockComponent {
selected: string;
constructor(public modalService: ModalService) {}
submit() {
this.modalService.submit({
selected: this.selected,
description: this.modalService.active.input.description,
isDone: this.modalService.active.input.isDone
});
}
}

View file

@ -0,0 +1,3 @@
<p>
get-started works!
</p>

View file

@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-get-started',
templateUrl: './get-started.component.html',
styleUrls: ['./get-started.component.scss']
})
export class GetStartedComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View file

@ -0,0 +1,3 @@
<p>
remove-block works!
</p>

View file

@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-remove-block',
templateUrl: './remove-block.component.html',
styleUrls: ['./remove-block.component.scss']
})
export class RemoveBlockComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View file

@ -0,0 +1,13 @@
<section>
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>Are you sure?</h1>
</div>
<p>
You are trying to remove <strong>{{ this.modalService.active.input }}</strong
>.
</p>
<button (click)="modalService.submit()">Remove</button>
</section>

View file

@ -0,0 +1,30 @@
@import '../../../../../styles';
section {
@include card();
width: 66vw;
max-width: 500px;
@media (max-width: $mobile-width) {
width: 300px;
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
box-shadow: $shadow;
@include inner-spacing(var(--large-padding));
.header {
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
@include exit();
}
}
}

View file

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
@Component({
selector: 'app-remove-page',
templateUrl: './remove-page.component.html',
styleUrls: ['./remove-page.component.scss']
})
export class RemovePageComponent {
constructor(public modalService: ModalService) {}
}

View file

@ -0,0 +1,16 @@
<section>
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>Are you sure?</h1>
</div>
<p>
You are trying to remove
<span [ngStyle]="{ color: modalService.active.input.baseColor.toString() }">{{
modalService.active.input.name ? modalService.active.input.name : 'an unnamed tower'
}}</span
>.
</p>
<button (click)="modalService.submit()">Remove</button>
</section>

View file

@ -0,0 +1,30 @@
@import '../../../../../styles';
section {
@include card();
width: 66vw;
max-width: 500px;
@media (max-width: $mobile-width) {
width: 300px;
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
box-shadow: $shadow;
@include inner-spacing(var(--large-padding));
.header {
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
@include exit();
}
}
}

View file

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
@Component({
selector: 'app-remove-tower',
templateUrl: './remove-tower.component.html',
styleUrls: ['./remove-tower.component.scss']
})
export class RemoveTowerComponent {
constructor(public modalService: ModalService) {}
}

View file

@ -0,0 +1,19 @@
<div class="header">
<div class="exit" (click)="modalService.cancel()"></div>
<h1>Settings</h1>
</div>
<div>
<!-- wrapper for easier styling -->
<app-toggle
[beforeText]="'Hide create tower button'"
[afterText]="'Show create tower button'"
[default]="!dataService.active.userData?.hideCreateTowerButton"
(value)="dataService.active.userData.hideCreateTowerButton = !$event"
></app-toggle>
</div>
<!-- wrapper for easier styling -->
<p *ngIf="dataService.active?.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

@ -0,0 +1,34 @@
@import '../../../../../styles';
:host {
@include card();
width: 66vw;
max-width: 500px;
@media (max-width: $mobile-width) {
width: 300px;
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
box-shadow: $shadow;
@include inner-spacing(var(--large-padding));
.header {
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
@include exit();
}
}
p {
font-size: var(--medium-font-size);
}
}

View file

@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
import { DataService } from '../../../../services/data.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent {
constructor(public modalService: ModalService, public dataService: DataService) {}
async deletePage() {
try {
await this.modalService.showRemovePage(this.dataService.active.name);
this.dataService.remove();
this.modalService.submit();
} catch {
// pass
}
}
}

View file

@ -0,0 +1,35 @@
<button
*ngIf="page && page.towers.length < 5 && !dataService.active?.userData?.hideCreateTowerButton"
(click)="createTower()"
>
Create tower
</button>
<section class="towers" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropDrag($event)">
<app-tower
*ngFor="let tower of page?.towers"
[tower]="tower"
[dateRange]="dateRange"
cdkDrag
(cdkDragStarted)="startDrag(page.towers.indexOf(tower))"
></app-tower>
</section>
<img
*ngIf="isDragging"
src="assets/trash.svg"
alt="trashcan"
(pointerenter)="trashEnter()"
(pointerleave)="trashExit()"
(pointerup)="removeTower()"
/>
<div class="double-slider-container">
<app-double-slider
*ngIf="!isDragging && dates.length >= MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER"
[values]="dates"
[labels]="dateLabels"
(lowerBound)="startDate = $event"
(upperBound)="endDate = $event"
></app-double-slider>
</div>

View file

@ -0,0 +1,53 @@
@import '../../../../styles';
:host {
display: flex;
flex-direction: column;
height: 100%;
@include inner-spacing(var(--large-padding));
button {
margin-top: 0;
}
.towers {
display: flex;
justify-content: center;
width: 100%;
flex: 1 0 auto;
transition: box-shadow $short-animation-time;
&.cdk-drop-list-dragging {
*:not(.cdk-drag-placeholder) {
transition: transform $long-animation-time cubic-bezier(0, 0, 0.2, 1);
}
}
@include inner-spacing(var(--medium-padding), $horizontal: true);
}
.double-slider-container {
@media (max-height: $min-height) {
display: none;
}
}
img {
@include square(47.33333px);
padding: 47.3333px 0;
margin: auto;
position: relative;
z-index: 1500;
transition: transform $long-animation-time;
&:hover {
transform: scale(1.2);
}
}
}

View file

@ -0,0 +1,98 @@
import { Component, Input } from '@angular/core';
import { Page } from '../../../model/page';
import { ModalService } from '../../../services/modal.service';
import { DataService } from '../../../services/data.service';
@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;
}
this._page = value;
value.subscribe(() => this.updateDates());
this.updateDates();
}
get page(): Page {
return this._page;
}
readonly MIN_BLOCK_COUNT_BEFORE_SHOWING_SLIDER = 3;
isDragging = false;
draggedTowerIndex: number;
nearTrashcan = false;
dates: Date[] = [];
startDate: Date;
endDate: 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) {}
createTower() {
this.page.addTower();
}
dropDrag(event: any) {
this.page.moveTower(event);
this.isDragging = false;
}
startDrag(id: number) {
this.draggedTowerIndex = id;
this.isDragging = true;
}
trashEnter() {
this.nearTrashcan = true;
window.document.querySelector('.cdk-drag-preview').className += ' trash-highlight';
}
trashExit() {
this.nearTrashcan = false;
const elem = window.document.querySelector('.cdk-drag-preview');
elem.className = elem.className
.split(' ')
.slice(0, -1)
.join(' ');
}
async removeTower() {
try {
const tower = this.page.towers[this.draggedTowerIndex];
await this.modalService.showRemoveTower(tower);
this.page.removeTower(tower);
} catch {
// 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

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

View file

@ -0,0 +1,15 @@
@import '../../../../../../styles';
:host {
position: relative;
width: calc(100% / 6);
padding-bottom: calc(100% / 6);
div {
position: absolute;
width: 100%;
height: 100%;
@include gravitate();
}
}

View file

@ -0,0 +1,32 @@
import { Component, Input } from '@angular/core';
import { Block } from '../../../../../model/block';
import { ModalService } from '../../../../../services/modal.service';
import { Tower } from '../../../../../model/tower';
@Component({
selector: 'app-block',
templateUrl: './block.component.html',
styleUrls: ['./block.component.scss']
})
export class BlockComponent {
@Input() block: Block;
@Input() tower: Tower;
constructor(private modalService: ModalService) {}
async handleClick() {
try {
const { selected, description, isDone } = await this.modalService.showEditBlock({
options: this.tower.tags,
default: this.block.tag,
description: this.block.description,
isDone: this.block.isDone
});
this.block.tag = selected;
this.block.description = description;
this.block.isDone = isDone;
} catch {
// pass
}
}
}

View file

@ -0,0 +1,16 @@
<div class="container">
<p class="header" (click)="isOpen = !isOpen">
{{ tasks.length == 0 ? 'no' : '' }}
<strong>
{{ tasks.length == 0 ? '' : tasks.length }}
</strong>
{{ tasks.length == 1 ? 'task' : 'tasks' }}
</p>
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
<p
*ngFor="let task of tasks"
(click)="handleClick(task)"
[innerText]="task.description ? task.description : 'unknown'"
></p>
</div>
</div>

View file

@ -0,0 +1,53 @@
@import '../../../../../../styles';
:host {
width: 100%;
box-sizing: border-box;
.container {
@include card();
box-shadow: $shadow-border;
padding: var(--small-padding);
margin: var(--small-padding);
max-height: 30vh;
overflow-y: auto;
.header {
cursor: pointer;
}
p {
width: 100%;
font-size: var(--medium-font-size);
}
.all-task {
@include inner-spacing(var(--small-padding));
:first-child {
margin-top: var(--small-padding);
}
width: 100%;
box-sizing: border-box;
overflow-y: hidden;
height: 0;
transition: height $long-animation-time;
p {
max-width: 60px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
text-align: left;
&:hover {
font-weight: bold;
}
}
}
}
}

View file

@ -0,0 +1,41 @@
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Block } from '../../../../../model/block';
import { Tower } from '../../../../../model/tower';
import { ModalService } from '../../../../../services/modal.service';
@Component({
selector: 'app-tasks',
templateUrl: './tasks.component.html',
styleUrls: ['./tasks.component.scss']
})
export class TasksComponent implements OnInit {
@Input() tasks: Block[];
@Input() tower: Tower;
@Input() isOpen = false;
@ViewChild('allTask') allTask: ElementRef;
constructor(private modalService: ModalService) {}
ngOnInit() {}
async handleClick(block: Block) {
try {
const { selected, description, isDone } = await this.modalService.showEditBlock({
options: this.tower.tags,
default: block.tag,
description: block.description,
isDone: block.isDone
});
block.tag = selected;
block.description = description;
if (!block.isDone && isDone) {
block.created = new Date();
}
block.isDone = isDone;
} catch {
// pass
}
}
}

View file

@ -0,0 +1,22 @@
<div class="tower">
<div class="container">
<div class="tasks-container">
<app-tasks [tasks]="tasks" [tower]="tower"></app-tasks>
</div>
<img src="assets/plus-sign.svg" alt="add item" (click)="addBlock()" />
<div class="block-container {{ isFalling ? 'falling' : '' }}">
<app-block *ngFor="let block of drawableBlocks" [block]="block" [tower]="tower"></app-block>
</div>
</div>
<label for="tower-name" class="hidden">Card name</label>
<input
id="tower-name"
type="text"
placeholder="…"
[(ngModel)]="tower.name"
[ngStyle]="{ color: tower.baseColor.toString() }"
/>
</div>

View file

@ -0,0 +1,140 @@
@import '../../../../../styles';
:host {
cursor: pointer;
&.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
&.cdk-drag-placeholder {
opacity: 0;
}
&.cdk-drag-preview,
&:hover {
div {
.container {
box-shadow: $shadow;
}
}
}
&.trash-highlight {
.container {
transform: scale(0.75);
position: relative;
:before {
opacity: 0.5 !important;
}
}
input {
display: none;
}
}
.tower {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
@include inner-spacing(var(--small-padding));
.container {
display: flex;
flex-direction: column;
flex: 1 1 auto;
position: relative;
@include card();
transition: transform $short-animation-time, box-shadow $long-animation-time;
@include inner-spacing(var(--medium-padding));
width: 100%;
:before {
content: '';
pointer-events: none;
position: absolute;
z-index: 3;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: red;
opacity: 0;
border-radius: var(--border-radius);
transition: opacity $short-animation-time;
}
.tasks-container {
}
img {
position: relative;
z-index: 2;
height: 56px;
@media (max-width: $mobile-width) {
height: 42px;
}
opacity: 0.33;
transition: opacity $long-animation-time;
cursor: pointer;
&:hover {
opacity: 1;
}
}
.block-container {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: flex-end;
transform: scaleY(-1);
flex: 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;
}
100% {
transform: translateY(0);
}
}
}
}
}
input[type='text'] {
font-size: var(--medium-font-size);
text-align: center;
@media (min-width: $mobile-width) {
width: 50%;
}
}
}
}

View file

@ -0,0 +1,49 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Tower } from '../../../../model/tower';
import { ModalService } from '../../../../services/modal.service';
import { Block } from '../../../../model/block';
@Component({
selector: 'app-tower',
templateUrl: './tower.component.html',
styleUrls: ['./tower.component.scss']
})
export class TowerComponent {
@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;
}
public constructor(private modalService: ModalService) {}
get drawableBlocks(): Block[] {
return this.tower.blocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
);
}
get tasks(): Block[] {
return this.tower.blocks.filter(block => !block.isDone);
}
@Input() tower: Tower;
_dateRange: { from: Date; to: Date };
isFalling = true;
public async addBlock() {
try {
const { selected: tag, description, isDone } = await this.modalService.showCreateBlock(this.tower.tags);
this.tower.addBlock({ tag, description, isDone });
} catch (e) {
// pass
}
}
}

View file

@ -0,0 +1,18 @@
<div class="select-add-container">
<!-- wrapper for easier styling -->
<app-select-add
[options]="dataService.pageNames"
[default]="dataService.active?.name"
(value)="selectPage($event)"
[placeholder]="'Add a new page…'"
></app-select-add>
</div>
<!-- wrapper for easier styling -->
<div class="page-container">
<!-- wrapper for easier styling -->
<app-page *ngIf="dataService.active !== null" [page]="dataService.active"></app-page>
</div>
<!-- wrapper for easier styling -->
<button (click)="openSettings()">Settings</button>

View file

@ -0,0 +1,21 @@
@import '../../../styles';
:host {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
@include inner-spacing(var(--large-padding));
.select-add-container {
width: 250px;
margin-left: auto;
margin-right: auto;
}
.page-container {
flex: 1 0 auto;
}
}

View file

@ -0,0 +1,40 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { Page } from '../../model/page';
import { DataService } from '../../services/data.service';
import { ModalService } from '../../services/modal.service';
@Component({
selector: 'app-pages',
templateUrl: './pages.component.html',
styleUrls: ['./pages.component.scss']
})
export class PagesComponent {
@ViewChild('top') top: ElementRef;
@ViewChild('page') page: ElementRef;
@ViewChild('bottom') bottom: ElementRef;
constructor(public dataService: DataService, private modalService: ModalService) {}
async selectPage(selected: string) {
if (!this.dataService.pageNames.includes(selected)) {
const page = new Page({
name: selected,
towers: [],
userData: {}
});
this.dataService.push(page);
page.addTower();
}
await this.dataService.changeActiveByName(selected);
}
async openSettings() {
try {
await this.modalService.showSettings();
} catch {
// pass
}
}
}

View file

@ -0,0 +1,13 @@
<div class="container">
<label for="date-selector-1">date selector 1</label>
<label for="date-selector-2">date selector 2</label>
<input id="date-selector-1" type="range" min="0" [max]="MAX - 1" [(ngModel)]="oneValue" />
<input id="date-selector-2" type="range" min="0" [max]="MAX - 1" [(ngModel)]="otherValue" />
<div class="value-container">
<span
*ngFor="let i of drawnLabelsIndices"
[innerHTML]="drawnLabels[i]"
[ngStyle]="{ transform: getOffset(i) }"
></span>
</div>
</div>

View file

@ -0,0 +1,75 @@
@import '../../../../styles';
$height: 70px;
$width: 300px;
$slider-size: 40px;
.container {
width: $width;
height: $height;
position: relative;
margin: $slider-size / 2 auto 0 auto;
label {
display: none;
}
input[type='range'] {
width: 100%;
position: absolute;
left: 0;
-webkit-appearance: none;
outline: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
height: $slider-size;
width: $slider-size;
border-radius: 1000px;
background-color: $light-color;
transform-origin: center center;
transform: translateY(-$slider-size / 2 + $line-height / 2);
transition: box-shadow $long-animation-time, transform $long-animation-time;
&:hover {
box-shadow: $shadow;
transform: translateY(-$slider-size / 2 + $line-height / 2) scale(1.1);
}
cursor: pointer;
position: relative;
z-index: 2;
}
&::-webkit-slider-runnable-track {
-webkit-appearance: none;
width: 100%;
height: $line-height;
background-color: $text-color;
border-radius: 1000px;
}
&::-moz-focus-outer {
border: 0;
}
}
.value-container {
@include small-text();
display: flex;
justify-content: space-evenly;
span {
display: block;
margin-top: 10px;
}
}
}

View file

@ -0,0 +1,98 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { range } from '../../../utils/range';
@Component({
selector: 'app-double-slider',
templateUrl: './double-slider.component.html',
styleUrls: ['./double-slider.component.scss']
})
export class DoubleSliderComponent {
@Input() labels: string[];
@Input() set values(values: any[]) {
if (values.length === 0) {
return;
}
this._values = values;
this.calculateLabels();
this.emitValue();
}
get values(): any[] {
return this._values;
}
get oneValue(): number {
return this._oneValue;
}
set oneValue(value: number) {
this._oneValue = value;
this.emitValue();
}
get otherValue(): number {
return this._otherValue;
}
set otherValue(value: number) {
this._otherValue = value;
this.emitValue();
}
private _values: any[];
@Output() lowerBound: EventEmitter<any> = new EventEmitter();
@Output() upperBound: EventEmitter<any> = new EventEmitter();
drawnLabels: string[];
readonly MAX = 100;
private _oneValue = 0;
private _otherValue: number = this.MAX - 1;
drawnLabelsIndices: Iterable<number>;
private calculateLabels() {
const labelCount = 6;
const jumpLength = Math.round(this.labels.length / labelCount);
this.drawnLabels = this.labels.filter((_, index) => index % jumpLength === 0);
this.drawnLabelsIndices = range({ max: this.drawnLabels.length });
}
private indexFromValue(value: number): number {
return Math.floor((value / this.MAX) * this.values.length);
}
getOffset(index: number): string {
const labelIndex = index / this.drawnLabels.length;
const slider1Index = this.oneValue / this.MAX - 0.1;
const slider2Index = this.otherValue / this.MAX - 0.1;
const dist = (a, b) => Math.abs(a - b);
const labelSliderDistance = Math.min(dist(labelIndex, slider1Index), dist(labelIndex, slider2Index));
const ACTIVE_ZONE = 0.2;
const BASE_TRANSFORM = 'translateX(-50%) rotate(-45deg) translateY(100%)';
if (labelSliderDistance > ACTIVE_ZONE) {
return BASE_TRANSFORM;
}
return `translateY(${Math.pow((ACTIVE_ZONE - labelSliderDistance) / ACTIVE_ZONE, 1) * 30}px) ${BASE_TRANSFORM}`;
}
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)]);
}
}
}

View file

@ -0,0 +1,27 @@
<div class="select-add {{ onlyShadowBorder ? 'shadow-border' : '' }}">
<div #top class="top" (click)="toggle()">
<p [innerHTML]="selected ? selected : placeholder"></p>
<img src="assets/arrow.svg" [className]="isOpen ? 'upside-down' : ''" alt="arrow" />
</div>
<div class="bottom-container">
<div #bottom class="bottom {{ isOpen ? 'open' : '' }}">
<p *ngFor="let option of otherOptions" [innerHTML]="option" (click)="select(option)"></p>
<input
type="text"
*ngIf="options.length <= maxItemCount"
placeholder="Add a value…"
[(ngModel)]="newOption"
(keyup)="handleKeys($event)"
/>
<button *ngIf="options.length <= maxItemCount" (click)="addNewOption()">Add</button>
</div>
</div>
<div
class="background {{ isOpen || alwaysDropShadow ? 'active' : '' }}"
[ngStyle]="{ height: backgroundHeight }"
></div>
</div>

View file

@ -0,0 +1,117 @@
@import '../../../../styles';
$inner-padding: var(--medium-padding);
.select-add {
width: 100%;
position: relative;
.top,
.bottom {
padding: $inner-padding;
z-index: 4;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
cursor: pointer;
p {
display: inline-block;
@include sub-title-text();
}
img {
@include square(16px);
transition: transform $long-animation-time;
&.upside-down {
transform: rotate(-180deg);
}
}
}
.bottom-container {
width: 100%;
height: 40vh;
position: absolute;
overflow-y: hidden;
pointer-events: none;
.bottom {
position: absolute;
width: 100%;
pointer-events: all;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
border-radius: 0 0 var(--border-radius) var(--border-radius);
padding-top: 0;
@include inner-spacing($inner-padding);
transform: translateY(-100%);
visibility: hidden;
&.open {
visibility: visible;
transform: none;
}
transition: transform $long-animation-time;
p {
@include sub-title-text();
display: inline-block;
text-align: left;
cursor: pointer;
}
}
}
.background {
position: absolute;
top: 0;
height: 100%;
width: 100%;
@include card();
z-index: 3;
transition: box-shadow $long-animation-time, height $long-animation-time;
&.active {
box-shadow: $shadow;
}
}
&:hover {
.background {
box-shadow: $shadow;
}
}
&.shadow-border {
.background.active {
box-shadow: $shadow-border;
}
}
&.shadow-border:hover {
.background {
box-shadow: $shadow-border;
}
}
}

View file

@ -0,0 +1,66 @@
import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-select-add',
templateUrl: './select-add.component.html',
styleUrls: ['./select-add.component.scss']
})
export class SelectAddComponent {
@Input() placeholder = 'Add a new value…';
@Input() maxItemCount = 7;
@Input() options: string[];
@Input() alwaysDropShadow = false;
@Input() onlyShadowBorder = false;
@Input() set default(value: string) {
this.selected = value;
if (value) {
this.value.emit(value);
}
}
@Output() value: EventEmitter<string> = new EventEmitter();
@ViewChild('top') top: ElementRef;
@ViewChild('bottom') bottom: ElementRef;
selected: string;
newOption: string;
isOpen = false;
get otherOptions(): string[] {
return this.options.filter(a => a !== this.selected);
}
handleKeys(event: KeyboardEvent) {
if (event.key === 'Enter') {
this.addNewOption();
}
}
get backgroundHeight(): string {
if (this.isOpen && this.top && this.bottom) {
const topHeight = this.top.nativeElement.clientHeight;
const bottomHeight = this.bottom.nativeElement.clientHeight;
return `${topHeight + bottomHeight}px`;
}
return `100%`;
}
addNewOption() {
if (this.newOption) {
this.select(this.newOption);
this.newOption = '';
}
}
select(option: string) {
this.selected = option;
this.value.emit(this.selected);
this.toggle();
}
toggle() {
this.isOpen = !this.isOpen;
}
}

View file

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

View file

@ -0,0 +1,66 @@
@import '../../../../styles';
:host {
$size: 30px;
@include center-child();
@include inner-spacing(var(--medium-padding), $horizontal: true);
span {
@include medium-text();
max-width: 3 * $size;
cursor: pointer;
&.active {
font-weight: bold;
}
&:first-of-type {
text-align: right;
}
&:last-of-type {
text-align: left;
}
}
input[type='checkbox'] {
-webkit-appearance: none;
width: 2 * $size;
height: $size;
border-radius: 1000px;
box-shadow: $shadow-border;
position: relative;
cursor: pointer;
&:after {
content: '';
position: absolute;
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;
}
&:hover:after {
box-shadow: $shadow;
transform: translateX(2px);
}
&.on:hover:after {
transform: translateX(-2px);
}
&.on:after {
left: $size;
}
}
}

View file

@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-toggle',
templateUrl: './toggle.component.html',
styleUrls: ['./toggle.component.scss']
})
export class ToggleComponent {
@Input() beforeText: string;
@Input() afterText: string;
@Output() value: EventEmitter<boolean> = new EventEmitter();
@Input() set default(value: boolean) {
this.on = value;
}
private _on = false;
set on(value: boolean) {
this._on = value;
this.value.emit(value);
}
get on(): boolean {
return this._on;
}
}