1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-02-08 07:48:40 +01:00

feat: hotkeys to navigate inside a table resultset

This commit is contained in:
Giulio Ganci 2022-06-04 08:45:48 +02:00
parent d3b9e08446
commit 49abd1ea7f
4 changed files with 195 additions and 35 deletions

View File

@ -5,7 +5,6 @@
tabindex="0" tabindex="0"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@keyup.delete="showDeleteConfirmModal" @keyup.delete="showDeleteConfirmModal"
@keydown.ctrl.a="selectAllRows($event)"
@keydown.esc="deselectRows" @keydown.esc="deselectRows"
> >
<TableContext <TableContext
@ -78,7 +77,11 @@
:key-usage="keyUsage" :key-usage="keyUsage"
:element-type="elementType" :element-type="elementType"
:class="{'selected': selectedRows.includes(row._antares_id)}" :class="{'selected': selectedRows.includes(row._antares_id)}"
@select-row="selectRow($event, row._antares_id)" :selected="selectedRows.includes(row._antares_id)"
:selected-cell="selectedRows.length === 1 && selectedRows.includes(row._antares_id) ? selectedField : null"
@start-editing="isEditingRow = true"
@stop-editing="isEditingRow = false"
@select-row="selectRow"
@update-field="updateField($event, row)" @update-field="updateField($event, row)"
@contextmenu="contextMenu" @contextmenu="contextMenu"
/> />
@ -162,7 +165,9 @@ export default {
currentSortDir: 'asc', currentSortDir: 'asc',
resultsetIndex: 0, resultsetIndex: 0,
scrollElement: null, scrollElement: null,
rowHeight: 23 rowHeight: 23,
selectedField: null,
isEditingRow: false
}; };
}, },
computed: { computed: {
@ -268,9 +273,11 @@ export default {
}, },
mounted () { mounted () {
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
window.addEventListener('keydown', this.onKey);
}, },
unmounted () { unmounted () {
window.removeEventListener('resize', this.resizeResults); window.removeEventListener('resize', this.resizeResults);
window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
fieldType (cKey) { fieldType (cKey) {
@ -447,20 +454,23 @@ export default {
return row; return row;
}); });
}, },
selectRow (event, row) { selectRow (event, row, field) {
if (event.ctrlKey) { this.selectedField = field;
if (this.selectedRows.includes(row)) const selectedRowId = row._antares_id;
this.selectedRows = this.selectedRows.filter(el => el !== row);
if (event.ctrlKey || event.metaKey) {
if (this.selectedRows.includes(selectedRowId))
this.selectedRows = this.selectedRows.filter(el => el !== selectedRowId);
else else
this.selectedRows.push(row); this.selectedRows.push(selectedRowId);
} }
else if (event.shiftKey) { else if (event.shiftKey) {
if (!this.selectedRows.length) if (!this.selectedRows.length)
this.selectedRows.push(row); this.selectedRows.push(selectedRowId);
else { else {
const lastID = this.selectedRows.slice(-1)[0]; const lastID = this.selectedRows.slice(-1)[0];
const lastIndex = this.sortedResults.findIndex(el => el._antares_id === lastID); const lastIndex = this.sortedResults.findIndex(el => el._antares_id === lastID);
const clickedIndex = this.sortedResults.findIndex(el => el._antares_id === row); const clickedIndex = this.sortedResults.findIndex(el => el._antares_id === selectedRowId);
if (lastIndex > clickedIndex) { if (lastIndex > clickedIndex) {
for (let i = clickedIndex; i < lastIndex; i++) for (let i = clickedIndex; i < lastIndex; i++)
this.selectedRows.push(this.sortedResults[i]._antares_id); this.selectedRows.push(this.sortedResults[i]._antares_id);
@ -472,17 +482,19 @@ export default {
} }
} }
else else
this.selectedRows = [row]; this.selectedRows = [selectedRowId];
}, },
selectAllRows (e) { selectAllRows (e) {
if (e.target.classList.contains('editable-field')) return; if (e.target.classList.contains('editable-field')) return;
this.selectedField = 0;
this.selectedRows = this.localResults.reduce((acc, curr) => { this.selectedRows = this.localResults.reduce((acc, curr) => {
acc.push(curr._antares_id); acc.push(curr._antares_id);
return acc; return acc;
}, []); }, []);
}, },
deselectRows () { deselectRows () {
if (!this.isEditingRow)
this.selectedRows = []; this.selectedRows = [];
}, },
contextMenu (event, cell) { contextMenu (event, cell) {
@ -536,6 +548,113 @@ export default {
content: rows, content: rows,
filename filename
}); });
},
onKey (e) {
if (!this.isSelected)
return;
if (this.isEditingRow)
return;
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyA' && !e.altKey)
this.selectAllRows(e);
// row naviation stuff
if ((e.code.includes('Arrow') || e.code === 'Tab') && this.sortedResults.length > 0 && !e.altKey) {
e.preventDefault();
const aviableFields= Object.keys(this.sortedResults[0]).slice(0, -1); // removes _antares_id
if (!this.selectedField)
this.selectedField = aviableFields[0];
const selectedId = this.selectedRows[0];
const selectedIndex = this.sortedResults.findIndex(row => row._antares_id === selectedId);
const selectedFieldIndex = aviableFields.findIndex(field => field === this.selectedField);
let nextIndex = 0;
let nextFieldIndex = 0;
if (selectedIndex > -1) {
switch (e.code) {
case 'ArrowDown':
nextIndex = selectedIndex + 1;
nextFieldIndex = selectedFieldIndex;
if (nextIndex > this.sortedResults.length -1)
nextIndex = this.sortedResults.length -1;
break;
case 'ArrowUp':
nextIndex = selectedIndex - 1;
nextFieldIndex = selectedFieldIndex;
if (nextIndex < 0)
nextIndex = 0;
break;
case 'ArrowRight':
nextIndex = selectedIndex;
nextFieldIndex = selectedFieldIndex + 1;
if (nextFieldIndex > aviableFields.length -1)
nextFieldIndex = 0;
break;
case 'ArrowLeft':
nextIndex = selectedIndex;
nextFieldIndex = selectedFieldIndex - 1;
if (nextFieldIndex < 0)
nextFieldIndex = aviableFields.length -1;
break;
case 'Tab':
nextIndex = selectedIndex;
if (e.shiftKey) {
nextFieldIndex = selectedFieldIndex - 1;
if (nextFieldIndex < 0)
nextFieldIndex = aviableFields.length -1;
}
else {
nextFieldIndex = selectedFieldIndex + 1;
if (nextFieldIndex > aviableFields.length -1)
nextFieldIndex = 0;
}
}
}
if (this.sortedResults[nextIndex] && nextIndex !== selectedIndex) {
this.selectedRows = [this.sortedResults[nextIndex]._antares_id];
this.$nextTick(() => this.scrollToCell(this.scrollElement.querySelector('.td.selected')));
}
if (aviableFields[nextFieldIndex] && nextFieldIndex !== selectedFieldIndex) {
this.selectedField = aviableFields[nextFieldIndex];
this.$nextTick(() => this.scrollToCell(this.scrollElement.querySelector('.td.selected')));
}
}
},
scrollToCell (el) {
if (!el) return;
const visYMin = this.scrollElement.scrollTop;
const visYMax = this.scrollElement.scrollTop + this.scrollElement.clientHeight - el.clientHeight;
const visXMin = this.scrollElement.scrollLeft;
const visXMax = this.scrollElement.scrollLeft + this.scrollElement.clientWidth - el.clientWidth;
if (el.offsetTop < visYMin)
this.scrollElement.scrollTop = el.offsetTop;
else if (el.offsetTop >= visYMax)
this.scrollElement.scrollTop = el.offsetTop - this.scrollElement.clientHeight + el.clientHeight;
if (el.offsetLeft < visXMin)
this.scrollElement.scrollLeft = el.offsetLeft;
else if (el.offsetLeft >= visXMax)
this.scrollElement.scrollLeft = el.offsetLeft - this.scrollElement.clientWidth + el.clientWidth;
} }
} }
}; };

