mirror of https://github.com/Fabio286/antares.git
perf: improved scroll speed of result tables
This commit is contained in:
parent
949f7add8f
commit
bbde2bd994
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="vscroll-holder">
|
||||
<div :style="{'height': visibleHeight+'px'}" class="vscroll-holder">
|
||||
<div
|
||||
class="vscroll-spacer"
|
||||
:style="{
|
||||
|
@ -21,56 +21,53 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
// credits: https://github.com/xrado 👼
|
||||
export default {
|
||||
name: 'BaseVirtualScroll',
|
||||
props: {
|
||||
items: Array,
|
||||
itemHeight: Number
|
||||
itemHeight: Number,
|
||||
visibleHeight: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
topHeight: 0,
|
||||
bottomHeight: 0,
|
||||
visibleItems: []
|
||||
visibleItems: [],
|
||||
renderTimeout: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this._checkScrollPosition = this.checkScrollPosition.bind(this);
|
||||
this.checkScrollPosition();
|
||||
this._checkScrollPosition = this.updateWindow.bind(this);
|
||||
this.updateWindow();
|
||||
this.$el.addEventListener('scroll', this._checkScrollPosition);
|
||||
this.$el.addEventListener('wheel', this._checkScrollPosition);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$el.removeEventListener('scroll', this._checkScrollPosition);
|
||||
this.$el.removeEventListener('wheel', this._checkScrollPosition);
|
||||
},
|
||||
methods: {
|
||||
checkScrollPosition (e = {}) {
|
||||
const el = this.$el;
|
||||
checkScrollPosition () {
|
||||
|
||||
// prevent parent scroll
|
||||
if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0))
|
||||
e.preventDefault();
|
||||
|
||||
this.updateWindow(e);
|
||||
},
|
||||
|
||||
updateWindow (e) {
|
||||
const visibleItemsCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
|
||||
const totalScrollHeight = this.items.length * this.itemHeight;
|
||||
const offset = 50;
|
||||
|
||||
const scrollTop = this.$el.scrollTop;
|
||||
const offset = 5;
|
||||
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
|
||||
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
|
||||
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
|
||||
const lastCutIndex = lastVisibleIndex + offset;
|
||||
|
||||
this.visibleItems = this.items.slice(firstCutIndex, lastCutIndex);
|
||||
clearTimeout(this.renderTimeout);
|
||||
|
||||
this.topHeight = firstCutIndex * this.itemHeight;
|
||||
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
|
||||
this.renderTimeout = setTimeout(() => {
|
||||
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
|
||||
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
|
||||
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
|
||||
const lastCutIndex = lastVisibleIndex + offset;
|
||||
|
||||
this.visibleItems = this.items.slice(firstCutIndex, lastCutIndex);
|
||||
|
||||
this.topHeight = firstCutIndex * this.itemHeight;
|
||||
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
v-if="results.rows"
|
||||
ref="resultTable"
|
||||
:items="sortedResults"
|
||||
:item-height="25"
|
||||
:item-height="22"
|
||||
class="vscroll"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
:visible-height="resultsSize"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<div class="table table-hover">
|
||||
|
@ -40,24 +41,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="tbody">
|
||||
<div
|
||||
<WorkspaceQueryTableRow
|
||||
v-for="row in items"
|
||||
:key="row._id"
|
||||
:row="row"
|
||||
:fields="fields"
|
||||
class="tr"
|
||||
:class="{'selected': selectedRows.includes(row._id)}"
|
||||
@click="selectRow($event, row._id)"
|
||||
>
|
||||
<WorkspaceQueryTableCell
|
||||
v-for="(col, cKey) in row"
|
||||
:key="cKey"
|
||||
:content="col"
|
||||
:field="cKey"
|
||||
:precision="fieldPrecision(cKey)"
|
||||
:type="fieldType(cKey)"
|
||||
@updateField="updateField($event, row[primaryField.name])"
|
||||
@contextmenu="contextMenu($event, {id: row._id, field: cKey})"
|
||||
/>
|
||||
</div>
|
||||
@selectRow="selectRow($event, row._id)"
|
||||
@updateField="updateField($event, row[primaryField.name])"
|
||||
@contextmenu="contextMenu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -68,7 +62,7 @@
|
|||
<script>
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
import WorkspaceQueryTableCell from '@/components/WorkspaceQueryTableCell';
|
||||
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
|
||||
import TableContext from '@/components/WorkspaceQueryTableContext';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
|
@ -76,7 +70,7 @@ export default {
|
|||
name: 'WorkspaceQueryTable',
|
||||
components: {
|
||||
BaseVirtualScroll,
|
||||
WorkspaceQueryTableCell,
|
||||
WorkspaceQueryTableRow,
|
||||
TableContext
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="field !== '_id'"
|
||||
ref="cell"
|
||||
class="td p-0"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent="$emit('contextmenu', $event)"
|
||||
>
|
||||
<span
|
||||
v-if="!isInlineEditor"
|
||||
class="cell-content px-2"
|
||||
:class="`${isNull(content)} type-${type}`"
|
||||
@dblclick="editON"
|
||||
>{{ content | typeFormat(type, precision) | cutText }}</span>
|
||||
<template v-else>
|
||||
<input
|
||||
v-if="inputProps.mask"
|
||||
ref="editField"
|
||||
v-model="localContent"
|
||||
v-mask="inputProps.mask"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="localContent"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</template>
|
||||
<div class="tr" @click="selectRow($event, row._id)">
|
||||
<div
|
||||
v-for="(col, cKey) in row"
|
||||
:key="cKey"
|
||||
class="td p-0"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
|
||||
@updateField="updateField($event, row[primaryField.name])"
|
||||
>
|
||||
<template v-if="cKey !== '_id'">
|
||||
<span
|
||||
v-if="!isInlineEditor[cKey]"
|
||||
class="cell-content px-2"
|
||||
:class="`${isNull(col)} type-${fieldType(cKey)}`"
|
||||
@dblclick="editON($event, col, cKey)"
|
||||
>{{ col | typeFormat(fieldType(cKey), fieldPrecision(cKey)) | cutText }}</span>
|
||||
<template v-else>
|
||||
<input
|
||||
v-if="inputProps.mask"
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
v-mask="inputProps.mask"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="isTextareaEditor"
|
||||
:confirm-text="$t('word.update')"
|
||||
|
@ -41,19 +46,19 @@
|
|||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
{{ $t('word.edit') }} "{{ field }}"
|
||||
{{ $t('word.edit') }} "{{ editingField }}"
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<textarea
|
||||
v-model="localContent"
|
||||
v-model="editingContent"
|
||||
class="form-input textarea-editor"
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-field-info">
|
||||
<div><b>{{ $t('word.size') }}</b>: {{ localContent.length }}</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ type.toUpperCase() }}</div>
|
||||
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,7 +70,7 @@
|
|||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
{{ $t('word.edit') }} "{{ field }}"
|
||||
{{ $t('word.edit') }} "{{ editingField }}"
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
|
@ -73,7 +78,7 @@
|
|||
<div v-if="contentInfo.size">
|
||||
<img
|
||||
v-if="isImage"
|
||||
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(localContent)}`"
|
||||
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(editingContent)}`"
|
||||
class="img-responsive p-centered bg-checkered"
|
||||
>
|
||||
<div v-else class="text-center">
|
||||
|
@ -93,10 +98,10 @@
|
|||
</transition>
|
||||
<div class="editor-field-info">
|
||||
<div>
|
||||
<b>{{ $t('word.size') }}</b>: {{ localContent.length | formatBytes }}<br>
|
||||
<b>{{ $t('word.size') }}</b>: {{ editingContent.length | formatBytes }}<br>
|
||||
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
|
||||
</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ type.toUpperCase() }}</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label>{{ $t('message.uploadFile') }}</label>
|
||||
|
@ -123,10 +128,13 @@ import { mask } from 'vue-the-mask';
|
|||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTableCell',
|
||||
name: 'WorkspaceQueryTableRow',
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
},
|
||||
filters: {
|
||||
formatBytes,
|
||||
cutText (val) {
|
||||
|
@ -163,22 +171,20 @@ export default {
|
|||
return val;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
},
|
||||
props: {
|
||||
type: String,
|
||||
field: String,
|
||||
precision: [Number, null],
|
||||
content: [String, Number, Object, Date, Uint8Array]
|
||||
row: Object,
|
||||
fields: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isInlineEditor: false,
|
||||
isInlineEditor: {},
|
||||
isTextareaEditor: false,
|
||||
isBlobEditor: false,
|
||||
willBeDeleted: false,
|
||||
localContent: null,
|
||||
originalContent: null,
|
||||
editingContent: null,
|
||||
editingType: null,
|
||||
editingField: null,
|
||||
contentInfo: {
|
||||
ext: '',
|
||||
mime: '',
|
||||
|
@ -220,35 +226,61 @@ export default {
|
|||
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fields.forEach(field => {
|
||||
this.isInlineEditor[field.name] = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
fieldType (cKey) {
|
||||
let type = 'unknown';
|
||||
const field = this.fields.filter(field => field.name === cKey)[0];
|
||||
if (field)
|
||||
type = field.type;
|
||||
|
||||
return type;
|
||||
},
|
||||
fieldPrecision (cKey) {
|
||||
let length = 0;
|
||||
const field = this.fields.filter(field => field.name === cKey)[0];
|
||||
if (field)
|
||||
length = field.precision;
|
||||
|
||||
return length;
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null ? ' is-null' : '';
|
||||
},
|
||||
bufferToBase64 (val) {
|
||||
return bufferToBase64(val);
|
||||
},
|
||||
editON () {
|
||||
if (LONG_TEXT.includes(this.type)) {
|
||||
editON (event, content, field) {
|
||||
const type = this.fieldType(field);
|
||||
this.originalContent = content;
|
||||
this.editingType = type;
|
||||
this.editingField = field;
|
||||
|
||||
if (LONG_TEXT.includes(type)) {
|
||||
this.isTextareaEditor = true;
|
||||
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOB.includes(this.type)) {
|
||||
if (BLOB.includes(type)) {
|
||||
this.isBlobEditor = true;
|
||||
this.localContent = this.content ? this.content : '';
|
||||
this.editingContent = this.originalContent || '';
|
||||
this.fileToUpload = null;
|
||||
this.willBeDeleted = false;
|
||||
|
||||
if (this.content !== null) {
|
||||
const buff = Buffer.from(this.localContent);
|
||||
if (this.originalContent !== null) {
|
||||
const buff = Buffer.from(this.editingContent);
|
||||
if (buff.length) {
|
||||
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||
const { ext, mime } = mimeFromHex(hex);
|
||||
this.contentInfo = {
|
||||
ext,
|
||||
mime,
|
||||
size: this.localContent.length
|
||||
size: this.editingContent.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -256,20 +288,24 @@ export default {
|
|||
}
|
||||
|
||||
// Inline editable fields
|
||||
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
|
||||
this.$nextTick(() => { // Focus on input
|
||||
this.$refs.cell.blur();
|
||||
event.target.blur();
|
||||
|
||||
this.$nextTick(() => this.$refs.editField.focus());
|
||||
this.$nextTick(() => document.querySelector('.editable-field').focus());
|
||||
});
|
||||
this.isInlineEditor = true;
|
||||
|
||||
const obj = {
|
||||
[field]: true
|
||||
};
|
||||
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
|
||||
},
|
||||
editOFF () {
|
||||
this.isInlineEditor = false;
|
||||
this.isInlineEditor[this.editingField] = false;
|
||||
let content;
|
||||
if (!['blob', 'mediumblob', 'longblob'].includes(this.type)) {
|
||||
if (this.localContent === this.$options.filters.typeFormat(this.content, this.type)) return;// If not changed
|
||||
content = this.localContent;
|
||||
if (!['blob', 'mediumblob', 'longblob'].includes(this.editingType)) {
|
||||
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
|
||||
content = this.editingContent;
|
||||
}
|
||||
else { // Handle file upload
|
||||
if (this.willBeDeleted) {
|
||||
|
@ -283,10 +319,13 @@ export default {
|
|||
}
|
||||
|
||||
this.$emit('updateField', {
|
||||
field: this.field,
|
||||
type: this.type,
|
||||
field: this.editingField,
|
||||
type: this.editingType,
|
||||
content
|
||||
});
|
||||
|
||||
this.editingType = null;
|
||||
this.editingField = null;
|
||||
},
|
||||
hideEditorModal () {
|
||||
this.isTextareaEditor = false;
|
||||
|
@ -295,7 +334,7 @@ export default {
|
|||
downloadFile () {
|
||||
const downloadLink = document.createElement('a');
|
||||
|
||||
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.localContent)}`;
|
||||
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.editingContent)}`;
|
||||
downloadLink.setAttribute('download', `${this.field}.${this.contentInfo.ext}`);
|
||||
document.body.appendChild(downloadLink);
|
||||
|
||||
|
@ -310,13 +349,22 @@ export default {
|
|||
this.willBeDeleted = false;
|
||||
},
|
||||
prepareToDelete () {
|
||||
this.localContent = '';
|
||||
this.editingContent = '';
|
||||
this.contentInfo = {
|
||||
ext: '',
|
||||
mime: '',
|
||||
size: null
|
||||
};
|
||||
this.willBeDeleted = true;
|
||||
},
|
||||
updateField (event, id) {
|
||||
this.$emit('updateField', event, id);
|
||||
},
|
||||
contextMenu (event, cell) {
|
||||
this.$emit('updateField', event, cell);
|
||||
},
|
||||
selectRow (event, row) {
|
||||
this.$emit('selectRow', event, row);
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue