Merge remote-tracking branch 'origin/issue/7957-adjustable-swimlane-size' into issue/7957-adjustable-swimlane-size

This commit is contained in:
Charles Hacskaylo 2025-05-07 09:08:08 -07:00
commit 9b17098333
14 changed files with 525 additions and 113 deletions

View File

@ -45,6 +45,7 @@
:object-path="[elementObject, domainObject]" :object-path="[elementObject, domainObject]"
@context-click-active="setContextClickState" @context-click-active="setContextClickState"
/> />
<slot name="content"></slot>
</div> </div>
</li> </li>
</template> </template>

View File

@ -42,7 +42,11 @@
:allow-drop="allowDrop" :allow-drop="allowDrop"
@dragstart-custom="moveFrom(index)" @dragstart-custom="moveFrom(index)"
@drop-custom="moveTo(index)" @drop-custom="moveTo(index)"
/> >
<template #content="slotProps">
<slot name="content" :index="index" :object="element" v-bind="slotProps"></slot>
</template>
</ElementItem>
<li class="js-last-place" @drop="moveToIndex(elements.length)"></li> <li class="js-last-place" @drop="moveToIndex(elements.length)"></li>
</ul> </ul>
<div v-if="elements.length === 0">No contained elements</div> <div v-if="elements.length === 0">No contained elements</div>

View File

