feat: add resizable columns and horizontal scrolling
- Columns can be resized by dragging the border between them - Column widths persist during navigation (150px min, 500px max) - Container scrolls horizontally when columns exceed available space - Auto-scrolls to show newly opened columns - Resize handle highlights on hover/active state
This commit is contained in:
@@ -8,6 +8,7 @@ interface IColumn {
|
||||
objects: IS3Object[];
|
||||
prefixes: string[];
|
||||
selectedItem: string | null;
|
||||
width: number;
|
||||
}
|
||||
|
||||
@customElement('tsview-s3-columns')
|
||||
@@ -24,6 +25,11 @@ export class TsviewS3Columns extends DeesElement {
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
private resizing: { columnIndex: number; startX: number; startWidth: number } | null = null;
|
||||
private readonly DEFAULT_COLUMN_WIDTH = 250;
|
||||
private readonly MIN_COLUMN_WIDTH = 150;
|
||||
private readonly MAX_COLUMN_WIDTH = 500;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@@ -31,25 +37,57 @@ export class TsviewS3Columns extends DeesElement {
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.columns-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.column-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-width: 220px;
|
||||
max-width: 280px;
|
||||
border-right: 1px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.column:last-child {
|
||||
border-right: none;
|
||||
.resize-handle {
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resize-handle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 2px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.resize-handle:hover::after,
|
||||
.resize-handle.active::after {
|
||||
background: #6366f1;
|
||||
width: 2px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.column-wrapper:last-child .resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
@@ -151,6 +189,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
objects: result.objects,
|
||||
prefixes: result.prefixes,
|
||||
selectedItem: null,
|
||||
width: this.DEFAULT_COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
} catch (err) {
|
||||
@@ -182,8 +221,11 @@ export class TsviewS3Columns extends DeesElement {
|
||||
objects: result.objects,
|
||||
prefixes: result.prefixes,
|
||||
selectedItem: null,
|
||||
width: this.DEFAULT_COLUMN_WIDTH,
|
||||
},
|
||||
];
|
||||
// Auto-scroll to show the new column
|
||||
this.updateComplete.then(() => this.scrollToEnd());
|
||||
} catch (err) {
|
||||
console.error('Error loading folder:', err);
|
||||
}
|
||||
@@ -192,6 +234,48 @@ export class TsviewS3Columns extends DeesElement {
|
||||
// The navigate event is only for breadcrumb sync, not for column navigation
|
||||
}
|
||||
|
||||
private scrollToEnd() {
|
||||
this.scrollLeft = this.scrollWidth - this.clientWidth;
|
||||
}
|
||||
|
||||
private startResize(e: MouseEvent, columnIndex: number) {
|
||||
e.preventDefault();
|
||||
this.resizing = {
|
||||
columnIndex,
|
||||
startX: e.clientX,
|
||||
startWidth: this.columns[columnIndex].width,
|
||||
};
|
||||
document.addEventListener('mousemove', this.handleResize);
|
||||
document.addEventListener('mouseup', this.stopResize);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
private handleResize = (e: MouseEvent) => {
|
||||
if (!this.resizing) return;
|
||||
|
||||
const delta = e.clientX - this.resizing.startX;
|
||||
const newWidth = Math.min(
|
||||
this.MAX_COLUMN_WIDTH,
|
||||
Math.max(this.MIN_COLUMN_WIDTH, this.resizing.startWidth + delta)
|
||||
);
|
||||
|
||||
this.columns = this.columns.map((col, i) => {
|
||||
if (i === this.resizing!.columnIndex) {
|
||||
return { ...col, width: newWidth };
|
||||
}
|
||||
return col;
|
||||
});
|
||||
};
|
||||
|
||||
private stopResize = () => {
|
||||
this.resizing = null;
|
||||
document.removeEventListener('mousemove', this.handleResize);
|
||||
document.removeEventListener('mouseup', this.stopResize);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
private selectFile(columnIndex: number, key: string) {
|
||||
// Update selection
|
||||
this.columns = this.columns.map((col, i) => {
|
||||
@@ -240,7 +324,19 @@ export class TsviewS3Columns extends DeesElement {
|
||||
|
||||
return html`
|
||||
<div class="columns-container">
|
||||
${this.columns.map((column, index) => this.renderColumn(column, index))}
|
||||
${this.columns.map((column, index) => this.renderColumnWrapper(column, index))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderColumnWrapper(column: IColumn, index: number) {
|
||||
return html`
|
||||
<div class="column-wrapper">
|
||||
${this.renderColumn(column, index)}
|
||||
<div
|
||||
class="resize-handle ${this.resizing?.columnIndex === index ? 'active' : ''}"
|
||||
@mousedown=${(e: MouseEvent) => this.startResize(e, index)}
|
||||
></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -251,7 +347,7 @@ export class TsviewS3Columns extends DeesElement {
|
||||
: this.bucketName;
|
||||
|
||||
return html`
|
||||
<div class="column">
|
||||
<div class="column" style="width: ${column.width}px">
|
||||
<div class="column-header" title=${column.prefix || this.bucketName}>
|
||||
${headerName}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user