Compare commits

...

4 Commits

Author SHA1 Message Date
Andrew Henry
e6daf2dd1b Custom column widths persist during resize 2018-10-26 16:27:11 -07:00
Andrew Henry
e4782c5e40 Refactored column headers into component 2018-10-26 13:38:15 -07:00
Andrew Henry
d342950f12 Implemented basic reordering of columns 2018-10-24 13:04:08 -07:00
Andrew Henry
cd6079fa6f Renders with resize hotzones 2018-10-22 13:23:34 -07:00
3 changed files with 308 additions and 35 deletions

View File

@@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<th :class="[
isSortable ? 'is-sortable' : '',
isSortable && sortOptions.key === headerKey ? 'is-sorting' : '',
isSortable && sortOptions.direction].join(' ')"
:style="{ width: columnWidths[headerKey] + 'px', 'max-width': columnWidths[headerKey] + 'px'}"
draggable="true"
@mouseup="sort"
@dragstart="columnMoveStart"
@drop="columnMoveEnd"
@dragleave="hideDropTarget"
@dragover="dragOverColumn($event.currentTarget, $event)">
<div class="c-telemetry-table__headers__content">
<div class="c-telemetry-table__resize-hotzone c-telemetry-table__resize-hotzone--right"
@mousedown="startResizeColumn"
></div>
<slot></slot>
</div>
</th>
</template>
<style lang="scss">
@import "~styles/sass-base";
@import "~styles/table";
$hotzone-size: 6px;
.c-telemetry-table__headers__content {
width: 100%;
}
.c-table.c-telemetry-table {
.c-telemetry-table__resize-hotzone {
display: block;
position: absolute;
height: 100%;
padding: 0;
margin: 0;
width: $hotzone-size;
min-width: $hotzone-size;
cursor: col-resize;
border: none;
right: 0px;
margin-right: -$tabularTdPadLR - $hotzone-size / 2;
}
th:last-child .c-telemetry-table__resize-hotzone {
display: none;
}
}
</style>
<script>
import _ from 'lodash';
export default {
props: {
headerKey: String,
headerIndex: Number,
isHeaderTitle: Boolean,
sortOptions: Object,
columnWidths: Object,
hotzone: Boolean
},
computed: {
isSortable() {
return this.sortOptions !== undefined;
}
},
methods: {
startResizeColumn($event) {
this.resizeStartX = event.clientX;
this.resizeStartWidth = this.columnWidths[this.headerKey];
document.addEventListener('mouseup', ()=>{
this.resizeStartX = undefined;
this.resizeStartWidth = undefined;
document.removeEventListener('mousemove', this.resizeColumn);
event.preventDefault();
event.stopPropagation();
}, {once: true, capture: true});
document.addEventListener('mousemove', this.resizeColumn);
event.preventDefault();
},
resizeColumn(event) {
let delta = event.clientX - this.resizeStartX;
let newWidth = this.resizeStartWidth + delta;
this.$emit('resizeColumn', this.headerKey, newWidth);
},
columnMoveStart(event) {
event.dataTransfer.setData('moveColumnFromIndex', this.headerIndex);
},
dragOverColumn(element, event) {
event.preventDefault();
this.updateDropOffset(element, event.clientX);
},
updateDropOffset(element, clientX) {
let thClientLeft = element.getBoundingClientRect().x;
let offsetInHeader = clientX - thClientLeft;
this.calculate
let dropOffsetLeft;
if (offsetInHeader < element.offsetWidth / 2) {
dropOffsetLeft = element.offsetLeft;
} else {
dropOffsetLeft = element.offsetLeft + element.offsetWidth;
}
this.$parent.$emit('drop-target-offset-changed', dropOffsetLeft);
this.$parent.$emit('drop-target-active', true);
},
hideDropTarget(){
this.$parent.$emit('drop-target-active', false);
},
columnMoveEnd(){
let toIndex = this.headerIndex;
let fromIndex = event.dataTransfer.getData('moveColumnFromIndex');
if (event.offsetX < event.target.offsetWidth / 2) {
if (toIndex > fromIndex){
toIndex--;
}
} else {
if (toIndex < fromIndex){
toIndex++;
}
}
if (toIndex !== fromIndex) {
this.$parent.$emit('move-column', fromIndex, toIndex);
}
},
sort(){
this.$emit("sort");
}
},
created() {
this.resizeColumn = _.throttle(this.resizeColumn, 50);
}
}
</script>

