WIP: use flex layout container sizing for timeline

This commit is contained in:
David Tsay 2025-02-26 18:09:33 -08:00
parent 25a372e725
commit 4345722fe0
5 changed files with 199 additions and 34 deletions

View File

@ -0,0 +1,17 @@
class Container {
constructor(domainObject, size) {
this.domainObjectIdentifier = domainObject.identifier;
this.size = size;
this.scale = getContainerScale(domainObject);
}
}
function getContainerScale(domainObject) {
if (domainObject.type === 'telemetry.plot.stacked') {
return domainObject?.composition?.length;
}
return 1;
}
export default Container;

View File

@ -24,7 +24,6 @@
<SwimLane
:icon-class="item.type.definition.cssClass"
:status="status"
:min-height="item.height"
:show-ucontents="isPlanLikeObject(item.domainObject)"
:span-rows-count="item.rowCount"
:domain-object="item.domainObject"
@ -33,6 +32,7 @@
:hide-button="!hasEventTelemetry()"
:button-click-on="enableExtendEventLines"
:button-click-off="disableExtendEventLines"
:style="[{ 'flex-basis': sizeString }]"
>
<template #label>
{{ item.domainObject.name }}
@ -67,6 +67,10 @@ export default {
extendedLinesBus: {
type: Object,
required: true
},
size: {
type: Number,
required: true
}
},
data() {
@ -76,6 +80,11 @@ export default {
status: ''
};
},
computed: {
sizeString() {
return `${this.size}px`;
}
},
watch: {
item(newItem) {
if (!this.context) {

View File

@ -45,6 +45,7 @@
<TimelineObjectView
class="c-timeline__content js-timeline__content"
:item="item"
:size="getContainerSize(item)"
:extended-lines-bus
/>
<ResizeHandle
@ -71,8 +72,10 @@
<script>
import _ from 'lodash';
import { identifierEquals } from 'objectUtils';
import { useDragResizer } from 'utils/vue/useDragResizer.js';
import { inject, provide } from 'vue';
import { useFlexContainers } from 'utils/vue/useFlexContainers.js';
import { inject, onMounted, provide, ref } from 'vue';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';
@ -80,6 +83,7 @@ import ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import { useAlignment } from '../../ui/composables/alignmentContext.js';
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
import Container from './Container.js';
import ExtendedLinesOverlay from './ExtendedLinesOverlay.vue';
import TimelineObjectView from './TimelineObjectView.vue';
@ -112,6 +116,8 @@ export default {
const domainObject = inject('domainObject');
const path = inject('path');
const openmct = inject('openmct');
const items = ref([]);
const { alignment: alignmentData, reset: resetAlignment } = useAlignment(
domainObject,
path,
@ -124,11 +130,46 @@ export default {
provide('swimLaneLabelWidth', swimLaneLabelWidth);
provide('mousedown', mousedown);
return { alignmentData, resetAlignment };
// Flex containers - Swimlane height
const timelineHolder = ref(null);
onMounted(() => {
console.log(timelineHolder.value);
});
const {
addContainer,
containers,
startContainerResizing,
containerResizing,
endContainerResizing
} = useFlexContainers(timelineHolder, {
containerClass: Container,
rowsLayout: true
});
function getContainerSize(item) {
const containerforItem = containers.value.find((container) =>
identifierEquals(container.domainObjectIdentifier, item.identifier)
);
return containerforItem.size;
}
return {
containers,
getContainerSize,
timelineHolder,
items,
addContainer,
alignmentData,
resetAlignment,
startContainerResizing,
containerResizing,
endContainerResizing
};
},
data() {
return {
items: [],
timeSystems: [],
height: 0,
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
@ -205,6 +246,7 @@ export default {
};
this.items.push(item);
this.addContainer(item);
},
removeItem(identifier) {
let index = this.items.findIndex((item) =>

View File

@ -22,42 +22,42 @@
/********************************************* TIME STRIP */
.c-timeline-holder {
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
gap: 1px;
height: 100%;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
gap: 1px;
height: 100%;
// Plot view overrides
.gl-plot-display-area,
.gl-plot-axis-area.gl-plot-y {
bottom: $interiorMargin !important;
}
// Plot view overrides
.gl-plot-display-area,
.gl-plot-axis-area.gl-plot-y {
bottom: $interiorMargin !important;
}
}
.c-timeline {
&__objects {
display: contents;
&__objects {
display: contents;
.c-swimlane {
min-height: 100px; // TEMP!! Will be replaced when heights are set by user
}
.c-swimlane {
// min-height: 100px; // TEMP!! Will be replaced when heights are set by user
}
}
&__overlay-lines {
//background: rgba(deeppink, 0.2);
@include abs();
top: 20px; // Offset down to line up with time axis ticks line
pointer-events: none; // Allows clicks to pass through
z-index: 10; // Ensure it sits atop swimlanes
}
&__overlay-lines {
//background: rgba(deeppink, 0.2);
@include abs();
top: 20px; // Offset down to line up with time axis ticks line
pointer-events: none; // Allows clicks to pass through
z-index: 10; // Ensure it sits atop swimlanes
}
&__no-items {
font-style: italic;
position: absolute;
left: $interiorMargin;
top: 50%;
transform: translateY(-50%);
}
&__no-items {
font-style: italic;
position: absolute;
left: $interiorMargin;
top: 50%;
transform: translateY(-50%);
}
}

View File

@ -0,0 +1,97 @@
import { ref } from 'vue';
export function useFlexContainers(element, { rowsLayout, minContainerSize = 5, callback }) {
const containers = ref([]);
const maxMoveSize = ref(null);
let sizingContainers = 0;
function addContainer(container) {
containers.value.push(container);
if (container.scale) {
sizingContainers += container.scale;
} else {
sizingContainers++;
}
sizeItems(containers.value, container);
callback?.();
}
function startContainerResizing(index) {
const beforeContainer = containers.value[index];
const afterContainer = containers.value[index + 1];
maxMoveSize.value = beforeContainer.size + afterContainer.size;
}
function containerResizing(index, delta, event) {
let percentageMoved = Math.round((delta / getElSize()) * 100);
let beforeContainer = containers.value[index];
let afterContainer = containers.value[index + 1];
beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved);
afterContainer.size = getContainerSize(afterContainer.size - percentageMoved);
}
function endContainerResizing() {
// persist the new container sizes
callback?.(containers.value);
}
function getElSize() {
if (rowsLayout) {
return element.value.offsetHeight;
} else {
return element.value.offsetWidth;
}
}
function getContainerSize(size) {
if (size < minContainerSize) {
return minContainerSize;
} else if (size > maxMoveSize.value - minContainerSize) {
return maxMoveSize.value - minContainerSize;
} else {
return size;
}
}
// 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) {
const newItemScale = newItem.scale || 1;
if (items.length === 1) {
newItem.size = 100;
} else {
if (!newItem.size || newItem.size === 100) {
newItem.size = Math.round((100 * newItemScale) / sizingContainers);
}
// Resize oldItems to fit inside remaining space;
const oldItems = items.filter((item) => item !== newItem);
const remainingSize = 100 - newItem.size;
oldItems.forEach((item) => {
const itemScale = item.scale || 1;
item.size = Math.round((item.size * itemScale * remainingSize) / 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;
}
}
return {
addContainer,
containers,
startContainerResizing,
containerResizing,
endContainerResizing
};
}