From 166e9896b59e137a5980f8e91a1b1a043d096e60 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Wed, 22 Oct 2025 10:43:56 +0200 Subject: [PATCH] Probably finish up --- task_2_ts/css/layout.css | 18 +++++- task_2_ts/index.html | 4 +- task_2_ts/ts/main.ts | 83 ++++++++++++++----------- task_2_ts/ts/persistance.ts | 6 +- task_2_ts/ts/rendering/list-renderer.ts | 1 - task_2_ts/ts/rendering/list.ts | 18 +++++- task_2_ts/ts/rendering/primitives.ts | 54 +++++++++------- task_2_ts/ts/rendering/rendering.d.ts | 3 +- task_2_ts/ts/types.d.ts | 2 +- 9 files changed, 117 insertions(+), 72 deletions(-) diff --git a/task_2_ts/css/layout.css b/task_2_ts/css/layout.css index c4ba28e..8d40809 100644 --- a/task_2_ts/css/layout.css +++ b/task_2_ts/css/layout.css @@ -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"; } diff --git a/task_2_ts/index.html b/task_2_ts/index.html index 0477444..bf4b91c 100644 --- a/task_2_ts/index.html +++ b/task_2_ts/index.html @@ -38,7 +38,7 @@

Data infos

-
+

Filename

File type

@@ -57,7 +57,7 @@

Selected column infos

-
+

Selected column

Data type

diff --git a/task_2_ts/ts/main.ts b/task_2_ts/ts/main.ts index 38d2b43..7370b55 100644 --- a/task_2_ts/ts/main.ts +++ b/task_2_ts/ts/main.ts @@ -57,7 +57,7 @@ const columnEntries = ref( [ columnEntriesElement ], 0 ); const columnMax = ref( [ columnMaxElement ], 0 ); const columnMin = ref( [ columnMinElement ], 0 ); const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement; -const ascendingSort = ref( [], true ); +const sorting = ref( [], '' ); 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 diff --git a/task_2_ts/ts/persistance.ts b/task_2_ts/ts/persistance.ts index fa8efb2..ecffd53 100644 --- a/task_2_ts/ts/persistance.ts +++ b/task_2_ts/ts/persistance.ts @@ -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 ) ); }; diff --git a/task_2_ts/ts/rendering/list-renderer.ts b/task_2_ts/ts/rendering/list-renderer.ts index 620b572..2e2fdde 100644 --- a/task_2_ts/ts/rendering/list-renderer.ts +++ b/task_2_ts/ts/rendering/list-renderer.ts @@ -19,7 +19,6 @@ const renderer = ( 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]! ); } diff --git a/task_2_ts/ts/rendering/list.ts b/task_2_ts/ts/rendering/list.ts index 2dc4752..30fa09e 100644 --- a/task_2_ts/ts/rendering/list.ts +++ b/task_2_ts/ts/rendering/list.ts @@ -9,7 +9,7 @@ export const listRef = ( 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 )[] = []; /** @@ -75,6 +75,21 @@ export const listRef = ( 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 = ( parent: HTMLElement, data: T[], name: string, templa get, set, sort, + resetSort, filter, setTemplate, onChange diff --git a/task_2_ts/ts/rendering/primitives.ts b/task_2_ts/ts/rendering/primitives.ts index 0f16842..308b124 100644 --- a/task_2_ts/ts/rendering/primitives.ts +++ b/task_2_ts/ts/rendering/primitives.ts @@ -9,8 +9,8 @@ interface ConditionalElement { interface ConditionalClass { 'element': HTMLElement, - 'onTrue': string, - 'onFalse': string, + 'onTrue': string[], + 'onFalse': string[], 'predicate': ( value: T ) => boolean; } @@ -50,31 +50,37 @@ export const ref = ( elements: HTMLElement[], data: T ): Ref => { * @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 = ( elements: HTMLElement[], data: T ): Ref => { const addConditionalClasses = ( element: HTMLElement, predicate: ( value: T ) => boolean, - onTrue: string, - onFalse: string + onTrue: string[], + onFalse: string[] ) => { conditionalClasses.push( { 'element': element, diff --git a/task_2_ts/ts/rendering/rendering.d.ts b/task_2_ts/ts/rendering/rendering.d.ts index 0ad87ee..a4426e7 100644 --- a/task_2_ts/ts/rendering/rendering.d.ts +++ b/task_2_ts/ts/rendering/rendering.d.ts @@ -4,7 +4,7 @@ export interface Ref { '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 { '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; diff --git a/task_2_ts/ts/types.d.ts b/task_2_ts/ts/types.d.ts index cfb0c38..a7c9d87 100644 --- a/task_2_ts/ts/types.d.ts +++ b/task_2_ts/ts/types.d.ts @@ -6,7 +6,7 @@ export type CSV_Data = CSVRecord[]; export interface PersistanceConfigEntry { 'sorted': string; 'active': string; - 'ascending': boolean; + 'sorting': string; } export interface PersistanceConfig {