Files
openmct/src/plugins/flexibleLayout/components/flexibleLayout.vue
charlesh88 ea69508e22 Misc Fixes 2
- Fix table resizing issue in Flex Layouts;
2019-04-16 14:45:08 -07:00

678 lines
21 KiB
Vue

/*****************************************************************************
* 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-fl">
<div
id="js-fl-drag-ghost"
class="c-fl__drag-ghost">
</div>
<div class="c-fl__empty"
v-if="areAllContainersEmpty()">
<span class="c-fl__empty-message">This Flexible Layout is currently empty</span>
</div>
<div class="c-fl__container-holder"
:class="{
'c-fl--rows': rowsLayout === true
}">
<template v-for="(container, index) in containers">
<drop-hint
class="c-fl-frame__drop-hint"
v-if="index === 0 && containers.length > 1"
:key="index"
:index="-1"
:allow-drop="allowContainerDrop"
@object-drop-to="moveContainer">
</drop-hint>
<container-component
class="c-fl__container"
:key="container.id"
:index="index"
:container="container"
:rowsLayout="rowsLayout"
:isEditing="isEditing"
@move-frame="moveFrame"
@new-frame="setFrameLocation"
@persist="persist">
</container-component>
<resize-handle
v-if="index !== (containers.length - 1)"
:key="index"
:index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'"
:isEditing="isEditing"
@init-move="startContainerResizing"
@move="containerResizing"
@end-move="endContainerResizing">
</resize-handle>
<drop-hint
class="c-fl-frame__drop-hint"
v-if="containers.length > 1"
:key="index"
:index="index"
:allowDrop="allowContainerDrop"
@object-drop-to="moveContainer">
</drop-hint>
</template>
</div>
</div>
</template>
<style lang="scss">
@import '~styles/sass-base';
@mixin containerGrippy($headerSize, $dir) {
position: absolute;
$h: 6px;
$minorOffset: ($headerSize - $h) / 2;
$majorOffset: 35%;
content: '';
display: block;
position: absolute;
@include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir);
@if $dir == 'x' {
top: $minorOffset; right: $majorOffset; bottom: $minorOffset; left: $majorOffset;
} @else {
top: $majorOffset; right: $minorOffset; bottom: $majorOffset; left: $minorOffset;
}
}
.c-fl {
@include abs();
display: flex;
.temp-toolbar {
flex: 0 0 auto;
}
&__container-holder {
display: flex;
flex: 1 1 100%; // Must be 100% to work
overflow: auto;
// Columns by default
flex-direction: row;
> * + * { margin-left: 1px; }
&[class*='--rows'] {
flex-direction: column;
> * + * {
margin-left: 0;
margin-top: 1px;
}
}
}
&__empty {
@include abs();
background: rgba($colorBodyFg, 0.1);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
> * {
font-style: italic;
opacity: 0.5;
}
}
&__drag-ghost{
background: $colorItemTreeHoverBg;
color: $colorItemTreeHoverFg;
border-radius: $basicCr;
display: flex;
align-items: center;
padding: $interiorMarginLg $interiorMarginLg * 2;
position: absolute;
top: -10000px;
z-index: 2;
&:before {
color: $colorKey;
margin-right: $interiorMarginSm;
}
}
}
.c-fl-container {
/***************************************************** CONTAINERS */
$headerSize: 16px;
display: flex;
flex-direction: column;
overflow: auto;
// flex-basis is set with inline style in code, controls size
flex-grow: 1;
flex-shrink: 1;
&__header {
// Only displayed when editing, controlled via JS
background: $editFrameMovebarColorBg;
color: $editFrameMovebarColorFg;
cursor: move;
display: flex;
align-items: center;
flex: 0 0 $headerSize;
&:before {
// Drag grippy
@include containerGrippy($headerSize, 'x');
opacity: 0.5;
}
}
&__size-indicator {
position: absolute;
display: inline-block;
right: $interiorMargin;
}
&__frames-holder {
display: flex;
flex: 1 1 100%; // Must be 100% to work
flex-direction: column; // Default
align-content: stretch;
align-items: stretch;
overflow: hidden; // This sucks, but doing in the short-term
}
.is-editing & {
&:hover {
.c-fl-container__header {
background: $editFrameHovMovebarColorBg;
color: $editFrameHovMovebarColorFg;
&:before {
opacity: .75;
}
}
}
&[s-selected] {
border: $editFrameSelectedBorder;
.c-fl-container__header {
background:$editFrameSelectedMovebarColorBg;
color: $editFrameSelectedMovebarColorFg;
&:before {
// Grippy
opacity: 1;
}
}
}
}
/****** THEIR FRAMES */
// Frames get styled here because this is particular to their presence in this layout type
.c-fl-frame {
@include browserPrefix(margin-collapse, collapse);
}
/****** ROWS LAYOUT */
.c-fl--rows & {
// Layout is rows
flex-direction: row;
&__header {
flex-basis: $headerSize;
overflow: hidden;
&:before {
// Grippy
@include containerGrippy($headerSize, 'y');
}
}
&__size-indicator {
right: 0;
top: $interiorMargin;
transform-origin: top right;
transform: rotate(-90deg) translateY(-100%);
}
&__frames-holder {
flex-direction: row;
}
}
}
.c-fl-frame {
/***************************************************** CONTAINER FRAMES */
$sizeIndicatorM: 16px;
$dropHintSize: 15px;
display: flex;
flex: 1 1;
flex-direction: column;
overflow: hidden; // Needed to allow frames to collapse when sized down
&__drag-wrapper {
flex: 1 1 auto;
overflow: auto;
.is-editing & {
> * {
pointer-events: none;
}
}
}
&__header {
flex: 0 0 auto;
margin-bottom: $interiorMargin;
}
&__size-indicator {
$size: 35px;
@include ellipsize();
background: $colorBtnBg;
border-top-left-radius: $controlCr;
color: $colorBtnFg;
display: inline-block;
padding: $interiorMarginSm 0;
position: absolute;
pointer-events: none;
text-align: center;
width: $size;
// Changed when layout is different, see below
border-top-right-radius: $controlCr;
bottom: 1px;
right: $sizeIndicatorM;
}
&__drop-hint {
flex: 0 0 $dropHintSize;
.c-drop-hint {
border-radius: $smallCr;
}
}
&__resize-handle {
$size: 2px;
$margin: 3px;
$marginHov: 0;
$grippyThickness: $size + 6;
$grippyLen: $grippyThickness * 2;
display: flex;
flex-direction: column;
flex: 0 0 ($margin * 2) + $size;
transition: $transOut;
&:before {
// The visible resize line
background: $editUIColor;
content: '';
display: block;
flex: 1 1 auto;
min-height: $size; min-width: $size;
}
&.vertical {
padding: $margin $size;
&:hover{
cursor: row-resize;
}
}
&.horizontal {
padding: $size $margin;
&:hover{
cursor: col-resize;
}
}
&:hover {
transition: $transOut;
&:before {
// The visible resize line
background: $editUIColorHov;
}
}
}
// Hide the resize-handles in first and last c-fl-frame elements
&:first-child,
&:last-child {
.c-fl-frame__resize-handle {
display: none;
}
}
.c-fl--rows & {
flex-direction: row;
&__size-indicator {
border-bottom-left-radius: $controlCr;
border-top-right-radius: 0;
bottom: $sizeIndicatorM;
right: 1px;
}
}
&--first-in-container {
border: none;
flex: 0 0 0;
.c-fl-frame__drag-wrapper {
display: none;
}
&.is-dragging {
flex-basis: $dropHintSize;
}
}
.is-empty & {
&.c-fl-frame--first-in-container {
flex: 1 1 auto;
}
&__drop-hint {
flex: 1 0 100%;
margin: 0;
}
}
.c-object-view {
display: contents;
}
}
</style>
<script>
import ContainerComponent from './container.vue';
import Container from '../utils/container';
import Frame from '../utils/frame';
import ResizeHandle from './resizeHandle.vue';
import DropHint from './dropHint.vue';
import RemoveAction from '../../remove/RemoveAction.js';
const MIN_CONTAINER_SIZE = 5;
// Resize items so that newItem fits proportionally (newItem must be an element
// of items). If newItem does not have a size or is sized at 100%, newItem will
// have size set to 1/n * 100, where n is the total number of items.
function sizeItems(items, newItem) {
if (items.length === 1) {
newItem.size = 100;
} else {
if (!newItem.size || newItem.size === 100) {
newItem.size = Math.round(100 / items.length);
}
let oldItems = items.filter(item => item !== newItem);
// Resize oldItems to fit inside remaining space;
let remainder = 100 - newItem.size;
oldItems.forEach((item) => {
item.size = Math.round(item.size * remainder / 100);
});
// Ensure items add up to 100 in case of rounding error.
let total = items.reduce((t, item) => t + item.size, 0);
let excess = Math.round(100 - total);
oldItems[oldItems.length - 1].size += excess;
}
}
// Scales items proportionally so total is equal to 100. Assumes that an item
// was removed from array.
function sizeToFill(items) {
if (items.length === 0) {
return;
}
let oldTotal = items.reduce((total, item) => total + item.size, 0);
items.forEach((item) => {
item.size = Math.round(item.size * 100 / oldTotal);
});
// Ensure items add up to 100 in case of rounding error.
let total = items.reduce((t, item) => t + item.size, 0);
let excess = Math.round(100 - total);
items[items.length - 1].size += excess;
}
export default {
inject: ['openmct', 'layoutObject'],
components: {
ContainerComponent,
ResizeHandle,
DropHint
},
data() {
return {
domainObject: this.layoutObject,
newFrameLocation: []
}
},
props: {
isEditing: Boolean
},
computed: {
layoutDirectionStr() {
if (this.rowsLayout) {
return 'Rows'
} else {
return 'Columns'
}
},
containers() {
return this.domainObject.configuration.containers;
},
rowsLayout() {
return this.domainObject.configuration.rowsLayout;
}
},
methods: {
areAllContainersEmpty() {
return !!!this.containers.filter(container => container.frames.length).length;
},
addContainer() {
let container = new Container();
this.containers.push(container);
sizeItems(this.containers, container);
this.persist();
},
deleteContainer(containerId) {
let container = this.containers.filter(c => c.id === containerId)[0],
containerIndex = this.containers.indexOf(container);
/*
remove associated domainObjects from composition
*/
container.frames.forEach(f => {
this.removeFromComposition(f.domainObjectIdentifier);
});
this.containers.splice(containerIndex, 1);
/*
add a container when there are no containers in the FL,
to prevent user from not being able to add a frame via
drag and drop.
*/
if (this.containers.length === 0) {
this.containers.push(new Container(100));
}
sizeToFill(this.containers);
this.setSelectionToParent();
this.persist();
},
moveFrame(toContainerIndex, toFrameIndex, frameId, fromContainerIndex) {
let toContainer = this.containers[toContainerIndex];
let fromContainer = this.containers[fromContainerIndex];
let frame = fromContainer.frames.filter(f => f.id === frameId)[0];
let fromIndex = fromContainer.frames.indexOf(frame);
fromContainer.frames.splice(fromIndex, 1);
sizeToFill(fromContainer.frames);
toContainer.frames.splice(toFrameIndex + 1, 0, frame);
sizeItems(toContainer.frames, frame);
this.persist();
},
setFrameLocation(containerIndex, insertFrameIndex) {
this.newFrameLocation = [containerIndex, insertFrameIndex];
},
addFrame(domainObject) {
if (this.newFrameLocation.length) {
let containerIndex = this.newFrameLocation[0],
frameIndex = this.newFrameLocation[1],
frame = new Frame(domainObject.identifier),
container = this.containers[containerIndex];
container.frames.splice(frameIndex + 1, 0, frame);
sizeItems(container.frames, frame);
this.newFrameLocation = [];
this.persist(containerIndex);
}
},
deleteFrame(frameId) {
let container = this.containers
.filter(c => c.frames.some(f => f.id === frameId))[0];
let frame = container
.frames
.filter((f => f.id === frameId))[0];
this.removeFromComposition(frame.domainObjectIdentifier)
.then(() => {
sizeToFill(container.frames)
this.setSelectionToParent();
});
},
removeFromComposition(identifier) {
return this.openmct.objects.get(identifier).then((childDomainObject) => {
this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject);
});
},
setSelectionToParent() {
this.$el.click();
},
allowContainerDrop(event, index) {
if (!event.dataTransfer.types.includes('containerid')) {
return false;
}
let containerId = event.dataTransfer.getData('containerid'),
container = this.containers.filter((c) => c.id === containerId)[0],
containerPos = this.containers.indexOf(container);
if (index === -1) {
return containerPos !== 0;
} else {
return containerPos !== index && (containerPos - 1) !== index
}
},
persist(index){
if (index) {
this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);
}
},
startContainerResizing(index) {
let beforeContainer = this.containers[index],
afterContainer = this.containers[index + 1];
this.maxMoveSize = beforeContainer.size + afterContainer.size;
},
containerResizing(index, delta, event) {
let percentageMoved = Math.round(delta / this.getElSize() * 100),
beforeContainer = this.containers[index],
afterContainer = this.containers[index + 1];
beforeContainer.size = this.getContainerSize(beforeContainer.size + percentageMoved);
afterContainer.size = this.getContainerSize(afterContainer.size - percentageMoved);
},
endContainerResizing(event) {
this.persist();
},
getElSize() {
if (this.rowsLayout) {
return this.$el.offsetHeight;
} else {
return this.$el.offsetWidth;
}
},
getContainerSize(size) {
if (size < MIN_CONTAINER_SIZE) {
return MIN_CONTAINER_SIZE
} else if (size > (this.maxMoveSize - MIN_CONTAINER_SIZE)) {
return (this.maxMoveSize - MIN_CONTAINER_SIZE);
} else {
return size;
}
},
updateDomainObject(newDomainObject) {
this.domainObject = newDomainObject;
},
moveContainer(toIndex, event) {
let containerId = event.dataTransfer.getData('containerid');
let container = this.containers.filter(c => c.id === containerId)[0];
let fromIndex = this.containers.indexOf(container);
this.containers.splice(fromIndex, 1);
if (fromIndex > toIndex) {
this.containers.splice(toIndex + 1, 0, container);
} else {
this.containers.splice(toIndex, 0, container);
}
this.persist();
},
removeChildObject(identifier) {
let removeIdentifier = this.openmct.objects.makeKeyString(identifier);
this.containers.forEach(container => {
container.frames = container.frames.filter(frame => {
let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
return removeIdentifier !== frameIdentifier;
});
});
this.persist();
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame);
this.RemoveAction = new RemoveAction(this.openmct);
this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
},
beforeDestroy() {
this.composition.off('remove', this.removeChildObject);
this.composition.off('add', this.addFrame);
this.unobserve();
}
}
</script>