mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 05:44:24 +00:00
Probably finish up
This commit is contained in:
@@ -58,13 +58,24 @@ body>main {
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
h4 {
|
||||
margin-bottom: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-scroll-wrapper {
|
||||
width: 100%;
|
||||
max-height: 600px;
|
||||
max-height: 70vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
@@ -87,6 +98,7 @@ body>main {
|
||||
|
||||
/* Because we must set sticky on th, we have to apply background styles here rather than on thead */
|
||||
& th {
|
||||
cursor: pointer;
|
||||
border-left: 1px dotted rgba(200, 209, 224, 0.6);
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
@@ -112,12 +124,12 @@ body>main {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&.active.asc::after {
|
||||
&.active.sorting.asc::after {
|
||||
font-family: FontAwesome;
|
||||
content: "\f0d8";
|
||||
}
|
||||
|
||||
&.active.desc::after {
|
||||
&.active.sorting.desc::after {
|
||||
font-family: FontAwesome;
|
||||
content: "\f0d7";
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<header>
|
||||
<h2>Data infos</h2>
|
||||
</header>
|
||||
<div id="data-info">
|
||||
<div id="data-info" class="info">
|
||||
<h4>Filename</h4>
|
||||
<p id="data-filename"></p>
|
||||
<h4>File type</h4>
|
||||
@@ -57,7 +57,7 @@
|
||||
<header>
|
||||
<h2>Selected column infos</h2>
|
||||
</header>
|
||||
<div id="column-info">
|
||||
<div id="column-info" class="info">
|
||||
<h4>Selected column</h4>
|
||||
<p id="column-selected"></p>
|
||||
<h4>Data type</h4>
|
||||
|
||||
@@ -57,7 +57,7 @@ const columnEntries = ref<number>( [ columnEntriesElement ], 0 );
|
||||
const columnMax = ref<number>( [ columnMaxElement ], 0 );
|
||||
const columnMin = ref<number>( [ columnMinElement ], 0 );
|
||||
const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement;
|
||||
const ascendingSort = ref<boolean>( [], true );
|
||||
const sorting = ref<string>( [], '' );
|
||||
|
||||
let selectedColumn = '';
|
||||
|
||||
@@ -91,7 +91,7 @@ fileInput.addEventListener( 'change', event => {
|
||||
|
||||
filename.set( file.name );
|
||||
filetype.set( file.type );
|
||||
filesize.set( String( file.size ) + 'B' ); // TODO: KB / MB conversion stuff?
|
||||
filesize.set( String( file.size ) + 'B' );
|
||||
readCSV( event )
|
||||
.then( data => {
|
||||
// Row count
|
||||
@@ -111,20 +111,35 @@ fileInput.addEventListener( 'change', event => {
|
||||
columnName.addConditionalClasses(
|
||||
el,
|
||||
val => val === header[ i ],
|
||||
'column-selected',
|
||||
''
|
||||
[ 'active' ],
|
||||
[]
|
||||
);
|
||||
sorting.addConditionalClasses(
|
||||
el, val => {
|
||||
return val === 'ascending' && selectedColumn === header[ i ];
|
||||
}, [ 'asc' ], [ 'desc' ]
|
||||
);
|
||||
sorting.addConditionalClasses(
|
||||
el, val => {
|
||||
return val !== '' && selectedColumn === header[ i ];
|
||||
}, [ 'sorting' ], []
|
||||
);
|
||||
|
||||
el.addEventListener( 'click', () => {
|
||||
// TODO: Decide on sorting cycling
|
||||
// TODO: Add indicator as well
|
||||
// TODO: Want to hide infos and do an else static info for file infos and selected columns info?
|
||||
if ( selectedColumn === column ) {
|
||||
ascendingSort.set( !ascendingSort.get() );
|
||||
const sort = sorting.get();
|
||||
|
||||
if ( sort === 'ascending' ) {
|
||||
sorting.set( 'descending' );
|
||||
} else if ( sort === 'descending' ) {
|
||||
sorting.set( '' );
|
||||
} else {
|
||||
sorting.set( 'ascending' );
|
||||
}
|
||||
} else {
|
||||
// This column will now be the active column
|
||||
selectedColumn = column;
|
||||
ascendingSort.set( true );
|
||||
sorting.set( 'ascending' );
|
||||
const dtype = typeof dataList.get()[0]![ column ];
|
||||
|
||||
columnDatatype.set( dtype );
|
||||
@@ -160,10 +175,10 @@ fileInput.addEventListener( 'change', event => {
|
||||
if ( config ) {
|
||||
const dtype = typeof dataList.get()[0]![ config.sorted ];
|
||||
|
||||
columnName.set( config.sorted );
|
||||
selectedColumn = config.active;
|
||||
ascendingSort.set( config.ascending );
|
||||
columnDatatype.set( dtype );
|
||||
columnName.set( config.sorted );
|
||||
sorting.set( config.sorting );
|
||||
|
||||
if ( dtype === 'string' )
|
||||
filterInput.disabled = false;
|
||||
@@ -183,49 +198,48 @@ fileInput.addEventListener( 'change', event => {
|
||||
|
||||
|
||||
// TODO: Maybe add an overlay that is shown during load?
|
||||
//
|
||||
|
||||
// ┌ ┐
|
||||
// │ Sorting │
|
||||
// └ ┘
|
||||
const doSort = () => {
|
||||
filter.set( '' );
|
||||
persistance.store(
|
||||
filename.get(), filesize.get(), selectedColumn, selectedColumn, ascendingSort.get()
|
||||
filename.get(), filesize.get(), selectedColumn, selectedColumn, sorting.get()
|
||||
);
|
||||
let sorter = ascendingStringSort;
|
||||
|
||||
if ( columnDatatype.get() === 'string' ) {
|
||||
columnEntries.set( computeDifferent( dataList.get(), selectedColumn ) );
|
||||
|
||||
if ( ascendingSort.get() ) {
|
||||
dataList.sort( ( a, b ) => {
|
||||
return ( a[ selectedColumn ] as string ).localeCompare( b[ selectedColumn ] as string );
|
||||
} );
|
||||
} else {
|
||||
dataList.sort( ( a, b ) => {
|
||||
return ( b[ selectedColumn ] as string ).localeCompare( a[ selectedColumn ] as string );
|
||||
} );
|
||||
}
|
||||
} else if ( columnDatatype.get() === 'number' ) {
|
||||
const stats = computeMinMax( dataList.get(), selectedColumn );
|
||||
|
||||
columnMin.set( stats[ 0 ] );
|
||||
columnMax.set( stats[ 1 ] );
|
||||
columnEntries.set( stats[ 2 ] );
|
||||
sorter = ascendingNumberSort;
|
||||
}
|
||||
|
||||
if ( ascendingSort.get() ) {
|
||||
dataList.sort( ( a, b ) => {
|
||||
return ( a[ selectedColumn ] as number ) - ( b[ selectedColumn ] as number );
|
||||
} );
|
||||
} else {
|
||||
dataList.sort( ( a, b ) => {
|
||||
return ( b[ selectedColumn ] as number ) - ( a[ selectedColumn ] as number );
|
||||
} );
|
||||
}
|
||||
if ( sorting.get() === 'ascending' ) {
|
||||
dataList.sort( ( a, b ) => sorter( a, b ) );
|
||||
} else if ( sorting.get() === 'descending' ) {
|
||||
dataList.sort( ( a, b ) => sorter( b, a ) );
|
||||
} else {
|
||||
dataList.resetSort();
|
||||
}
|
||||
};
|
||||
|
||||
const ascendingStringSort = ( a: CSVRecord, b: CSVRecord ) => {
|
||||
return ( a[ selectedColumn ] as string ).localeCompare( b[ selectedColumn ] as string );
|
||||
};
|
||||
|
||||
const ascendingNumberSort = ( a: CSVRecord, b: CSVRecord ) => {
|
||||
return ( a[ selectedColumn ] as number ) - ( b[ selectedColumn ] as number );
|
||||
};
|
||||
|
||||
|
||||
columnName.onChange( doSort );
|
||||
ascendingSort.onChange( doSort );
|
||||
sorting.onChange( doSort );
|
||||
|
||||
|
||||
|
||||
@@ -237,9 +251,6 @@ filter.bind( filterInput, val => val );
|
||||
|
||||
// Add listener to change of filter value.
|
||||
filter.onChange( () => {
|
||||
// TODO: Task says need to fire custom event on filter card... sure, why not.
|
||||
// It doesn't say that we need to use it though!
|
||||
// SO: Do you think this is good enough?
|
||||
document.dispatchEvent( new CustomEvent( 'explorer:filter', {
|
||||
'detail': 'Filtering has changed',
|
||||
'cancelable': false
|
||||
|
||||
@@ -11,19 +11,19 @@ const persistanceStore: PersistanceConfig = JSON.parse( localStorage.getItem( 'p
|
||||
* @param size - The filesize (in case file is changed)
|
||||
* @param sorted - The sorted column
|
||||
* @param active - The active column
|
||||
* @param ascending - True if sorting ascending
|
||||
* @param sorting - True if sorting ascending
|
||||
*/
|
||||
const store = (
|
||||
filename: string,
|
||||
size: string,
|
||||
sorted: string,
|
||||
active: string,
|
||||
ascending: boolean
|
||||
sorting: string
|
||||
) => {
|
||||
persistanceStore[ `${ filename }-${ size }` ] = {
|
||||
'active': active,
|
||||
'sorted': sorted,
|
||||
'ascending': ascending
|
||||
'sorting': sorting
|
||||
};
|
||||
localStorage.setItem( 'persistance', JSON.stringify( persistanceStore ) );
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ const renderer = <T extends StringIndexedObject>( data: T, template: RenderTempl
|
||||
const parent = document.createElement( template.type );
|
||||
|
||||
for ( let i = 0; i < template.cssClasses.length; i++ ) {
|
||||
console.log( 'Adding css class', template.cssClasses[i]! );
|
||||
parent.classList.add( template.cssClasses[i]! );
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const listRef = <T>( parent: HTMLElement, data: T[], name: string, templa
|
||||
let list: T[] = data; // contains all values passed in
|
||||
|
||||
const nodes: HTMLElement[] = [];
|
||||
const rendered: boolean[] = []; // Mask for
|
||||
const rendered: boolean[] = []; // Mask for rendering
|
||||
const onChangeFunctions: ( () => Promise<void> )[] = [];
|
||||
|
||||
/**
|
||||
@@ -75,6 +75,21 @@ export const listRef = <T>( parent: HTMLElement, data: T[], name: string, templa
|
||||
} );
|
||||
};
|
||||
|
||||
/** Reset the sorting */
|
||||
const resetSort = (): void => {
|
||||
const children = [ ...parent.children ];
|
||||
|
||||
children.sort( ( elA, elB ) => {
|
||||
const a = parseInt( elA.id.split( '--' )[1]! );
|
||||
const b = parseInt( elB.id.split( '--' )[1]! );
|
||||
|
||||
return a - b;
|
||||
} );
|
||||
children.forEach( el => {
|
||||
parent.appendChild( el );
|
||||
} );
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Filter elements. More performant than doing it with set operation, as it is cheaper to reverse.
|
||||
@@ -118,6 +133,7 @@ export const listRef = <T>( parent: HTMLElement, data: T[], name: string, templa
|
||||
get,
|
||||
set,
|
||||
sort,
|
||||
resetSort,
|
||||
filter,
|
||||
setTemplate,
|
||||
onChange
|
||||
|
||||
@@ -9,8 +9,8 @@ interface ConditionalElement<T> {
|
||||
|
||||
interface ConditionalClass<T> {
|
||||
'element': HTMLElement,
|
||||
'onTrue': string,
|
||||
'onFalse': string,
|
||||
'onTrue': string[],
|
||||
'onFalse': string[],
|
||||
'predicate': ( value: T ) => boolean;
|
||||
}
|
||||
|
||||
@@ -50,31 +50,37 @@ export const ref = <T>( elements: HTMLElement[], data: T ): Ref<T> => {
|
||||
* @param data - The new value of the
|
||||
*/
|
||||
const set = ( data: T ): void => {
|
||||
value = data;
|
||||
if ( value !== data ) {
|
||||
value = data;
|
||||
// Update normal ref elements
|
||||
elements.forEach( el => {
|
||||
el.textContent = String( data );
|
||||
} );
|
||||
|
||||
// Update normal ref elements
|
||||
elements.forEach( el => {
|
||||
el.textContent = String( data );
|
||||
} );
|
||||
|
||||
// Update conditional elements
|
||||
conditionalElements.forEach( el => {
|
||||
// Update conditional elements
|
||||
conditionalElements.forEach( el => {
|
||||
// convert to boolean (explicitly)
|
||||
el.element.hidden = !el.predicate( data );
|
||||
} );
|
||||
el.element.hidden = !el.predicate( data );
|
||||
} );
|
||||
|
||||
conditionalClasses.forEach( el => {
|
||||
// FIXME: Use add and remove!
|
||||
el.element.classList.value = el.predicate( data ) ? el.onTrue : el.onFalse;
|
||||
} );
|
||||
conditionalClasses.forEach( el => {
|
||||
if ( el.predicate( data ) ) {
|
||||
el.element.classList.remove( ...el.onFalse );
|
||||
el.element.classList.add( ...el.onTrue );
|
||||
} else {
|
||||
el.element.classList.remove( ...el.onTrue );
|
||||
el.element.classList.add( ...el.onFalse );
|
||||
}
|
||||
} );
|
||||
|
||||
// Update boundElements
|
||||
boundElements.forEach( el => {
|
||||
el.value = String( value );
|
||||
} );
|
||||
// Update boundElements
|
||||
boundElements.forEach( el => {
|
||||
el.value = String( value );
|
||||
} );
|
||||
|
||||
for ( let i = 0; i < onChangeFunctions.length; i++ ) {
|
||||
onChangeFunctions[ i ]!();
|
||||
for ( let i = 0; i < onChangeFunctions.length; i++ ) {
|
||||
onChangeFunctions[ i ]!();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,8 +126,8 @@ export const ref = <T>( elements: HTMLElement[], data: T ): Ref<T> => {
|
||||
const addConditionalClasses = (
|
||||
element: HTMLElement,
|
||||
predicate: ( value: T ) => boolean,
|
||||
onTrue: string,
|
||||
onFalse: string
|
||||
onTrue: string[],
|
||||
onFalse: string[]
|
||||
) => {
|
||||
conditionalClasses.push( {
|
||||
'element': element,
|
||||
|
||||
3
task_2_ts/ts/rendering/rendering.d.ts
vendored
3
task_2_ts/ts/rendering/rendering.d.ts
vendored
@@ -4,7 +4,7 @@ export interface Ref<T> {
|
||||
'addAdditionalElement': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void;
|
||||
'addConditionalElementBind': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void;
|
||||
'addConditionalClasses': (
|
||||
element: HTMLElement, predicate: ( value: T ) => boolean, onTrue: string, onFalse: string ) => void;
|
||||
element: HTMLElement, predicate: ( value: T ) => boolean, onTrue: string[], onFalse: string[] ) => void;
|
||||
'resetConditionalClasses': () => void;
|
||||
'resetConditionalElementBinds': () => void;
|
||||
'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void;
|
||||
@@ -15,6 +15,7 @@ export interface ListRef<T> {
|
||||
'set': ( data: T[] ) => void;
|
||||
'get': () => T[];
|
||||
'sort': ( compare: ( a: T, b: T ) => number ) => void;
|
||||
'resetSort': () => void;
|
||||
'filter': ( predicate: ( value: T ) => boolean ) => void;
|
||||
'setTemplate': ( newTemplate: RenderTemplate ) => void;
|
||||
'onChange': ( callback: () => void ) => void;
|
||||
|
||||
2
task_2_ts/ts/types.d.ts
vendored
2
task_2_ts/ts/types.d.ts
vendored
@@ -6,7 +6,7 @@ export type CSV_Data = CSVRecord[];
|
||||
export interface PersistanceConfigEntry {
|
||||
'sorted': string;
|
||||
'active': string;
|
||||
'ascending': boolean;
|
||||
'sorting': string;
|
||||
}
|
||||
|
||||
export interface PersistanceConfig {
|
||||
|
||||
Reference in New Issue
Block a user