mirror of
https://github.com/nasa/openmct.git
synced 2025-05-09 12:03:21 +00:00
Merge remote-tracking branch 'origin/issue/7957-adjustable-swimlane-size' into issue/7957-adjustable-swimlane-size
This commit is contained in:
commit
9b17098333
@ -45,6 +45,7 @@
|
||||
:object-path="[elementObject, domainObject]"
|
||||
@context-click-active="setContextClickState"
|
||||
/>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -42,7 +42,11 @@
|
||||
:allow-drop="allowDrop"
|
||||
@dragstart-custom="moveFrom(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>
|
||||
</ul>
|
||||
<div v-if="elements.length === 0">No contained elements</div>
|
||||
|
@ -29,6 +29,7 @@ export default function ElementsViewProvider(openmct) {
|
||||
key: 'elementsView',
|
||||
name: 'Elements',
|
||||
canView: function (selection) {
|
||||
// TODO - only use this view provider if another custom provider has not been applied
|
||||
const hasValidSelection = selection?.length;
|
||||
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
|
||||
|
||||
|
@ -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 {
|
||||
constructor(domainObject, size) {
|
||||
/**
|
||||
* the identifier of the associated domain object
|
||||
* @type {import('@/api/objects/ObjectAPI.js').Identifier}
|
||||
*/
|
||||
this.domainObjectIdentifier = domainObject.identifier;
|
||||
/**
|
||||
* the size in percentage or pixels
|
||||
* @type {number}
|
||||
*/
|
||||
this.size = size;
|
||||
/**
|
||||
* the default percentage scale of an object
|
||||
* @type {number}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
82
src/plugins/timeline/TimelineElementsContent.vue
Normal file
82
src/plugins/timeline/TimelineElementsContent.vue
Normal 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>
|
21
src/plugins/timeline/TimelineElementsPool.vue
Normal file
21
src/plugins/timeline/TimelineElementsPool.vue
Normal 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>
|
76
src/plugins/timeline/TimelineElementsViewProvider.js
Normal file
76
src/plugins/timeline/TimelineElementsViewProvider.js
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
@ -32,7 +32,8 @@
|
||||
:hide-button="!item.isEventTelemetry"
|
||||
:button-click-on="enableExtendEventLines"
|
||||
:button-click-off="disableExtendEventLines"
|
||||
:style="[{ 'flex-basis': sizeString }]"
|
||||
:class="sizeClass"
|
||||
:style="sizeStyle"
|
||||
>
|
||||
<template #label>
|
||||
{{ item.domainObject.name }}
|
||||
@ -64,12 +65,12 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
extendedLinesBus: {
|
||||
container: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
extendedLinesBus: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
@ -81,8 +82,17 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sizeString() {
|
||||
return `${this.size}%`;
|
||||
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: {
|
||||
|
@ -47,7 +47,7 @@
|
||||
<TimelineObjectView
|
||||
class="c-timeline__content js-timeline__content"
|
||||
:item="item"
|
||||
:size="getContainerSize(item)"
|
||||
:container="containers[index]"
|
||||
:extended-lines-bus
|
||||
/>
|
||||
<ResizeHandle
|
||||
@ -76,7 +76,7 @@
|
||||
import _ from 'lodash';
|
||||
import { useDragResizer } from 'utils/vue/useDragResizer.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 ResizeHandle from '@/ui/layout/ResizeHandle/ResizeHandle.vue';
|
||||
@ -109,11 +109,17 @@ export default {
|
||||
const openmct = inject('openmct');
|
||||
const domainObject = inject('domainObject');
|
||||
const path = inject('path');
|
||||
const composition = inject('composition');
|
||||
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 loadedComposition = ref(null);
|
||||
const extendedLinesPerKey = ref({});
|
||||
|
||||
const { alignment: alignmentData, reset: resetAlignment } = useAlignment(
|
||||
@ -146,16 +152,56 @@ export default {
|
||||
addContainer,
|
||||
removeContainer,
|
||||
reorderContainers,
|
||||
setContainers,
|
||||
containers,
|
||||
startContainerResizing,
|
||||
containerResizing,
|
||||
endContainerResizing
|
||||
endContainerResizing,
|
||||
toggleFixed,
|
||||
sizeFixedContainer
|
||||
} = useFlexContainers(timelineHolder, {
|
||||
containers: domainObject.configuration.containers,
|
||||
rowsLayout: true,
|
||||
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) {
|
||||
let rowCount = 0;
|
||||
|
||||
@ -187,14 +233,7 @@ export default {
|
||||
|
||||
items.value.push(item);
|
||||
|
||||
if (
|
||||
!containers.value.some((container) =>
|
||||
openmct.objects.areIdsEqual(
|
||||
container.domainObjectIdentifier,
|
||||
item.domainObject.identifier
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (isCompositionLoaded) {
|
||||
const container = new Container(domainObject);
|
||||
addContainer(container);
|
||||
}
|
||||
@ -245,30 +284,20 @@ export default {
|
||||
openmct.objects.mutate(domainObject, 'configuration.containers', containers.value);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (composition) {
|
||||
composition.on('add', addItem);
|
||||
composition.on('remove', removeItem);
|
||||
composition.on('reorder', reorder);
|
||||
// context action called from outside component
|
||||
function toggleFixedContextAction(index, fixed) {
|
||||
toggleFixed(index, fixed);
|
||||
}
|
||||
|
||||
loadedComposition.value = await composition.load();
|
||||
|
||||
const containersToRemove = containers.value.filter(
|
||||
(container) =>
|
||||
!items.value.some((item) =>
|
||||
openmct.objects.areIdsEqual(
|
||||
container.domainObjectIdentifier,
|
||||
item.domainObject.identifier
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
// context action called from outside component
|
||||
function changeSizeContextAction(index, size) {
|
||||
sizeFixedContainer(index, size);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
composition.off('add', addItem);
|
||||
composition.off('remove', removeItem);
|
||||
composition.off('reorder', reorder);
|
||||
compositionCollection.off('add', addItem);
|
||||
compositionCollection.off('remove', removeItem);
|
||||
compositionCollection.off('reorder', reorder);
|
||||
});
|
||||
|
||||
return {
|
||||
@ -281,7 +310,6 @@ export default {
|
||||
containers,
|
||||
getContainerSize,
|
||||
timelineHolder,
|
||||
loadedComposition,
|
||||
items,
|
||||
addContainer,
|
||||
removeContainer,
|
||||
@ -291,7 +319,9 @@ export default {
|
||||
startContainerResizing,
|
||||
containerResizing,
|
||||
endContainerResizing,
|
||||
mutateContainers
|
||||
mutateContainers,
|
||||
toggleFixedContextAction,
|
||||
changeSizeContextAction
|
||||
};
|
||||
},
|
||||
data() {
|
||||
|
@ -61,7 +61,8 @@ export default function TimelineViewProvider(openmct, extendedLinesBus) {
|
||||
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,
|
||||
@ -71,6 +72,11 @@ export default function TimelineViewProvider(openmct, extendedLinesBus) {
|
||||
component = vNode.componentInstance;
|
||||
_destroy = destroy;
|
||||
},
|
||||
contextAction(action, ...args) {
|
||||
if (component?.$refs?.timeline?.[action]) {
|
||||
component.$refs.timeline[action](...args);
|
||||
}
|
||||
},
|
||||
onEditModeChange(isEditing) {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
|
@ -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 = {
|
||||
useIndependentTime: false,
|
||||
containers: [],
|
||||
|
@ -23,9 +23,9 @@
|
||||
import { configuration } from './configuration.js';
|
||||
import ExtendedLinesBus from './ExtendedLinesBus.js';
|
||||
import TimelineCompositionPolicy from './TimelineCompositionPolicy.js';
|
||||
import TimelineElementsViewProvider from './TimelineElementsViewProvider.js';
|
||||
import timelineInterceptor from './timelineInterceptor.js';
|
||||
import TimelineViewProvider from './TimelineViewProvider.js';
|
||||
|
||||
const extendedLinesBus = new ExtendedLinesBus();
|
||||
|
||||
export { extendedLinesBus };
|
||||
@ -48,6 +48,7 @@ export default function () {
|
||||
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
|
||||
|
||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus));
|
||||
openmct.inspectorViews.addProvider(new TimelineElementsViewProvider(openmct));
|
||||
}
|
||||
|
||||
install.extendedLinesBus = extendedLinesBus;
|
||||
|
@ -45,6 +45,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
&.--scales {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
&.--fixed {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay-lines {
|
||||
@include abs();
|
||||
opacity: 0.5;
|
||||
|
@ -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
|
||||
@ -22,25 +43,29 @@ export function useFlexContainers(
|
||||
) {
|
||||
const containers = ref(existingContainers);
|
||||
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) {
|
||||
containers.value.push(container);
|
||||
|
||||
if (container.scale) {
|
||||
sizingContainers += container.scale;
|
||||
} else {
|
||||
sizingContainers++;
|
||||
}
|
||||
|
||||
sizeItems(containers.value, container);
|
||||
sizeItems(containers.value);
|
||||
|
||||
callback?.();
|
||||
}
|
||||
|
||||
function removeContainer(index) {
|
||||
const isFlexContainer = !containers.value[index].fixed;
|
||||
|
||||
containers.value.splice(index, 1);
|
||||
sizeToFill(containers.value);
|
||||
|
||||
if (isFlexContainer) {
|
||||
sizeItems(containers.value);
|
||||
}
|
||||
|
||||
callback?.();
|
||||
}
|
||||
@ -55,20 +80,44 @@ export function useFlexContainers(
|
||||
callback?.();
|
||||
}
|
||||
|
||||
function startContainerResizing(index) {
|
||||
const beforeContainer = containers.value[index];
|
||||
const afterContainer = containers.value[index + 1];
|
||||
function setContainers(_containers) {
|
||||
containers.value = _containers;
|
||||
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) {
|
||||
let percentageMoved = Math.round((delta / getElSize()) * 100);
|
||||
let beforeContainer = containers.value[index];
|
||||
let afterContainer = containers.value[index + 1];
|
||||
const beforeContainer = getBeforeContainer(index);
|
||||
const afterContainer = getAfterContainer(index);
|
||||
const percentageMoved = Math.round((delta / getElSize()) * 100);
|
||||
|
||||
beforeContainer.size = getContainerSize(beforeContainer.size + percentageMoved);
|
||||
afterContainer.size = getContainerSize(afterContainer.size - percentageMoved);
|
||||
if (beforeContainer && afterContainer && !beforeContainer.fixed && !afterContainer.fixed) {
|
||||
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() {
|
||||
@ -76,11 +125,9 @@ export function useFlexContainers(
|
||||
}
|
||||
|
||||
function getElSize() {
|
||||
if (rowsLayout) {
|
||||
return element.value.offsetHeight;
|
||||
} else {
|
||||
return element.value.offsetWidth;
|
||||
}
|
||||
const elSize = rowsLayout === true ? element.value.offsetHeight : element.value.offsetWidth;
|
||||
|
||||
return elSize - fixedContainersSize.value;
|
||||
}
|
||||
|
||||
function getContainerSize(size) {
|
||||
@ -94,69 +141,118 @@ export function useFlexContainers(
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 * newItemScale) / n * 100,
|
||||
* where n is the total number of items and newItemScale is the scale of newItem.
|
||||
* All other items will be resized to fit inside the remaining space.
|
||||
* Resize flexible sized items so they fit proportionally within a viewport
|
||||
* 1. add size to 0 sized items based on scale proportional to total scale
|
||||
* 2. resize item sizes to equal 100
|
||||
* if total size < 100, resize all items
|
||||
* 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 {*} newItem
|
||||
* @param {Number} (optional) index of the item to apply excess to in the event of rounding errors
|
||||
*/
|
||||
function sizeItems(items, newItem) {
|
||||
const newItemScale = newItem.scale || 1;
|
||||
function sizeItems(items, index) {
|
||||
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) {
|
||||
newItem.size = 100;
|
||||
} else {
|
||||
if (!newItem.size || newItem.size === 100) {
|
||||
newItem.size = Math.round((100 * newItemScale) / sizingContainers);
|
||||
flexItemsWithoutSize.forEach((item) => {
|
||||
const scale = item.scale ?? 1;
|
||||
item.size = Math.round((100 * scale) / totalScale);
|
||||
});
|
||||
|
||||
// Resize oldItems to fit inside remaining space;
|
||||
const oldItems = items.filter((item) => !_.isEqual(item, newItem));
|
||||
const remainingSize = 100 - newItem.size;
|
||||
totalSize = flexItems.reduce((total, item) => total + item.size, 0);
|
||||
|
||||
oldItems.forEach((item) => {
|
||||
const itemScale = item.scale || 1;
|
||||
item.size = Math.round((item.size * itemScale * remainingSize) / 100);
|
||||
});
|
||||
if (totalSize > 100) {
|
||||
const addedSize = flexItemsWithoutSize.reduce((total, item) => total + item.size, 0);
|
||||
const remainingSize = 100 - addedSize;
|
||||
|
||||
// Ensure items add up to 100 in case of rounding error.
|
||||
const total = items.reduce((t, item) => t + item.size, 0);
|
||||
const excess = Math.round(100 - total);
|
||||
oldItems[oldItems.length - 1].size += excess;
|
||||
}
|
||||
flexItemsWithSize.forEach((item) => {
|
||||
const scale = item.scale ?? 1;
|
||||
item.size = Math.round((item.size * scale * remainingSize) / 100);
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales items proportionally so total is equal to 100.
|
||||
* Assumes that an item was removed from array.
|
||||
* @param {*} items
|
||||
*/
|
||||
function sizeToFill(items) {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
function toggleFixed(index, fixed) {
|
||||
let addExcessToContainer;
|
||||
const remainingItems = containers.value.slice();
|
||||
const container = remainingItems.splice(index, 1)[0];
|
||||
|
||||
if (container.fixed !== fixed) {
|
||||
if (fixed) {
|
||||
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);
|
||||
items.forEach((item) => {
|
||||
const itemScale = item.scale || 1;
|
||||
item.size = Math.round((item.size * itemScale * 100) / oldTotal);
|
||||
});
|
||||
function sizeFixedContainer(index, size) {
|
||||
const container = containers.value[index];
|
||||
|
||||
// Ensure items add up to 100 in case of rounding error.
|
||||
const total = items.reduce((t, item) => t + item.size, 0);
|
||||
const excess = Math.round(100 - total);
|
||||
items[items.length - 1].size += excess;
|
||||
if (container.fixed) {
|
||||
container.size = size;
|
||||
|
||||
callback?.();
|
||||
} else {
|
||||
console.warn('Use view drag resizing to resize flexible containers.');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addContainer,
|
||||
removeContainer,
|
||||
reorderContainers,
|
||||
setContainers,
|
||||
containers,
|
||||
startContainerResizing,
|
||||
containerResizing,
|
||||
endContainerResizing
|
||||
endContainerResizing,
|
||||
toggleFixed,
|
||||
sizeFixedContainer
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user