Probably finish up

This commit is contained in:
2025-10-22 10:43:56 +02:00
parent fde38a8869
commit 166e9896b5
9 changed files with 117 additions and 72 deletions

View File

@@ -58,13 +58,24 @@ body>main {
} }
} }
.info {
h4 {
margin-bottom: 2px;
margin-top: 10px;
}
p {
margin: 0;
}
}
.table-container { .table-container {
width: 100%; width: 100%;
} }
.table-scroll-wrapper { .table-scroll-wrapper {
width: 100%; width: 100%;
max-height: 600px; max-height: 70vh;
overflow: scroll; 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 */ /* Because we must set sticky on th, we have to apply background styles here rather than on thead */
& th { & th {
cursor: pointer;
border-left: 1px dotted rgba(200, 209, 224, 0.6); border-left: 1px dotted rgba(200, 209, 224, 0.6);
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
@@ -112,12 +124,12 @@ body>main {
color: #999; color: #999;
} }
&.active.asc::after { &.active.sorting.asc::after {
font-family: FontAwesome; font-family: FontAwesome;
content: "\f0d8"; content: "\f0d8";
} }
&.active.desc::after { &.active.sorting.desc::after {
font-family: FontAwesome; font-family: FontAwesome;
content: "\f0d7"; content: "\f0d7";
} }

View File

@@ -38,7 +38,7 @@
<header> <header>
<h2>Data infos</h2> <h2>Data infos</h2>
</header> </header>
<div id="data-info"> <div id="data-info" class="info">
<h4>Filename</h4> <h4>Filename</h4>
<p id="data-filename"></p> <p id="data-filename"></p>
<h4>File type</h4> <h4>File type</h4>
@@ -57,7 +57,7 @@
<header> <header>
<h2>Selected column infos</h2> <h2>Selected column infos</h2>
</header> </header>
<div id="column-info"> <div id="column-info" class="info">
<h4>Selected column</h4> <h4>Selected column</h4>
<p id="column-selected"></p> <p id="column-selected"></p>
<h4>Data type</h4> <h4>Data type</h4>

View File

@@ -57,7 +57,7 @@ const columnEntries = ref<number>( [ columnEntriesElement ], 0 );
const columnMax = ref<number>( [ columnMaxElement ], 0 ); const columnMax = ref<number>( [ columnMaxElement ], 0 );
const columnMin = ref<number>( [ columnMinElement ], 0 ); const columnMin = ref<number>( [ columnMinElement ], 0 );
const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement; const fileInput = document.getElementById( 'file-input' )! as HTMLInputElement;
const ascendingSort = ref<boolean>( [], true ); const sorting = ref<string>( [], '' );
let selectedColumn = ''; let selectedColumn = '';
@@ -91,7 +91,7 @@ fileInput.addEventListener( 'change', event => {
filename.set( file.name ); filename.set( file.name );
filetype.set( file.type ); filetype.set( file.type );
filesize.set( String( file.size ) + 'B' ); // TODO: KB / MB conversion stuff? filesize.set( String( file.size ) + 'B' );
readCSV( event ) readCSV( event )
.then( data => { .then( data => {
// Row count // Row count
@@ -111,20 +111,35 @@ fileInput.addEventListener( 'change', event => {
columnName.addConditionalClasses( columnName.addConditionalClasses(
el, el,
val => val === header[ i ], 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', () => { 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 ) { 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 { } else {
// This column will now be the active column // This column will now be the active column
selectedColumn = column; selectedColumn = column;
ascendingSort.set( true ); sorting.set( 'ascending' );
const dtype = typeof dataList.get()[0]![ column ]; const dtype = typeof dataList.get()[0]![ column ];
columnDatatype.set( dtype ); columnDatatype.set( dtype );
@@ -160,10 +175,10 @@ fileInput.addEventListener( 'change', event => {
if ( config ) { if ( config ) {
const dtype = typeof dataList.get()[0]![ config.sorted ]; const dtype = typeof dataList.get()[0]![ config.sorted ];
columnName.set( config.sorted );
selectedColumn = config.active; selectedColumn = config.active;
ascendingSort.set( config.ascending );
columnDatatype.set( dtype ); columnDatatype.set( dtype );
columnName.set( config.sorted );
sorting.set( config.sorting );
if ( dtype === 'string' ) if ( dtype === 'string' )
filterInput.disabled = false; filterInput.disabled = false;
@@ -183,49 +198,48 @@ fileInput.addEventListener( 'change', event => {
// TODO: Maybe add an overlay that is shown during load? // TODO: Maybe add an overlay that is shown during load?
//
// ┌ ┐ // ┌ ┐
// │ Sorting │ // │ Sorting │
// └ ┘ // └ ┘
const doSort = () => { const doSort = () => {
filter.set( '' ); filter.set( '' );
persistance.store( 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' ) { if ( columnDatatype.get() === 'string' ) {
columnEntries.set( computeDifferent( dataList.get(), selectedColumn ) ); 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' ) { } else if ( columnDatatype.get() === 'number' ) {
const stats = computeMinMax( dataList.get(), selectedColumn ); const stats = computeMinMax( dataList.get(), selectedColumn );
columnMin.set( stats[ 0 ] ); columnMin.set( stats[ 0 ] );
columnMax.set( stats[ 1 ] ); columnMax.set( stats[ 1 ] );
columnEntries.set( stats[ 2 ] ); columnEntries.set( stats[ 2 ] );
sorter = ascendingNumberSort;
}
if ( ascendingSort.get() ) { if ( sorting.get() === 'ascending' ) {
dataList.sort( ( a, b ) => { dataList.sort( ( a, b ) => sorter( a, b ) );
return ( a[ selectedColumn ] as number ) - ( b[ selectedColumn ] as number ); } else if ( sorting.get() === 'descending' ) {
} ); dataList.sort( ( a, b ) => sorter( b, a ) );
} else { } else {
dataList.sort( ( a, b ) => { dataList.resetSort();
return ( b[ selectedColumn ] as number ) - ( a[ selectedColumn ] as number );
} );
}
} }
}; };
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 ); 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. // Add listener to change of filter value.
filter.onChange( () => { 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', { document.dispatchEvent( new CustomEvent( 'explorer:filter', {
'detail': 'Filtering has changed', 'detail': 'Filtering has changed',
'cancelable': false 'cancelable': false

View File

@@ -11,19 +11,19 @@ const persistanceStore: PersistanceConfig = JSON.parse( localStorage.getItem( 'p
* @param size - The filesize (in case file is changed) * @param size - The filesize (in case file is changed)
* @param sorted - The sorted column * @param sorted - The sorted column
* @param active - The active column * @param active - The active column
* @param ascending - True if sorting ascending * @param sorting - True if sorting ascending
*/ */
const store = ( const store = (
filename: string, filename: string,
size: string, size: string,
sorted: string, sorted: string,
active: string, active: string,
ascending: boolean sorting: string
) => { ) => {
persistanceStore[ `${ filename }-${ size }` ] = { persistanceStore[ `${ filename }-${ size }` ] = {
'active': active, 'active': active,
'sorted': sorted, 'sorted': sorted,
'ascending': ascending 'sorting': sorting
}; };
localStorage.setItem( 'persistance', JSON.stringify( persistanceStore ) ); localStorage.setItem( 'persistance', JSON.stringify( persistanceStore ) );
}; };

View File

@@ -19,7 +19,6 @@ const renderer = <T extends StringIndexedObject>( data: T, template: RenderTempl
const parent = document.createElement( template.type ); const parent = document.createElement( template.type );
for ( let i = 0; i < template.cssClasses.length; i++ ) { for ( let i = 0; i < template.cssClasses.length; i++ ) {
console.log( 'Adding css class', template.cssClasses[i]! );
parent.classList.add( template.cssClasses[i]! ); parent.classList.add( template.cssClasses[i]! );
} }

View File

@@ -9,7 +9,7 @@ export const listRef = <T>( parent: HTMLElement, data: T[], name: string, templa
let list: T[] = data; // contains all values passed in let list: T[] = data; // contains all values passed in
const nodes: HTMLElement[] = []; const nodes: HTMLElement[] = [];
const rendered: boolean[] = []; // Mask for const rendered: boolean[] = []; // Mask for rendering
const onChangeFunctions: ( () => Promise<void> )[] = []; 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. * 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, get,
set, set,
sort, sort,
resetSort,
filter, filter,
setTemplate, setTemplate,
onChange onChange

View File

@@ -9,8 +9,8 @@ interface ConditionalElement<T> {
interface ConditionalClass<T> { interface ConditionalClass<T> {
'element': HTMLElement, 'element': HTMLElement,
'onTrue': string, 'onTrue': string[],
'onFalse': string, 'onFalse': string[],
'predicate': ( value: T ) => boolean; '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 * @param data - The new value of the
*/ */
const set = ( data: T ): void => { 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 // Update conditional elements
elements.forEach( el => { conditionalElements.forEach( el => {
el.textContent = String( data );
} );
// Update conditional elements
conditionalElements.forEach( el => {
// convert to boolean (explicitly) // convert to boolean (explicitly)
el.element.hidden = !el.predicate( data ); el.element.hidden = !el.predicate( data );
} ); } );
conditionalClasses.forEach( el => { conditionalClasses.forEach( el => {
// FIXME: Use add and remove! if ( el.predicate( data ) ) {
el.element.classList.value = el.predicate( data ) ? el.onTrue : el.onFalse; 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 // Update boundElements
boundElements.forEach( el => { boundElements.forEach( el => {
el.value = String( value ); el.value = String( value );
} ); } );
for ( let i = 0; i < onChangeFunctions.length; i++ ) { for ( let i = 0; i < onChangeFunctions.length; i++ ) {
onChangeFunctions[ i ]!(); onChangeFunctions[ i ]!();
}
} }
}; };
@@ -120,8 +126,8 @@ export const ref = <T>( elements: HTMLElement[], data: T ): Ref<T> => {
const addConditionalClasses = ( const addConditionalClasses = (
element: HTMLElement, element: HTMLElement,
predicate: ( value: T ) => boolean, predicate: ( value: T ) => boolean,
onTrue: string, onTrue: string[],
onFalse: string onFalse: string[]
) => { ) => {
conditionalClasses.push( { conditionalClasses.push( {
'element': element, 'element': element,

View File

@@ -4,7 +4,7 @@ export interface Ref<T> {
'addAdditionalElement': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void; 'addAdditionalElement': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void;
'addConditionalElementBind': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void; 'addConditionalElementBind': ( elements: HTMLElement, predicate: ( value: T ) => boolean ) => void;
'addConditionalClasses': ( '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; 'resetConditionalClasses': () => void;
'resetConditionalElementBinds': () => void; 'resetConditionalElementBinds': () => void;
'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void; 'bind': ( element: HTMLInputElement, castFunction: ( val: string ) => T ) => void;
@@ -15,6 +15,7 @@ export interface ListRef<T> {
'set': ( data: T[] ) => void; 'set': ( data: T[] ) => void;
'get': () => T[]; 'get': () => T[];
'sort': ( compare: ( a: T, b: T ) => number ) => void; 'sort': ( compare: ( a: T, b: T ) => number ) => void;
'resetSort': () => void;
'filter': ( predicate: ( value: T ) => boolean ) => void; 'filter': ( predicate: ( value: T ) => boolean ) => void;
'setTemplate': ( newTemplate: RenderTemplate ) => void; 'setTemplate': ( newTemplate: RenderTemplate ) => void;
'onChange': ( callback: () => void ) => void; 'onChange': ( callback: () => void ) => void;

View File

@@ -6,7 +6,7 @@ export type CSV_Data = CSVRecord[];
export interface PersistanceConfigEntry { export interface PersistanceConfigEntry {
'sorted': string; 'sorted': string;
'active': string; 'active': string;
'ascending': boolean; 'sorting': string;
} }
export interface PersistanceConfig { export interface PersistanceConfig {