File
Metadata
changeDetection |
ChangeDetectionStrategy.OnPush |
selector |
spreadsheet-ui |
styleUrls |
./spreadsheet-ui.component.scss |
templateUrl |
./spreadsheet-ui.component.html |
Index
Properties
|
|
Methods
|
|
Inputs
|
|
Outputs
|
|
Outputs
addColumnClicked
|
Type : EventEmitter<void>
|
|
addRowClicked
|
Type : EventEmitter<void>
|
|
cellClicked
|
Type : EventEmitter<number>
|
|
Methods
cellClick
|
cellClick(index: number)
|
|
Parameters :
Name |
Type |
Optional |
index |
number
|
No
|
|
counter
|
counter(number: number | string)
|
|
Parameters :
Name |
Type |
Optional |
number |
number | string
|
No
|
Returns : any[]
|
getCellValue
|
getCellValue(index: number)
|
|
Parameters :
Name |
Type |
Optional |
index |
number
|
No
|
|
isCellActive
|
isCellActive(cellIndex: number)
|
|
Parameters :
Name |
Type |
Optional |
cellIndex |
number
|
No
|
|
activeCellInput
|
Type : ElementRef
|
Decorators :
@ViewChild('active_cell_input')
|
|
getColumnLabel
|
Default value : getColumnLabel
|
|
|
import { Component, Input, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, ElementRef } from "@angular/core"
import { FormGroup } from '@angular/forms'
import { Cell, CellValue, CellPosition } from '../../../../spreadsheet-data/models/cell.interfaces'
import { getCellPositionByIndex, getColumnLabel } from '../../../../spreadsheet-data/helpers/cell.helpers'
@Component({
selector: 'spreadsheet-ui',
templateUrl: './spreadsheet-ui.component.html',
styleUrls: ['./spreadsheet-ui.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpreadsheetUiComponent {
// Form
@ViewChild('active_cell_input') activeCellInput: ElementRef;
// Grid
@Input() rows: number
@Input() columns: number
// Active
@Input() activeCellIndex: number
@Input() activeCellPosition: CellPosition
@Input() activeCellValue: CellValue
@Input() activeCellFormGroup: FormGroup
//
@Output() cellClicked: EventEmitter<number> = new EventEmitter<number>()
@Output() addRowClicked: EventEmitter<void> = new EventEmitter<void>()
@Output() addColumnClicked: EventEmitter<void> = new EventEmitter<void>()
/**
* @description pre-sorted data so on render DOM, the lookup calls are O(1), instead of O(n^2)
*/
@Input() cells: Cell[]
/**
* @description Helper method to generate an array from a number/string representing the length of the array
* @param number
*/
counter(number: number|string): any[] {
if (typeof number === 'number') {
return Array(number)
}
return Array(parseInt(number))
}
/**
*
* @param index
*/
cellClick(index: number) {
this.cellClicked.emit(index)
setTimeout(() => this.activeCellInput.nativeElement.focus(), 0)
}
/**
* @description Convert Index number to Column Label ie A, AA, CT, etc
* @param index
*/
getColumnLabel = getColumnLabel
/**
* @description Gets the value of the cell based on the cell index (it wraps like regular text, in the grid)
* @param index
*/
getCellValue(index: number): CellValue {
if (this.cells.length === 0) {
return ''
}
const cell = this.cells.filter(cell => {
const indexCellPosition = getCellPositionByIndex(this.rows, this.columns, index)
return (cell.position.column === indexCellPosition.column &&
cell.position.row === indexCellPosition.row)
})
return cell[0] && cell[0].value ? cell[0].value : ''
}
/**
*
* @param rows
* @param columns
* @param cellIndex
*/
isCellActive(cellIndex: number): boolean {
const cellPosition = getCellPositionByIndex(this.rows, this.columns, cellIndex)
return (cellPosition.column === this.activeCellPosition.column &&
cellPosition.row === this.activeCellPosition.row)
}
}
<section class="container"
gdAreas="nav | spreadsheet"
gdRows="2rem calc(100vh - 64px - 2rem)">
<section gdArea="nav"
gdAreas="add_row label input add_column"
gdColumns="2rem 2rem auto 3rem">
<div gdArea="add_row">
<button (click)="addRowClicked.emit()" class="tool" title="Add Row">
<svg viewBox="0 0 360.61 431.32" class="rotate-clockwise-180">
<g id="g3484">
<path id="path3486" style="fill: #000000;" d="m0.213 180.2l180.2-180.2 180.2 180.2v118.85l-137.07-137.06v269.33h-87.22v-268.38l-136.32 136.32 0.213-119.06z"/>
</g>
</svg>
</button>
</div>
<div gdArea="label">
{{ getColumnLabel(activeCellPosition.column) }}{{ activeCellPosition.row }}:
</div>
<div gdArea="input">
<form [formGroup]="activeCellFormGroup">
<input type="text" formControlName="active_cell" #active_cell_input autofocus id="active-cell-editor" />
<label for="active-cell-editor" class="hide-element">Edit the value of the spreadsheet's active square</label>
</form>
</div>
<div gdArea="add_column">
<button (click)="addColumnClicked.emit()" class="tool" title="Add Column">
<svg viewBox="0 0 360.61 431.32" class="rotate-clockwise-90">
<g id="g3484">
<path id="path3486" style="fill: #000000;" d="m0.213 180.2l180.2-180.2 180.2 180.2v118.85l-137.07-137.06v269.33h-87.22v-268.38l-136.32 136.32 0.213-119.06z"/>
</g>
</svg>
</button>
</div>
</section>
<section
gdArea="spreadsheet"
gdAreas="filler column_labels | row_labels sheet"
gdRows="1.5rem auto"
gdColumns="2.75rem auto">
<div gdArea="filler"></div>
<section
gdArea="column_labels"
gdColumns="repeat({{columns}}, 6rem)">
<div *ngFor="let column of counter(columns); let i = index">
{{ getColumnLabel(i + 1) }}
</div>
</section>
<section
gdArea="row_labels"
gdRows="repeat({{rows}}, 1.5rem)">
<div *ngFor="let row of counter(rows); let i = index">
{{ i + 1 }}
</div>
</section>
<section
gdArea="sheet"
gdRows="repeat({{rows}}, 1.5rem)"
gdColumns="repeat({{columns}}, 6rem)">
<div
*ngFor="let cell of counter(rows * columns); let i = index"
[class.active]="isCellActive(i)"
(click)="cellClick(i)">
{{ getCellValue(i) | expression | async | conditionalNumber }}
</div>
</section>
</section>
</section>
@import './../../../../../../styles/helpers/transforms';
@import './../../../../../../styles/accessibility';
$blue: rgb(26, 115, 232);
$light_blue: rgba(26, 115, 232, .2);
$gray: #F8F9FA;
$light_gray: #E2E3E3;
$spreadsheet_label_font: tahoma;
html {
font-family: 'Courier New', Courier, monospace;
}
section, div, form, input {
box-sizing: border-box;
}
.container {
height: calc(100vh - 64px);
width: 100vw;
}
[gdarea="nav"] {
form {
height: 100%;
input {
width: 100%;
height: 100%;
padding: .25rem;
border: 0;
background: none;
outline: 0;
padding-left: .5rem;
}
}
[gdarea="add_row"], [gdarea="add_column"] {
display: grid;
justify-items: center;
align-items: center;
}
.tool {
background: none;
border: none !important;
&:hover {
cursor: pointer;
}
}
[gdarea="add_row"], [gdarea="add_column"] {
button {
outline: 0;
svg {
width: .8rem;
&.special {
// todo remove this with proper container that aligns children
position: relative;
left: 6px;
top: 1px;
}
}
}
}
[gdarea="add_row"] button {
justify-self: flex-start;
margin-left: .5rem;
}
[gdarea="add_column"] button {
justify-self: flex-end;
margin-right: .5rem;
}
[gdarea="label"] {
width: 100%;
display: inline-grid;
justify-items: end;
align-items: center;
font-size: 11px;
text-transform: uppercase;
font-weight: 600;
font-family: $spreadsheet_label_font;
}
}
[gdarea="column_labels"], [gdarea="row_labels"] {
text-align: center;
text-transform: uppercase;
}
[gdarea="spreadsheet"] {
overflow: auto;
height: calc(100vh - 64px - 2rem);
div {
display: inline-grid;
justify-items: center;
align-items: center;
border-right: 1px solid $light_gray;
border-bottom: 1px solid $light_gray;
}
[gdarea="sheet"] {
div {
width: 6rem;
height: 1.5rem;
font-family: $spreadsheet_label_font;
font-size: .8rem;
&.active {
border: 2px solid $blue;
}
&:hover {
cursor: cell;
background-color: $light_blue;
}
}
}
[gdarea="row_labels"], [gdarea="column_labels"] {
div {
background-color: $gray;
font-family: $spreadsheet_label_font;
font-size: .7rem;
height: 1.5rem;
}
}
[gdarea="row_labels"] {
div {
&:first-child {
border-top: 1px solid $light_gray;
}
width: 2.75rem;
font-size: .65rem;
}
}
[gdarea="column_labels"] {
div {
&:first-child {
border-left: 1px solid $light_gray;
}
border-top: 1px solid $light_gray;
width: 6rem;
}
}
}
Legend
Html element with directive