@ -29,6 +29,7 @@ export default function ElementsViewProvider(openmct) {
key: 'elementsView', key: 'elementsView',
name: 'Elements', name: 'Elements',
canView: function (selection) { canView: function (selection) {
// TODO - only use this view provider if another custom provider has not been applied
const hasValidSelection = selection?.length; const hasValidSelection = selection?.length;
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';

View File

@ -1,8 +1,52 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/**
* a sizing container for objects in a layout
*/
class Container { class Container {
constructor(domainObject, size) { constructor(domainObject, size) {
/**
* the identifier of the associated domain object
* @type {import('@/api/objects/ObjectAPI.js').Identifier}
*/
this.domainObjectIdentifier = domainObject.identifier; this.domainObjectIdentifier = domainObject.identifier;
/**
* the size in percentage or pixels
* @type {number}
*/
this.size = size; this.size = size;
/**
* the default percentage scale of an object
* @type {number}
*/
this.scale = getContainerScale(domainObject); this.scale = getContainerScale(domainObject);
/**
* true if the container should be a fixed pixel size
* false if the container should be a flexible percentage size
* containers are added as flex
* @type {boolean}
*/
this.fixed = false;
} }
} }

View File

@ -0,0 +1,82 @@
<template>
<template v-if="fixed">
<input
:value="size"
aria-labelledby="pixelSize"
class="field control"
:pattern="/\d+/"
type="number"
name="value"
min="0"
@change="changeSize"
/>
<span>px</span>
</template>
<select v-model="isFixed" aria-label="fixedOrFlex">
<option :selected="!isFixed" :value="false">flex</option>
<option :selected="isFixed" :value="true">fixed</option>
</select>
</template>
<script>
import { computed, inject, ref, toRaw } from 'vue';
export default {
props: {
object: {
type: Object,
required: true
},
index: {
type: Number,
required: true
}
},
setup(props) {
const openmct = inject('openmct');
const domainObject = inject('domainObject');
openmct.objects.observe(domainObject, 'configuration.containers', updateContainer);
const container = ref(null);
const fixed = computed(() => {
return container.value?.fixed;
});
const isFixed = computed({ get: () => fixed, set: (_isFixed) => toggleFixed(_isFixed) });
const size = computed(() => container.value?.size);
function toggleFixed(_fixed) {
openmct.objectViews.emit(
`contextAction:${openmct.objects.makeKeyString(domainObject.identifier)}`,
'toggleFixedContextAction',
props.index,
_fixed
);
}
function changeSize(event) {
const _size = Number(event.target.value);
openmct.objectViews.emit(
`contextAction:${openmct.objects.makeKeyString(domainObject.identifier)}`,
'changeSizeContextAction',
props.index,
_size
);
}
function updateContainer(containers) {
container.value = containers[props.index];
}
return {
openmct,
domainObject,
container,
fixed,
isFixed,
size,
updateContainer,
changeSize
};
}
};
</script>

View File

@ -0,0 +1,21 @@
<template>
<ElementsPool>
<template #content="{ index, object }">
<TimelineElementsContent :index="index" :object="object" />
</template>
</ElementsPool>
</template>
<script>
import ElementsPool from '@/plugins/inspectorViews/elements/ElementsPool.vue';
import TimelineElementsContent from './TimelineElementsContent.vue';
export default {
components: {
ElementsPool,
TimelineElementsContent
},
setup() {
return {};
}
};
</script>

View File

@ -0,0 +1,76 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
import mount from 'utils/mount';
import TimelineElementsPool from './TimelineElementsPool.vue';
export default function TimelineElementsViewProvider(openmct) {
return {
key: 'timelineElementsView',
name: 'Elements',
canView: function (selection) {
return selection?.[0]?.[0]?.context?.item?.type === 'time-strip';
},
view: function (selection) {
let _destroy = null;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (element) {
const { destroy } = mount(
{
el: element,
components: {
TimelineElementsPool
},
provide: {
openmct,
domainObject
},
template: `<TimelineElementsPool />`
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
},
showTab: function (isEditing) {
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
return hasComposition && isEditing;
},
priority: function () {
return openmct.priority.HIGH - 1;
},
destroy: function () {
if (_destroy) {
_destroy();
}
}
};
}
};
}

View File

@ -32,7 +32,8 @@
:hide-button="!item.isEventTelemetry" :hide-button="!item.isEventTelemetry"
:button-click-on="enableExtendEventLines" :button-click-on="enableExtendEventLines"
:button-click-off="disableExtendEventLines" :button-click-off="disableExtendEventLines"
:style="[{ 'flex-basis': sizeString }]" :class="sizeClass"
:style="sizeStyle"
> >
<template #label> <template #label>
{{ item.domainObject.name }} {{ item.domainObject.name }}
@ -64,12 +65,12 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
extendedLinesBus: { container: {
type: Object, type: Object,
required: true required: true
}, },
size: { extendedLinesBus: {
type: Number, type: Object,
required: true required: true
} }
}, },
@ -81,8 +82,17 @@ export default {
}; };
}, },
computed: { computed: {
sizeString() { size() {
return `${this.size}%`; return this.container.size;
},
fixed() {
return this.container.fixed;
},
sizeClass() {
return `--${this.fixed ? 'fixed' : 'scales'}`;
},
sizeStyle() {
return `flex-basis: ${this.size}${this.fixed ? 'px' : '%'}`;
} }
}, },
watch: { watch: {

View File

@ -47,7 +47,7 @@
<TimelineObjectView <TimelineObjectView
class="c-timeline__content js-timeline__content" class="c-timeline__content js-timeline__content"
:item="item" :item="item"
:size="getContainerSize(item)" :container="containers[index]"
:extended-lines-bus :extended-lines-bus
/> />
<ResizeHandle <ResizeHandle
@ -76,7 +76,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { useDragResizer } from 'utils/vue/useDragResizer.js'; import { useDragResizer } from 'utils/vue/useDragResizer.js';
import { useFlexContainers } from 'utils/vue/useFlexContainers.js'; import { useFlexContainers } from 'utils/vue/useFlexContainers.js';
import { inject, onBeforeUnmount, onMounted, provide, ref } from 'vue'; import { inject, onBeforeUnmount, provide, ref, toRaw } from 'vue';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue'; import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue'; import ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';
@ -109,11 +109,17 @@ export default {
const openmct = inject('openmct'); const openmct = inject('openmct');
const domainObject = inject('domainObject'); const domainObject = inject('domainObject');
const path = inject('path'); const path = inject('path');
const composition = inject('composition');
const extendedLinesBus = inject('extendedLinesBus'); const extendedLinesBus = inject('extendedLinesBus');
const composition = ref(null);
let isCompositionLoaded = false;
const existingContainers = [];
const compositionCollection = openmct.composition.get(toRaw(domainObject));
compositionCollection.on('add', addItem);
compositionCollection.on('remove', removeItem);
compositionCollection.on('reorder', reorder);
const items = ref([]); const items = ref([]);
const loadedComposition = ref(null);
const extendedLinesPerKey = ref({}); const extendedLinesPerKey = ref({});
const { alignment: alignmentData, reset: resetAlignment } = useAlignment( const { alignment: alignmentData, reset: resetAlignment } = useAlignment(
@ -146,16 +152,56 @@ export default {
addContainer, addContainer,
removeContainer, removeContainer,
reorderContainers, reorderContainers,
setContainers,
containers, containers,
startContainerResizing, startContainerResizing,
containerResizing, containerResizing,
endContainerResizing endContainerResizing,
toggleFixed,
sizeFixedContainer
} = useFlexContainers(timelineHolder, { } = useFlexContainers(timelineHolder, {
containers: domainObject.configuration.containers, containers: domainObject.configuration.containers,
rowsLayout: true, rowsLayout: true,
callback: mutateContainers callback: mutateContainers
}); });
compositionCollection.load().then((loadedComposition) => {
composition.value = loadedComposition;
isCompositionLoaded = true;
// check if containers configuration matches composition
// in case composition has been modified outside of view
// if so, rebuild containers to match composition
// sync containers to composition,
// in case composition modified outside of view
// but do not mutate until user makes a change
let isConfigurationChanged = false;
composition.value.forEach((object, index) => {
const containerIndex = domainObject.configuration.containers.findIndex((container) =>
openmct.objects.areIdsEqual(container.domainObjectIdentifier, object.identifier)
);
if (containerIndex !== index) {
isConfigurationChanged = true;
}
if (containerIndex > -1) {
existingContainers.push(domainObject.configuration.containers[containerIndex]);
} else {
const container = new Container(object);
existingContainers.push(container);
}
});
// add check for total size not equal to 100? if comp and containers same, probably safe
if (isConfigurationChanged) {
console.log('yo');
setContainers(existingContainers);
mutateContainers();
}
});
function addItem(_domainObject) { function addItem(_domainObject) {
let rowCount = 0; let rowCount = 0;
@ -187,14 +233,7 @@ export default {
items.value.push(item); items.value.push(item);
if ( if (isCompositionLoaded) {
!containers.value.some((container) =>
openmct.objects.areIdsEqual(
container.domainObjectIdentifier,
item.domainObject.identifier
)
)
) {
const container = new Container(domainObject); const container = new Container(domainObject);
addContainer(container); addContainer(container);
} }
@ -245,30 +284,20 @@ export default {
openmct.objects.mutate(domainObject, 'configuration.containers', containers.value); openmct.objects.mutate(domainObject, 'configuration.containers', containers.value);
} }
onMounted(async () => { // context action called from outside component
if (composition) { function toggleFixedContextAction(index, fixed) {
composition.on('add', addItem); toggleFixed(index, fixed);
composition.on('remove', removeItem); }
composition.on('reorder', reorder);
loadedComposition.value = await composition.load(); // context action called from outside component
function changeSizeContextAction(index, size) {
const containersToRemove = containers.value.filter( sizeFixedContainer(index, size);
(container) => }
!items.value.some((item) =>
openmct.objects.areIdsEqual(
container.domainObjectIdentifier,
item.domainObject.identifier
)
)
);
}
});
onBeforeUnmount(() => { onBeforeUnmount(() => {
composition.off('add', addItem); compositionCollection.off('add', addItem);
composition.off('remove', removeItem); compositionCollection.off('remove', removeItem);
composition.off('reorder', reorder); compositionCollection.off('reorder', reorder);
}); });
return { return {
@ -281,7 +310,6 @@ export default {
containers, containers,
getContainerSize, getContainerSize,
timelineHolder, timelineHolder,
loadedComposition,
items, items,
addContainer, addContainer,
removeContainer, removeContainer,
@ -291,7 +319,9 @@ export default {
startContainerResizing, startContainerResizing,
containerResizing, containerResizing,
endContainerResizing, endContainerResizing,
mutateContainers mutateContainers,
toggleFixedContextAction,
changeSizeContextAction
}; };
}, },
data() { data() {

View File

@ -61,7 +61,8 @@ export default function TimelineViewProvider(openmct, extendedLinesBus) {
isEditing isEditing
}; };
}, },
template: '<timeline-view-layout :is-editing="isEditing"></timeline-view-layout>' template:
'<timeline-view-layout ref="timeline" :is-editing="isEditing"></timeline-view-layout>'
}, },
{ {
app: openmct.app, app: openmct.app,
@ -71,6 +72,11 @@ export default function TimelineViewProvider(openmct, extendedLinesBus) {
component = vNode.componentInstance; component = vNode.componentInstance;
_destroy = destroy; _destroy = destroy;
}, },
contextAction(action, ...args) {
if (component?.$refs?.timeline?.[action]) {
component.$refs.timeline[action](...args);
}
},
onEditModeChange(isEditing) { onEditModeChange(isEditing) {
component.isEditing = isEditing; component.isEditing = isEditing;
}, },

View File

@ -1,3 +1,31 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/**
* @typedef {Object} TimeStripConfig configuration for Time Strip views
* @property {boolean} useIndependentTime true for independent time, false for global time
* @property {Array<import('./Container').default>} containers
* @property {number} swimLaneLabelWidth
*/
export const configuration = { export const configuration = {
useIndependentTime: false, useIndependentTime: false,
containers: [], containers: [],

View File

@ -23,9 +23,9 @@
import { configuration } from './configuration.js'; import { configuration } from './configuration.js';
import ExtendedLinesBus from './ExtendedLinesBus.js'; import ExtendedLinesBus from './ExtendedLinesBus.js';
import TimelineCompositionPolicy from './TimelineCompositionPolicy.js'; import TimelineCompositionPolicy from './TimelineCompositionPolicy.js';
import TimelineElementsViewProvider from './TimelineElementsViewProvider.js';
import timelineInterceptor from './timelineInterceptor.js'; import timelineInterceptor from './timelineInterceptor.js';
import TimelineViewProvider from './TimelineViewProvider.js'; import TimelineViewProvider from './TimelineViewProvider.js';
const extendedLinesBus = new ExtendedLinesBus(); const extendedLinesBus = new ExtendedLinesBus();
export { extendedLinesBus }; export { extendedLinesBus };
@ -48,6 +48,7 @@ export default function () {
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus)); openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus));
openmct.inspectorViews.addProvider(new TimelineElementsViewProvider(openmct));
} }
install.extendedLinesBus = extendedLinesBus; install.extendedLinesBus = extendedLinesBus;

View File

@ -45,6 +45,18 @@
} }
} }
&__content {
&.--scales {
flex-grow: 1;
flex-shrink: 1;
}
&.--fixed {
flex-grow: 0;
flex-shrink: 0;
}
}
&__overlay-lines { &__overlay-lines {
@include abs(); @include abs();
opacity: 0.5; opacity: 0.5;

View File

@ -1,5 +1,26 @@
import _ from 'lodash'; /*****************************************************************************
import { ref } from 'vue'; * Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
import { computed, ref } from 'vue';
/** /**
* @typedef {Object} configuration * @typedef {Object} configuration
@ -22,25 +43,29 @@ export function useFlexContainers(
) { ) {
const containers = ref(existingContainers); const containers = ref(existingContainers);
const maxMoveSize = ref(null); const maxMoveSize = ref(null);
let sizingContainers = 0;
const fixedContainersSize = computed(() => {
return containers.value
.filter((container) => container.fixed === true)
.reduce((size, currentContainer) => size + currentContainer.size, 0);
});
function addContainer(container) { function addContainer(container) {
containers.value.push(container); containers.value.push(container);
if (container.scale) { sizeItems(containers.value);
sizingContainers += container.scale;
} else {
sizingContainers++;
}
sizeItems(containers.value, container);
callback?.(); callback?.();
} }
function removeContainer(index) { function removeContainer(index) {
const isFlexContainer = !containers.value[index].fixed;
containers.value.splice(index, 1); containers.value.splice(index, 1);
sizeToFill(containers.value);
if (isFlexContainer) {
sizeItems(containers.value);
}
callback?.(); callback?.();
} }
@ -55,20 +80,44 @@ export function useFlexContainers(
callback?.(); callback?.();
} }
function startContainerResizing(index) { function setContainers(_containers) {
const beforeContainer = containers.value[index]; containers.value = _containers;
const afterContainer = containers.value[index + 1]; sizeItems(containers.value);
}
maxMoveSize.value = beforeContainer.size + afterContainer.size; function startContainerResizing(index) {
const beforeContainer = getBeforeContainer(index);
const afterContainer = getAfterContainer(index);
if (beforeContainer && afterContainer && !beforeContainer.fixed && !afterContainer.fixed) {
maxMoveSize.value = beforeContainer.size + afterContainer.size;
}
}
function getBeforeContainer(index) {
return containers.value
.slice(0, index + 1)
.filter((container) => !container.fixed === true)
.at(-1);
}
function getAfterContainer(index) {
return containers.value.slice(index + 1).filter((container) => !container.fixed === true)[0];
} }
function containerResizing(index, delta, event) { function containerResizing(index, delta, event) {
let percentageMoved = Math.round((delta / getElSize()) * 100); const beforeContainer = getBeforeContainer(index);
let beforeContainer = containers.value[index]; const afterContainer = getAfterContainer(index);
let afterContainer = containers.value[index + 1]; const percentageMoved = Math.round((delta / getElSize()) * 100);
beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved); if (beforeContainer && afterContainer && !beforeContainer.fixed && !afterContainer.fixed) {
afterContainer.size = getContainerSize(afterContainer.size - percentageMoved); beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved);
afterContainer.size = getContainerSize(afterContainer.size - percentageMoved);
} else {
console.warn(
'Drag requires two flexible containers. Use Elements Tab in Inspector to resize.'
);
}
} }
function endContainerResizing() { function endContainerResizing() {
@ -76,11 +125,9 @@ export function useFlexContainers(
} }
function getElSize() { function getElSize() {
if (rowsLayout) { const elSize = rowsLayout === true ? element.value.offsetHeight : element.value.offsetWidth;
return element.value.offsetHeight;
} else { return elSize - fixedContainersSize.value;
return element.value.offsetWidth;
}
} }
function getContainerSize(size) { function getContainerSize(size) {
@ -94,69 +141,118 @@ export function useFlexContainers(
} }
/** /**
* Resize items so that newItem fits proportionally (newItem must be an element of items). * Resize flexible sized items so they fit proportionally within a viewport
* If newItem does not have a size or is sized at 100%, * 1. add size to 0 sized items based on scale proportional to total scale
* newItem will have size set to (1 * newItemScale) / n * 100, * 2. resize item sizes to equal 100
* where n is the total number of items and newItemScale is the scale of newItem. * if total size < 100, resize all items
* All other items will be resized to fit inside the remaining space. * if total size > 100, resize only items not resized in step 1 (newly added)
* 3. round excess and apply to last item
*
* Items may have a scale (ie. items with composition)
*
* Handles single add or removal, as well as atypical use cases,
* such as composition out of sync with containers config
* due to composition edits outside of view
*
* @param {*} items * @param {*} items
* @param {*} newItem * @param {Number} (optional) index of the item to apply excess to in the event of rounding errors
*/ */
function sizeItems(items, newItem) { function sizeItems(items, index) {
const newItemScale = newItem.scale || 1; let totalSize;
const flexItems = items.filter((item) => !item.fixed);
const flexItemsWithSize = flexItems.filter((item) => item.size);
const flexItemsWithoutSize = flexItems.filter((item) => !item.size);
// total number of flexible items, adjusted by each item scale
const totalScale = flexItems.reduce((total, item) => {
const scale = item.scale ?? 1;
return total + scale;
}, 0);
if (!newItem.size && items.length === 1) { flexItemsWithoutSize.forEach((item) => {
newItem.size = 100; const scale = item.scale ?? 1;
} else { item.size = Math.round((100 * scale) / totalScale);
if (!newItem.size || newItem.size === 100) { });
newItem.size = Math.round((100 * newItemScale) / sizingContainers);
// Resize oldItems to fit inside remaining space; totalSize = flexItems.reduce((total, item) => total + item.size, 0);
const oldItems = items.filter((item) => !_.isEqual(item, newItem));
const remainingSize = 100 - newItem.size;
oldItems.forEach((item) => { if (totalSize > 100) {
const itemScale = item.scale || 1; const addedSize = flexItemsWithoutSize.reduce((total, item) => total + item.size, 0);
item.size = Math.round((item.size * itemScale * remainingSize) / 100); const remainingSize = 100 - addedSize;
});
// Ensure items add up to 100 in case of rounding error. flexItemsWithSize.forEach((item) => {
const total = items.reduce((t, item) => t + item.size, 0); const scale = item.scale ?? 1;
const excess = Math.round(100 - total); item.size = Math.round((item.size * scale * remainingSize) / 100);
oldItems[oldItems.length - 1].size += excess; });
} } else if (totalSize < 100) {
const sizeToFill = 100 - totalSize;
flexItems.forEach((item) => {
const scale = item.scale ?? 1;
item.size = Math.round((item.size * scale * 100) / sizeToFill);
});
}
// Ensure items add up to 100 in case of rounding error.
totalSize = flexItems.reduce((total, item) => total + item.size, 0);
const excess = Math.round(100 - totalSize);
if (excess) {
const _index = index !== undefined && !items[index].fixed ? index : items.length - 1;
items[_index].size += excess;
} }
} }
/** function toggleFixed(index, fixed) {
* Scales items proportionally so total is equal to 100. let addExcessToContainer;
* Assumes that an item was removed from array. const remainingItems = containers.value.slice();
* @param {*} items const container = remainingItems.splice(index, 1)[0];
*/
function sizeToFill(items) { if (container.fixed !== fixed) {
if (items.length === 0) { if (fixed) {
return; const sizeToFill = 100 - container.size;
container.size = Math.round((container.size / 100) * getElSize());
remainingItems.forEach((item) => {
const scale = item.scale ?? 1;
item.size = Math.round((item.size * scale * 100) / sizeToFill);
});
} else {
container.size = Math.round((container.size * 100) / (getElSize() + container.size));
addExcessToContainer = index;
const remainingSize = 100 - container.size;
remainingItems.forEach((item) => {
const scale = item.scale ?? 1;
item.size = Math.round((item.size * scale * remainingSize) / 100);
});
}
container.fixed = fixed;
sizeItems(containers.value, addExcessToContainer);
callback?.();
} }
}
const oldTotal = items.reduce((total, item) => total + item.size, 0); function sizeFixedContainer(index, size) {
items.forEach((item) => { const container = containers.value[index];
const itemScale = item.scale || 1;
item.size = Math.round((item.size * itemScale * 100) / oldTotal);
});
// Ensure items add up to 100 in case of rounding error. if (container.fixed) {
const total = items.reduce((t, item) => t + item.size, 0); container.size = size;
const excess = Math.round(100 - total);
items[items.length - 1].size += excess; callback?.();
} else {
console.warn('Use view drag resizing to resize flexible containers.');
}
} }
return { return {
addContainer, addContainer,
removeContainer, removeContainer,
reorderContainers, reorderContainers,
setContainers,
containers, containers,
startContainerResizing, startContainerResizing,
containerResizing, containerResizing,
endContainerResizing endContainerResizing,
toggleFixed,
sizeFixedContainer
}; };
} }