View File

@@ -1,9 +1,31 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<tr :style="{ top: rowTop }" :class="rowLimitClass">
<td v-for="(title, key, headerIndex) in headers"
:style="{ width: columnWidths[headerIndex], 'max-width': columnWidths[headerIndex]}"
<template v-for="(title, key) in headers">
<td :style="{ width: columnWidths[key] + 'px', 'max-width': columnWidths[key] + 'px'}"
:title="formattedRow[key]"
:class="cellLimitClasses[key]">{{formattedRow[key]}}</td>
</template>
</tr>
</template>
@@ -30,11 +52,8 @@ export default {
required: true
},
columnWidths: {
type: Array,
required: false,
default() {
return [];
},
type: Object,
required: true
},
rowIndex: {
type: Number,

View File

@@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
:class="{'loading': loading}">
@@ -8,35 +29,46 @@
Export As CSV
</a>
</div>
<div v-if="isDropTargetActive" class="c-telemetry-table__drop-target" :style="dropTargetStyle"></div>
<!-- Headers table -->
<div class="c-telemetry-table__headers-w js-table__headers-w">
<div class="c-telemetry-table__headers-w js-table__headers-w" ref="headersTable">
<table class="c-table__headers c-telemetry-table__headers"
:style="{ 'max-width': totalWidth + 'px'}">
<thead>
<tr>
<th v-for="(title, key, headerIndex) in headers"
v-on:click="sortBy(key)"
:class="['is-sortable', sortOptions.key === key ? 'is-sorting' : '', sortOptions.direction].join(' ')"
:style="{ width: columnWidths[headerIndex], 'max-width': columnWidths[headerIndex]}">{{title}}</th>
<template v-for="(title, key, headerIndex) in headers">
<table-column-header
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@sort="sortBy(key)"
@resizeColumn="resizeColumn"
:columnWidths="columnWidths"
:sortOptions="sortOptions"
>{{title}}</table-column-header>
</template>
</tr>
<tr>
<th v-for="(title, key, headerIndex) in headers"
:style="{
width: columnWidths[headerIndex],
'max-width': columnWidths[headerIndex],
}">
<template v-for="(title, key, headerIndex) in headers">
<table-column-header
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@resizeColumn="resizeColumn"
:columnWidths="columnWidths">
<search class="c-table__search"
v-model="filters[key]"
v-on:input="filterChanged(key)"
v-on:clear="clearFilter(key)" />
</th>
</table-column-header>
</template>
</tr>
</thead>
</table>
</div>
<!-- Content table -->
<div class="c-table__body-w c-telemetry-table__body-w js-telemetry-table__body-w" @scroll="scroll">
<div class="c-telemetry-table__scroll-forcer" :style="{ width: totalWidth }"></div>
<div class="c-telemetry-table__scroll-forcer" :style="{ width: totalWidth + 'px' }"></div>
<table class="c-table__body c-telemetry-table__body"
:style="{ height: totalHeight + 'px', 'max-width': totalWidth + 'px'}">
<tbody>
@@ -46,8 +78,7 @@
:rowIndex="rowIndex"
:rowOffset="rowOffset"
:rowHeight="rowHeight"
:row="row"
>
:row="row">
</telemetry-table-row>
</tbody>
</table>
@@ -56,10 +87,13 @@
<table class="c-telemetry-table__sizing js-telemetry-table__sizing"
:style="{width: calcTableWidth}">
<tr>
<th v-for="(title, key, headerIndex) in headers">{{title}}</th>
<template v-for="(title, key) in headers">
<th :key="key">{{title}}</th>
</template>
</tr>
<telemetry-table-row v-for="(sizingRowData, objectKeyString) in sizingRows"
:headers="headers"
:columnWidths="configuredColumnWidths"
:row="sizingRowData">
</telemetry-table-row>
</table>
@@ -70,6 +104,16 @@
@import "~styles/sass-base";
@import "~styles/table";
.c-telemetry-table__drop-target {
position: absolute;
width: 2px;
top: 27px;
background-color: $editColor;
box-shadow: rgba($editColor, 0.5) 0 0 10px;
z-index: 1;
pointer-events: none;
}
.c-telemetry-table {
// Table that displays telemetry in a scrolling body area
overflow: hidden;
@@ -101,12 +145,6 @@
thead {
display: block;
}
th {
&:not(:first-child) {
border-left: 1px solid $colorTabHeaderBorder;
}
}
}
/******************************* ELEMENTS */
@@ -178,16 +216,20 @@
<script>
import TelemetryTableRow from './table-row.vue';
import search from '../../../ui/components/controls/search.vue';
import TableColumnHeader from './table-column-header.vue';
import _ from 'lodash';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = 20;
const RESIZE_HOT_ZONE = 10;
const MOVE_TRIGGER_WAIT = 500;
export default {
components: {
TelemetryTableRow,
TableColumnHeader,
search
},
inject: ['table', 'openmct', 'csvExporter'],
@@ -196,7 +238,8 @@ export default {
return {
headers: {},
visibleRows: [],
columnWidths: [],
columnWidths: {},
configuredColumnWidths: {},
sizingRows: {},
rowHeight: ROW_HEIGHT,
scrollOffset: 0,
@@ -212,7 +255,18 @@ export default {
headersHolderEl: undefined,
calcTableWidth: '100%',
processingScroll: false,
updatingView: false
updatingView: false,
dropOffsetLeft: undefined,
isDropTargetActive: false,
lastHeaderKey: undefined
}
},
computed: {
dropTargetStyle() {
return {
height: this.totalHeight + 47 + 'px',
left: this.dropOffsetLeft && this.dropOffsetLeft + 'px'
}
}
},
methods: {
@@ -256,8 +310,9 @@ export default {
},
updateHeaders() {
let headers = this.table.configuration.getVisibleHeaders();
this.headers = headers;
let headerKeys = Object.keys(this.headers);
this.lastHeaderKey = headerKeys[headerKeys.length - 1];
this.$nextTick().then(this.calculateColumnWidths);
},
setSizingTableWidth() {
@@ -268,16 +323,17 @@ export default {
}
},
calculateColumnWidths() {
let columnWidths = [];
let columnWidths = {};
let totalWidth = 0;
let sizingRowEl = this.sizingTable.children[0];
let sizingCells = Array.from(sizingRowEl.children);
sizingCells.forEach((cell) => {
Object.keys(this.headers).forEach((headerKey, headerIndex)=>{
let cell = sizingCells[headerIndex];
let columnWidth = cell.offsetWidth;
columnWidths.push(columnWidth + 'px');
columnWidths[headerKey] = columnWidth;
totalWidth += columnWidth;
});
})
this.columnWidths = columnWidths;
this.totalWidth = totalWidth;
@@ -404,12 +460,53 @@ export default {
let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);
delete this.sizingRows[objectKeyString];
this.updateHeaders();
},
resizeColumn(key, newWidth) {
let delta = newWidth - this.columnWidths[key];
this.columnWidths[key] = newWidth;
this.$set(this.configuredColumnWidths, key, newWidth);
this.columnWidths[this.lastHeaderKey] = this.columnWidths[this.lastHeaderKey] - delta;
},
setDropTargetOffset(dropOffsetLeft) {
this.dropOffsetLeft = dropOffsetLeft;
},
moveColumn(from, to) {
let newHeaderKeys = Object.keys(this.headers);
let moveFromKey = newHeaderKeys[from];
let moveFromWidth = columnWidths[from];
if (to < from) {
newHeaderKeys.splice(from, 1);
newHeaderKeys.splice(to, 0, moveFromKey);
} else {
newHeaderKeys.splice(from, 1);
newHeaderKeys.splice(to, 0, moveFromKey);
}
let newHeaders = newHeaderKeys.reduce((headers, headerKey)=>{
headers[headerKey] = this.headers[headerKey];
return headers;
}, {});
this.headers = newHeaders;
this.dropOffsetLeft = undefined;
this.lastHeaderKey = newHeaderKeys[newHeaderKeys.length - 1];
this.dropTargetActive(false);
},
dropTargetActive(isActive) {
this.isDropTargetActive = isActive;
}
},
created() {
this.filterChanged = _.debounce(this.filterChanged, 500);
},
mounted() {
this.$on('drop-target-offset-changed', this.setDropTargetOffset);
this.$on('drop-target-active', this.dropTargetActive);
this.$on('move-column', this.moveColumn);
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);