View File

@ -2,14 +2,15 @@
<div <div
class="tr" class="tr"
:style="{height: itemHeight+'px'}" :style="{height: itemHeight+'px'}"
@click="selectRow($event, row._antares_id)"
> >
<div <div
v-for="(col, cKey) in row" v-for="(col, cKey) in row"
v-show="cKey !== '_antares_id'" v-show="cKey !== '_antares_id'"
:key="cKey" :key="cKey"
class="td p-0" class="td p-0"
tabindex="0" :class="{selected: selectedCell === cKey}"
@click="selectRow($event, cKey)"
@contextmenu.prevent="openContext($event, { id: row._antares_id, orgField: cKey })" @contextmenu.prevent="openContext($event, { id: row._antares_id, orgField: cKey })"
> >
<template v-if="cKey !== '_antares_id'"> <template v-if="cKey !== '_antares_id'">
@ -17,7 +18,7 @@
v-if="!isInlineEditor[cKey] && fields[cKey]" v-if="!isInlineEditor[cKey] && fields[cKey]"
class="cell-content" class="cell-content"
:class="`${isNull(col)} ${typeClass(fields[cKey].type)}`" :class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)" @dblclick="editON(cKey)"
>{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length)) }}</span> >{{ cutText(typeFormat(col, fields[cKey].type.toLowerCase(), fields[cKey].length)) }}</span>
<ForeignKeySelect <ForeignKeySelect
v-else-if="isForeignKey(cKey)" v-else-if="isForeignKey(cKey)"
@ -105,7 +106,7 @@
<div class="mr-4"> <div class="mr-4">
<b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }} <b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}
</div> </div>
<div> <div v-if="editingType">
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }} <b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div> </div>
</div> </div>
@ -170,7 +171,9 @@
<b>{{ $t('word.size') }}</b>: {{ formatBytes(editingContent.length) }}<br> <b>{{ $t('word.size') }}</b>: {{ formatBytes(editingContent.length) }}<br>
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }} <b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
</div> </div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div> <div v-if="editingType">
<b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}
</div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label>{{ $t('message.uploadFile') }}</label> <label>{{ $t('message.uploadFile') }}</label>
@ -230,9 +233,11 @@ export default {
fields: Object, fields: Object,
keyUsage: Array, keyUsage: Array,
itemHeight: Number, itemHeight: Number,
elementType: { type: String, default: 'table' } elementType: { type: String, default: 'table' },
selected: { type: Boolean, default: false },
selectedCell: { type: String, default: null }
}, },
emits: ['update-field', 'select-row', 'contextmenu'], emits: ['update-field', 'select-row', 'contextmenu', 'start-editing', 'stop-editing'],
data () { data () {
return { return {
isInlineEditor: {}, isInlineEditor: {},
@ -336,6 +341,9 @@ export default {
return false; return false;
}, },
isBaseSelectField () {
return this.isForeignKey(this.editingField) || this.inputProps.type === 'boolean' || this.enumArray;
},
enumArray () { enumArray () {
if (this.fields[this.editingField] && this.fields[this.editingField].enumValues) if (this.fields[this.editingField] && this.fields[this.editingField].enumValues)
return this.fields[this.editingField].enumValues.replaceAll('\'', '').split(','); return this.fields[this.editingField].enumValues.replaceAll('\'', '').split(',');
@ -362,7 +370,20 @@ export default {
this.editorMode = this.availableLanguages.find(lang => lang.id === filteredLanguages[0].languageId).slug; this.editorMode = this.availableLanguages.find(lang => lang.id === filteredLanguages[0].languageId).slug;
})(); })();
} }
},
selected (isSelected) {
if (isSelected)
window.addEventListener('keydown', this.onKey);
else {
this.editOFF();
window.removeEventListener('keydown', this.onKey);
} }
}
},
beforeUnmount () {
if (this.selected)
window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
isForeignKey (key) { isForeignKey (key) {
@ -382,11 +403,10 @@ export default {
bufferToBase64 (val) { bufferToBase64 (val) {
return bufferToBase64(val); return bufferToBase64(val);
}, },
editON (event, content, field) { editON (field) {
if (!this.isEditable || this.editingType === 'none') return; if (!this.isEditable || this.editingType === 'none') return;
window.addEventListener('keydown', this.onKey); const content = this.row[field];
const type = this.fields[field].type.toUpperCase(); const type = this.fields[field].type.toUpperCase();
this.originalContent = this.typeFormat(content, type, this.fields[field].length); this.originalContent = this.typeFormat(content, type, this.fields[field].length);
this.editingType = type; this.editingType = type;
@ -396,6 +416,7 @@ export default {
if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) { if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true; this.isTextareaEditor = true;
this.editingContent = this.typeFormat(content, type); this.editingContent = this.typeFormat(content, type);
this.$emit('start-editing', field);
return; return;
} }
@ -405,6 +426,7 @@ export default {
this.isMapModal = true; this.isMapModal = true;
this.editingContent = this.typeFormat(content, type); this.editingContent = this.typeFormat(content, type);
} }
this.$emit('start-editing', field);
return; return;
} }
@ -426,19 +448,21 @@ export default {
}; };
} }
} }
this.$emit('start-editing', field);
return; return;
} }
// Inline editable fields // Inline editable fields
this.editingContent = this.originalContent; this.editingContent = this.originalContent;
this.$nextTick(() => { // Focus on input
event.target.blur();
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
const obj = { [field]: true }; const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj }; this.isInlineEditor = { ...this.isInlineEditor, ...obj };
this.$nextTick(() => { // Focus on input
document.querySelector('.editable-field').focus();
});
this.$emit('start-editing', field);
}, },
editOFF () { editOFF () {
if (!this.editingField) return; if (!this.editingField) return;
@ -451,7 +475,13 @@ export default {
this.editingContent = this.editingContent.slice(0, -1); this.editingContent = this.editingContent.slice(0, -1);
} }
if (this.editingContent === this.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed // If not changed
if (this.editingContent === this.typeFormat(this.originalContent, this.editingType, this.editingLength)) {
this.editingType = null;
this.editingField = null;
this.$emit('stop-editing', this.editingField);
return;
}
content = this.editingContent; content = this.editingContent;
} }
@ -472,15 +502,17 @@ export default {
content content
}); });
this.$emit('stop-editing', this.editingField);
this.editingType = null; this.editingType = null;
this.editingField = null; this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}, },
hideEditorModal () { hideEditorModal () {
this.isTextareaEditor = false; this.isTextareaEditor = false;
this.isBlobEditor = false; this.isBlobEditor = false;
this.isMapModal = false; this.isMapModal = false;
this.isMultiSpatial = false; this.isMultiSpatial = false;
this.$emit('stop-editing', this.editingField);
}, },
downloadFile () { downloadFile () {
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
@ -508,8 +540,8 @@ export default {
}; };
this.willBeDeleted = true; this.willBeDeleted = true;
}, },
selectRow (event, row) { selectRow (event, field) {
this.$emit('select-row', event, row); this.$emit('select-row', event, this.row, field);
}, },
getKeyUsage (keyName) { getKeyUsage (keyName) {
if (keyName.includes('.')) if (keyName.includes('.'))
@ -523,10 +555,17 @@ export default {
}, },
onKey (e) { onKey (e) {
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') {
if (!this.editingField && e.key === 'Enter')
return this.editON(this.selectedCell);
if (this.editingField && e.key === 'Enter' && !this.isBaseSelectField)
return this.editOFF();
if (this.editingField && e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false; this.isInlineEditor[this.editingField] = false;
this.editingField = null; this.editingField = null;
window.removeEventListener('keydown', this.onKey); this.$emit('stop-editing', this.editingField);
} }
}, },
formatBytes, formatBytes,

View File

@ -231,7 +231,8 @@
.td { .td {
border-color: $body-bg-dark; border-color: $body-bg-dark;
&:focus { &:focus,
&.selected {
box-shadow: inset 0 0 0 2px darken($body-font-color-dark, 40%); box-shadow: inset 0 0 0 2px darken($body-font-color-dark, 40%);
background-color: rgba($color: #000, $alpha: 0.3); background-color: rgba($color: #000, $alpha: 0.3);
} }

View File

@ -252,7 +252,8 @@
.td { .td {
border-color: $body-bg; border-color: $body-bg;
&:focus { &:focus,
&.selected {
box-shadow: inset 0 0 0 2px lighten($body-font-color, 10%); box-shadow: inset 0 0 0 2px lighten($body-font-color, 10%);
background-color: $body-font-color-dark; background-color: $body-font-color-dark;
} }