Flexible Layout Refactor (#2223)

* only store identifiers in frames

* move drop hints outside frame

* fix resizing

* fix drag and drop frames

* fix reordering of columns

* multiple improvements

* fix styling for drop hint in empty container

* fix frame reorder in same column

* better drop target show logic

* better frame drop to logic

* fix container reordering

* add uuids for frames and containers to prevent clashing

* toolbar updates

* use shared subobject component to ease styling

* add type cssClass to subobject component header, and delete frame header vue file

* add context menu in subobject views

* change height and width to size for both frames and containers

* remove uneccesary methods from resizeHanfle and inline logic instead

* remove left click logic from context-menu mixin, add a click handler to dropDownContextMenu to show menu

* make a mixin to listen for isEditing

* encapsulate drop hints, and pass allowDrop logic to check if drop hint is valid

* use event.dataTransfer instead of vue events for container reordering

* remove vue events for frame dnd and use html events

* better implementation of toolbar

* remove unused event

* fix container resizing when adding new container

* make reviewer requested changes

* add containerId to event vs having a JSON object

* watch domainObject from flexible layouts and pass down to components

* change domainObject directly on add Container

* update domainObject on change, and cahnge toolBarProvider to function that returns an object

* fix plugin

* set domainObject as data property in felxibleLayouts

* use class instead of inline styles

* Cleanup code

inline this.$el for components that measure their own size
replace snapToPercentage with Math.round
inject object as layoutObject, not dObject.

* reuse sizing logic between frames and containers

* clean up handlers

split handlers for createFrame and moveFrame events in container.
reorganize methods for each to clarify how they operate.

* ObjectView only stops propagation when it handles event

* use ids in toolbar to ensure correct items are mutated

Because index may change due to drag and drop events,
deleteFrame and deleteContainer should operate using identifiers
instead of index.

Also, generate path for hasFrame using indexes when object is
selected, otherwise hasFrame may refer to the incorrect path.
This commit is contained in:
Deep Tailor 2018-12-07 09:34:33 -08:00 committed by Pete Richards
parent e07cfc9394
commit a87fc51fbb
18 changed files with 674 additions and 619 deletions

View File

@ -3,13 +3,15 @@ import Overlay from './Overlay';
import Vue from 'vue';
class Dialog extends Overlay {
constructor({iconClass, message, title, ...options}) {
constructor({iconClass, message, title, hint, timestamp, ...options}) {
let component = new Vue({
provide: {
iconClass,
message,
title
title,
hint,
timestamp
},
components: {
DialogComponent: DialogComponent

View File

@ -23,8 +23,13 @@
<div class="u-contents">
<div class="c-so-view__header">
<div class="c-so-view__header__start">
<div class="c-so-view__name icon-object">{{ item.domainObject.name }}</div>
<div class="c-so-view__context-actions c-disclosure-button"></div>
<div class="c-so-view__name"
:class="cssClass">
{{ item.domainObject.name }}
</div>
<context-menu-drop-down
:object-path="objectPath">
</context-menu-drop-down>
</div>
<div class="c-so-view__header__end">
<div class="c-button icon-expand local-controls--hidden"></div>
@ -98,7 +103,8 @@
</style>
<script>
import ObjectView from '../../../ui/components/layout/ObjectView.vue'
import ObjectView from '../../../ui/components/layout/ObjectView.vue';
import contextMenuDropDown from './contextMenuDropDown.vue';
export default {
inject: ['openmct'],
@ -107,12 +113,25 @@
},
components: {
ObjectView,
contextMenuDropDown
},
data() {
let type = this.openmct.types.get(this.item.domainObject.type);
return {
cssClass: type.definition.cssClass,
objectPath: [this.item.domainObject].concat(this.openmct.router.path)
}
},
mounted() {
this.item.config.attachListeners();
if (this.item.config) {
this.item.config.attachListeners();
}
},
destroyed() {
this.item.config.removeListeners();
if (this.item.config) {
this.item.config.removeListeners();
}
}
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<div class="c-so-view__context-actions c-disclosure-button"
@click="showContextMenu"></div>
</template>
<script>
import contextMenu from '../../../ui/components/mixins/context-menu'
export default {
props: ['object-path'],
mixins: [contextMenu]
}
</script>

View File

@ -22,47 +22,52 @@
<template>
<div class="c-fl-container"
:style="[{'flex-basis': size}]"
:class="{'is-empty': frames.length === 1}">
:style="[{'flex-basis': sizeString}]"
:class="{'is-empty': !frames.length}">
<div class="c-fl-container__header icon-grippy-ew"
v-show="isEditing"
draggable="true"
@dragstart="startContainerDrag"
@dragend="stopContainerDrag">
<span class="c-fl-container__size-indicator">{{ size }}</span>
@dragstart="startContainerDrag">
<span class="c-fl-container__size-indicator">{{ sizeString }}</span>
</div>
<drop-hint
class="c-fl-frame__drop-hint"
:index="-1"
:allow-drop="allowDrop"
@object-drop-to="moveOrCreateFrame">
</drop-hint>
<div class="c-fl-container__frames-holder">
<div class="u-contents"
v-for="(frame, i) in frames"
:key="i">
<template
v-for="(frame, i) in frames">
<frame-component
class="c-fl-container__frame"
:style="{
'flex-basis': `${frame.height}%`
}"
:key="frame.id"
:frame="frame"
:size="frame.height"
:index="i"
:containerIndex="index"
:isEditing="isEditing"
:isDragging="isDragging"
@frame-drag-from="frameDragFrom"
@frame-drop-to="frameDropTo"
@delete-frame="promptBeforeDeletingFrame"
@add-container="addContainer">
:containerIndex="index">
</frame-component>
<drop-hint
class="c-fl-frame__drop-hint"
:key="i"
:index="i"
:allowDrop="allowDrop"
@object-drop-to="moveOrCreateFrame">
</drop-hint>
<resize-handle
v-if="i !== 0 && (i !== frames.length - 1)"
v-show="isEditing"
v-if="(i !== frames.length - 1)"
:key="i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
@init-move="startFrameResizing"
@move="frameResizing"
@end-move="endFrameResizing">
</resize-handle>
</div>
</template>
</div>
</div>
</template>
@ -71,61 +76,98 @@
import FrameComponent from './frame.vue';
import Frame from '../utils/frame';
import ResizeHandle from './resizeHandle.vue';
import DropHint from './dropHint.vue';
import isEditingMixin from '../mixins/isEditing';
const SNAP_TO_PERCENTAGE = 1;
const MIN_FRAME_SIZE = 5;
export default {
inject:['openmct', 'domainObject'],
props: ['size', 'frames', 'index', 'isEditing', 'isDragging', 'rowsLayout'],
inject:['openmct'],
props: ['container', 'index', 'rowsLayout'],
mixins: [isEditingMixin],
components: {
FrameComponent,
ResizeHandle
ResizeHandle,
DropHint
},
data() {
return {
initialPos: 0,
frameIndex: 0,
maxMoveSize: 0
computed: {
frames() {
return this.container.frames;
},
sizeString() {
return `${Math.round(this.container.size)}%`
}
},
methods: {
frameDragFrom(frameIndex) {
this.$emit('frame-drag-from', this.index, frameIndex);
},
frameDropTo(frameIndex, event) {
let domainObject = event.dataTransfer.getData('domainObject'),
frameObject;
allowDrop(event, index) {
if (event.dataTransfer.getData('domainObject')) {
return true;
}
let frameId = event.dataTransfer.getData('frameid'),
containerIndex = Number(event.dataTransfer.getData('containerIndex'));
if (domainObject) {
frameObject = new Frame(JSON.parse(domainObject));
if (!frameId) {
return false;
}
this.$emit('frame-drop-to', this.index, frameIndex, frameObject);
if (containerIndex === this.index) {
let frame = this.container.frames.filter((f) => f.id === frameId)[0],
framePos = this.container.frames.indexOf(frame);
if (index === -1) {
return framePos !== 0;
} else {
return framePos !== index && (framePos - 1) !== index
}
} else {
return true;
}
},
moveOrCreateFrame(insertIndex, event) {
if (event.dataTransfer.types.includes('domainobject')) {
// create frame using domain object
let domainObject = JSON.parse(event.dataTransfer.getData('domainObject'));
this.$emit(
'create-frame',
this.index,
insertIndex,
domainObject.identifier
);
return;
};
// move frame.
let frameId = event.dataTransfer.getData('frameid');
let containerIndex = Number(event.dataTransfer.getData('containerIndex'));
this.$emit(
'move-frame',
this.index,
insertIndex,
frameId,
containerIndex
);
},
startFrameResizing(index) {
let beforeFrame = this.frames[index],
afterFrame = this.frames[index + 1];
this.maxMoveSize = beforeFrame.height + afterFrame.height;
this.maxMoveSize = beforeFrame.size + afterFrame.size;
},
frameResizing(index, delta, event) {
let percentageMoved = (delta / this.getElSize(this.$el))*100,
let percentageMoved = Math.round(delta / this.getElSize() * 100),
beforeFrame = this.frames[index],
afterFrame = this.frames[index + 1];
beforeFrame.height = this.snapToPercentage(beforeFrame.height + percentageMoved);
afterFrame.height = this.snapToPercentage(afterFrame.height - percentageMoved);
beforeFrame.size = this.getFrameSize(beforeFrame.size + percentageMoved);
afterFrame.size = this.getFrameSize(afterFrame.size - percentageMoved);
},
endFrameResizing(index, event) {
this.persist();
},
getElSize(el) {
getElSize() {
if (this.rowsLayout) {
return el.offsetWidth;
return this.$el.offsetWidth;
} else {
return el.offsetHeight;
return this.$el.offsetHeight;
}
},
getFrameSize(size) {
@ -137,76 +179,23 @@ export default {
return size;
}
},
snapToPercentage(value){
let rem = value % SNAP_TO_PERCENTAGE,
roundedValue;
if (rem < 0.5) {
roundedValue = Math.floor(value/SNAP_TO_PERCENTAGE)*SNAP_TO_PERCENTAGE;
} else {
roundedValue = Math.ceil(value/SNAP_TO_PERCENTAGE)*SNAP_TO_PERCENTAGE;
}
return this.getFrameSize(roundedValue);
},
persist() {
this.$emit('persist', this.index);
},
promptBeforeDeletingFrame(frameIndex) {
let deleteFrame = this.deleteFrame;
let prompt = this.openmct.overlays.dialog({
iconClass: 'alert',
message: `This action will remove ${this.frames[frameIndex].domainObject.name} from this Flexible Layout. Do you want to continue?`,
buttons: [
{
label: 'Ok',
emphasis: 'true',
callback: function () {
deleteFrame(frameIndex);
prompt.dismiss();
},
},
{
label: 'Cancel',
callback: function () {
prompt.dismiss();
}
}
]
});
},
deleteFrame(frameIndex) {
this.frames.splice(frameIndex, 1);
this.$parent.recalculateOldFrameSize(this.frames);
this.persist();
},
deleteContainer() {
this.$emit('delete-container', this.index);
},
addContainer() {
this.$emit('add-container', this.index);
},
startContainerDrag(event) {
event.stopPropagation();
this.$emit('start-container-drag', this.index);
},
stopContainerDrag(event) {
event.stopPropagation();
this.$emit('stop-container-drag');
event.dataTransfer.setData('containerid', this.container.id);
}
},
mounted() {
let context = {
item: this.domainObject,
method: this.deleteContainer,
item: this.$parent.domainObject,
addContainer: this.addContainer,
index: this.index,
type: 'container'
type: 'container',
containerId: this.container.id
}
this.unsubscribeSelection = this.openmct.selection.selectable(this.$el, context, false);
},
},
beforeDestroy() {
this.unsubscribeSelection();
}

View File

@ -21,7 +21,8 @@
*****************************************************************************/
<template>
<div>
<div v-if="isEditing"
v-show="isValidTarget">
<div class="c-drop-hint c-drop-hint--always-show"
:class="{'is-mouse-over': isMouseOver}"
@dragenter="dragenter"
@ -36,11 +37,21 @@
</style>
<script>
import isEditingMixin from '../mixins/isEditing';
export default {
props:['index'],
props:{
index: Number,
allowDrop: {
type: Function,
required: true
}
},
mixins: [isEditingMixin],
data() {
return {
isMouseOver: false
isMouseOver: false,
isValidTarget: false
}
},
methods: {
@ -51,8 +62,23 @@ export default {
this.isMouseOver = false;
},
dropHandler(event) {
this.$emit('object-drop-to', event, this.index);
this.$emit('object-drop-to', this.index, event);
this.isValidTarget = false;
},
dragstart(event) {
this.isValidTarget = this.allowDrop(event, this.index);
},
dragend() {
this.isValidTarget = false;
}
},
mounted() {
document.addEventListener('dragstart', this.dragstart);
document.addEventListener('dragend', this.dragend);
},
destroyed() {
document.removeEventListener('dragstart', this.dragstart);
document.removeEventListener('dragend', this.dragend);
}
}
</script>

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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 class="c-fl__empty"
@ -10,40 +32,31 @@
'c-fl--rows': rowsLayout === true
}">
<div class="u-contents"
v-for="(container, index) in containers"
:key="index">
<template v-for="(container, index) in containers">
<drop-hint
style="flex-basis: 15px;"
class="c-fl-frame__drop-hint"
v-if="index === 0 && containers.length > 1"
v-show="isContainerDragging"
:key="index"
:index="-1"
@object-drop-to="containerDropTo">
:allow-drop="allowContainerDrop"
@object-drop-to="moveContainer">
</drop-hint>
<container-component
class="c-fl__container"
ref="containerComponent"
:key="container.id"
:index="index"
:size="`${Math.round(container.width)}%`"
:frames="container.frames"
:isEditing="isEditing"
:isDragging="isDragging"
:container="container"
:rowsLayout="rowsLayout"
@addFrame="addFrame"
@frame-drag-from="frameDragFromHandler"
@frame-drop-to="frameDropToHandler"
@persist="persist"
@delete-container="promptBeforeDeletingContainer"
@add-container="addContainer"
@start-container-drag="startContainerDrag"
@stop-container-drag="stopContainerDrag">
@move-frame="moveFrame"
@create-frame="createFrame"
@persist="persist">
</container-component>
<resize-handle
v-if="index !== (containers.length - 1)"
v-show="isEditing"
:key="index"
:index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'"
@init-move="startContainerResizing"
@ -52,14 +65,15 @@
</resize-handle>
<drop-hint
style="flex-basis: 15px;"
class="c-fl-frame__drop-hint"
v-if="containers.length > 1"
v-show="isContainerDragging"
:key="index"
:index="index"
@object-drop-to="containerDropTo">
:allowDrop="allowContainerDrop"
@object-drop-to="moveContainer">
</drop-hint>
</div>
</div>
</template>
</div>
</div>
</template>
@ -383,7 +397,7 @@
}
&__drop-hint {
flex: 1 1 100%;
flex: 1 0 100%;
margin: 0;
}
}
@ -397,31 +411,63 @@
<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 isEditingMixin from '../mixins/isEditing';
const SNAP_TO_PERCENTAGE = 1,
MIN_CONTAINER_SIZE = 5;
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', 'domainObject'],
inject: ['openmct', 'layoutObject'],
mixins: [isEditingMixin],
components: {
ContainerComponent,
ResizeHandle,
DropHint
},
data() {
let containers = this.domainObject.configuration.containers,
rowsLayout = this.domainObject.configuration.rowsLayout;
return {
containers: containers,
dragFrom: [],
isEditing: false,
isDragging: false,
isContainerDragging: false,
rowsLayout: rowsLayout,
maxMoveSize: 0
domainObject: this.layoutObject
}
},
computed: {
@ -431,144 +477,105 @@ export default {
} 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 > 1).length;
return !!!this.containers.filter(container => container.frames.length).length;
},
addContainer() {
let newSize = 100/(this.containers.length+1);
let container = new Container(newSize)
this.recalculateContainerSize(newSize);
let container = new Container();
this.containers.push(container);
},
recalculateContainerSize(newSize) {
this.containers.forEach((container) => {
container.width = newSize;
});
},
recalculateNewFrameSize(multFactor, framesArray){
framesArray.forEach((frame, index) => {
if (index === 0) {
return;
}
let frameSize = frame.height
frame.height = this.snapToPercentage(multFactor * frameSize);
});
},
recalculateOldFrameSize(framesArray) {
let totalRemainingSum = framesArray.map((frame,i) => {
if (i !== 0) {
return frame.height
} else {
return 0;
}
}).reduce((a, c) => a + c);
framesArray.forEach((frame, index) => {
if (index === 0) {
return;
}
if (framesArray.length === 2) {
frame.height = 100;
} else {
let newSize = frame.height + ((frame.height / totalRemainingSum) * (100 - totalRemainingSum));
frame.height = this.snapToPercentage(newSize);
}
});
},
addFrame(frame, index) {
this.containers[index].addFrame(frame);
},
frameDragFromHandler(containerIndex, frameIndex) {
this.dragFrom = [containerIndex, frameIndex];
},
frameDropToHandler(containerIndex, frameIndex, frameObject) {
let newContainer = this.containers[containerIndex];
this.isDragging = false;
if (!frameObject) {
frameObject = this.containers[this.dragFrom[0]].frames.splice(this.dragFrom[1], 1)[0];
this.recalculateOldFrameSize(this.containers[this.dragFrom[0]].frames);
}
if (!frameObject.height) {
frameObject.height = 100 / Math.max(newContainer.frames.length - 1, 1);
}
newContainer.frames.splice((frameIndex + 1), 0, frameObject);
let newTotalHeight = newContainer.frames.reduce((total, frame) => {
let num = Number(frame.height);
if(isNaN(num)) {
return total;
} else {
return total + num;
}
},0);
let newMultFactor = 100 / newTotalHeight;
this.recalculateNewFrameSize(newMultFactor, newContainer.frames);
sizeItems(this.containers, container);
this.persist();
},
deleteContainer(containerId) {
let container = this.containers.filter(c => c.id === containerId)[0];
let containerIndex = this.containers.indexOf(container);
this.containers.splice(containerIndex, 1);
sizeToFill(this.containers);
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();
},
createFrame(containerIndex, insertFrameIndex, objectIdentifier) {
let frame = new Frame(objectIdentifier);
let container = this.containers[containerIndex];
container.frames.splice(insertFrameIndex + 1, 0, frame);
sizeItems(container.frames, frame);
this.persist();
},
deleteFrame(frameId) {
let container = this.containers
.filter(c => c.frames.some(f => f.id === frameId))[0];
let containerIndex = this.containers.indexOf(container);
let frame = container
.frames
.filter((f => f.id === frameId))[0];
let frameIndex = container.frames.indexOf(frame);
container.frames.splice(frameIndex, 1);
sizeToFill(container.frames);
this.persist(containerIndex);
},
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]);
this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, '.configuration.containers', this.containers);
this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers);
}
},
isEditingHandler(isEditing) {
this.isEditing = isEditing;
if (this.isEditing) {
this.$el.click(); //force selection of flexible-layout for toolbar
}
if (this.isDragging && isEditing === false) {
this.isDragging = false;
}
},
dragstartHandler() {
if (this.isEditing) {
this.isDragging = true;
}
},
dragendHandler() {
this.isDragging = false;
},
startContainerResizing(index) {
let beforeContainer = this.containers[index],
afterContainer = this.containers[index + 1];
this.maxMoveSize = beforeContainer.width + afterContainer.width;
this.maxMoveSize = beforeContainer.size + afterContainer.size;
},
containerResizing(index, delta, event) {
let percentageMoved = (delta/this.getElSize(this.$el))*100,
let percentageMoved = Math.round(delta / this.getElSize() * 100),
beforeContainer = this.containers[index],
afterContainer = this.containers[index + 1];
beforeContainer.width = this.getContainerSize(this.snapToPercentage(beforeContainer.width + percentageMoved));
afterContainer.width = this.getContainerSize(this.snapToPercentage(afterContainer.width - percentageMoved));
beforeContainer.size = this.getContainerSize(beforeContainer.size + percentageMoved);
afterContainer.size = this.getContainerSize(afterContainer.size - percentageMoved);
},
endContainerResizing(event) {
this.persist();
},
getElSize(el) {
getElSize() {
if (this.rowsLayout) {
return el.offsetHeight;
return this.$el.offsetHeight;
} else {
return el.offsetWidth;
return this.$el.offsetWidth;
}
},
getContainerSize(size) {
@ -580,80 +587,19 @@ export default {
return size;
}
},
snapToPercentage(value) {
let rem = value % SNAP_TO_PERCENTAGE,
roundedValue;
if (rem < 0.5) {
roundedValue = Math.floor(value/SNAP_TO_PERCENTAGE)*SNAP_TO_PERCENTAGE;
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 {
roundedValue = Math.ceil(value/SNAP_TO_PERCENTAGE)*SNAP_TO_PERCENTAGE;
this.containers.splice(toIndex, 0, container);
}
return roundedValue;
},
toggleLayoutDirection(v) {
this.rowsLayout = v;
},
promptBeforeDeletingContainer(containerIndex) {
let deleteContainer = this.deleteContainer;
let prompt = this.openmct.overlays.dialog({
iconClass: 'alert',
message: `This action will permanently delete container ${containerIndex + 1} from this Flexible Layout`,
buttons: [
{
label: 'Ok',
emphasis: 'true',
callback: function () {
deleteContainer(containerIndex);
prompt.dismiss();
},
},
{
label: 'Cancel',
callback: function () {
prompt.dismiss();
}
}
]
});
},
deleteContainer(containerIndex) {
this.containers.splice(containerIndex, 1);
this.recalculateContainerSize(100/this.containers.length);
this.persist();
},
addContainer(containerIndex) {
let newContainer = new Container();
if (typeof containerIndex === 'number') {
this.containers.splice(containerIndex+1, 0, newContainer);
} else {
this.containers.push(newContainer);
}
this.recalculateContainerSize(100/this.containers.length);
this.persist();
},
startContainerDrag(index) {
this.isContainerDragging = true;
this.containerDragFrom = index;
},
stopContainerDrag() {
this.isContainerDragging = false;
},
containerDropTo(event, index) {
let fromContainer = this.containers.splice(this.containerDragFrom, 1)[0];
if (index === -1) {
this.containers.unshift(fromContainer);
} else {
this.containers.splice(index, 0, fromContainer);
}
this.persist();
}
},
@ -662,23 +608,17 @@ export default {
let context = {
item: this.domainObject,
addContainer: this.addContainer,
deleteContainer: this.deleteContainer,
deleteFrame: this.deleteFrame,
type: 'flexible-layout'
}
this.unsubscribeSelection = this.openmct.selection.selectable(this.$el, context, true);
this.openmct.objects.observe(this.domainObject, 'configuration.rowsLayout', this.toggleLayoutDirection);
this.openmct.editor.on('isEditing', this.isEditingHandler);
document.addEventListener('dragstart', this.dragstartHandler);
document.addEventListener('dragend', this.dragendHandler);
this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
},
beforeDestroy() {
this.unsubscribeSelection();
this.openmct.editor.off('isEditing', this.isEditingHandler);
document.removeEventListener('dragstart', this.dragstartHandler);
document.removeEventListener('dragend', this.dragendHandler);
this.unobserve();
}
}
</script>

View File

@ -22,103 +22,78 @@
<template>
<div class="c-fl-frame"
:class="{
'is-dragging': isDragging,
[frame.cssClass]: true
}"
@dragstart="initDrag"
@drag="continueDrag">
:style="{
'flex-basis': `${frame.size}%`
}">
<div class="c-frame c-fl-frame__drag-wrapper is-selectable is-moveable"
:class="{'no-frame': noFrame}"
draggable="true"
ref="frame"
v-if="frame.domainObject">
@dragstart="initDrag"
ref="frame">
<frame-header
v-if="index !== 0"
ref="dragObject"
class="c-fl-frame__header"
:domainObject="frame.domainObject">
</frame-header>
<object-view
class="c-fl-frame__object-view"
:object="frame.domainObject">
</object-view>
<subobject-view
v-if="item.domainObject.identifier"
:item="item">
</subobject-view>
<div class="c-fl-frame__size-indicator"
v-if="isEditing"
v-show="frame.height && frame.height < 100">
{{frame.height}}%
v-show="frame.size && frame.size < 100">
{{frame.size}}%
</div>
</div>
<drop-hint
v-show="isEditing && isDragging"
class="c-fl-frame__drop-hint"
:class="{'is-dragging': isDragging}"
@object-drop-to="dropHandler">
</drop-hint>
</div>
</template>
<script>
import ObjectView from '../../../ui/components/layout/ObjectView.vue';
import DropHint from './dropHint.vue';
import ResizeHandle from './resizeHandle.vue';
import FrameHeader from '../../../ui/components/utils/frameHeader.vue';
import SubobjectView from '../../displayLayout/components/SubobjectView.vue';
import isEditingMixin from '../mixins/isEditing';
export default {
inject: ['openmct', 'domainObject'],
props: ['frame', 'index', 'containerIndex', 'isEditing', 'isDragging'],
inject: ['openmct'],
props: ['frame', 'index', 'containerIndex'],
mixins: [isEditingMixin],
data() {
return {
noFrame: this.frame.noFrame
item: {domainObject: {}}
}
},
components: {
ObjectView,
DropHint,
ResizeHandle,
FrameHeader
SubobjectView
},
computed: {
noFrame() {
return this.frame.noFrame;
}
},
methods: {
setDomainObject(object) {
this.item.domainObject = object;
this.setSelection();
},
setSelection() {
let context = {
item: this.item.domainObject,
addContainer: this.addContainer,
type: 'frame',
frameId: this.frame.id
};
this.unsubscribeSelection = this.openmct.selection.selectable(this.$refs.frame, context, false);
},
initDrag(event) {
this.$emit('frame-drag-from', this.index);
},
dropHandler(event) {
this.$emit('frame-drop-to', this.index, event);
},
continueDrag(event) {
if (!this.isDragging) {
this.isDragging = true;
}
},
deleteFrame() {
this.$emit('delete-frame', this.index);
},
addContainer() {
this.$emit('add-container');
},
toggleFrame(v) {
this.noFrame = v;
event.dataTransfer.setData('frameid', this.frame.id);
event.dataTransfer.setData('containerIndex', this.containerIndex);
}
},
mounted() {
if (this.frame.domainObject.identifier) {
let context = {
item: this.frame.domainObject,
method: this.deleteFrame,
addContainer: this.addContainer,
type: 'frame',
index: this.index
}
this.unsubscribeSelection = this.openmct.selection.selectable(this.$refs.frame, context, false);
this.openmct.objects.observe(this.domainObject, `configuration.containers[${this.containerIndex}].frames[${this.index}].noFrame`, this.toggleFrame);
if (this.frame.domainObjectIdentifier) {
this.openmct.objects.get(this.frame.domainObjectIdentifier).then((object)=>{
this.setDomainObject(object);
});
}
},
beforeDestroy() {

View File

@ -23,16 +23,21 @@
<template>
<div class="c-fl-frame__resize-handle"
:class="[orientation]"
v-show="isEditing && !isDragging"
@mousedown="mousedown">
</div>
</template>
<script>
import isEditingMixin from '../mixins/isEditing';
export default {
props: ['orientation', 'index'],
mixins: [isEditingMixin],
data() {
return {
initialPos: 0
initialPos: 0,
isDragging: false,
}
},
methods: {
@ -47,7 +52,18 @@ export default {
mousemove(event) {
event.preventDefault();
let delta = this.getMousePosition(event) - this.getElSizeFromRect(this.$el);
let elSize, mousePos, delta;
if (this.orientation === 'horizontal') {
elSize = this.$el.getBoundingClientRect().x;
mousePos = event.clientX;
} else {
elSize = this.$el.getBoundingClientRect().y;
mousePos = event.clientY;
}
delta = mousePos - elSize;
this.$emit('move', this.index, delta, event);
},
mouseup(event) {
@ -56,20 +72,20 @@ export default {
document.body.removeEventListener('mousemove', this.mousemove);
document.body.removeEventListener('mouseup', this.mouseup);
},
getMousePosition(event) {
if (this.orientation === 'horizontal') {
return event.clientX;
} else {
return event.clientY;
}
},
getElSizeFromRect(el) {
if (this.orientation === 'horizontal') {
return el.getBoundingClientRect().x;
} else {
return el.getBoundingClientRect().y;
}
setDragging(event) {
this.isDragging = true;
},
unsetDragging(event) {
this.isDragging = false;
}
},
mounted() {
document.addEventListener('dragstart', this.setDragging);
document.addEventListener('dragend', this.unsetDragging);
},
destroyed() {
document.removeEventListener('dragstart', this.setDragging);
document.removeEventListener('dragend', this.unsetDragging);
}
}
</script>

View File

@ -46,7 +46,7 @@ define([
},
provide: {
openmct,
domainObject
layoutObject: domainObject
},
el: element,
template: '<flexible-layout-component></flexible-layout-component>'

View File

@ -0,0 +1,19 @@
export default {
inject: ['openmct'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
mounted() {
this.openmct.editor.on('isEditing', this.toggleEditing);
},
destroyed() {
this.openmct.editor.off('isEditing', this.toggleEditing);
},
methods: {
toggleEditing(value) {
this.isEditing = value;
}
}
};

View File

@ -22,10 +22,12 @@
define([
'./flexibleLayoutViewProvider',
'./utils/container'
'./utils/container',
'./toolbarProvider'
], function (
FlexibleLayoutViewProvider,
Container
Container,
ToolBarProvider
) {
return function plugin() {
@ -45,119 +47,9 @@ define([
}
});
openmct.toolbars.addProvider({
name: "Flexible Layout Toolbar",
key: "flex-layout",
description: "A toolbar for objects inside a Flexible Layout.",
forSelection: function (selection) {
let context = selection[0].context;
let toolbar = ToolBarProvider.default(openmct);
return (openmct.editor.isEditing() && context && context.type &&
(context.type === 'flexible-layout' || context.type === 'container' || context.type === 'frame'));
},
toolbar: function (selection) {
let primary = selection[0],
parent = selection[1],
deleteFrame,
toggleContainer,
deleteContainer,
addContainer,
toggleFrame,
separator;
addContainer = {
control: "button",
domainObject: parent ? parent.context.item : primary.context.item,
method: parent ? parent.context.addContainer : primary.context.addContainer,
key: "add",
icon: "icon-plus-in-rect",
title: 'Add Container'
};
separator = {
control: "separator",
domainObject: selection[0].context.item,
key: "separator"
};
toggleContainer = {
control: 'toggle-button',
key: 'toggle-layout',
domainObject: parent ? parent.context.item : primary.context.item,
property: 'configuration.rowsLayout',
options: [
{
value: false,
icon: 'icon-columns',
title: 'Columns'
},
{
value: true,
icon: 'icon-rows',
title: 'Rows'
}
]
};
if (primary.context.type === 'frame') {
deleteFrame = {
control: "button",
domainObject: primary.context.item,
method: primary.context.method,
key: "remove",
icon: "icon-trash",
title: "Remove Frame"
};
toggleFrame = {
control: "toggle-button",
domainObject: parent.context.item,
property: `configuration.containers[${parent.context.index}].frames[${primary.context.index}].noFrame`,
options: [
{
value: true,
icon: 'icon-frame-hide',
title: "Hide frame"
},
{
value: false,
icon: 'icon-frame-show',
title: "Show frame"
}
]
};
} else if (primary.context.type === 'container') {
deleteContainer = {
control: "button",
domainObject: primary.context.item,
method: primary.context.method,
key: "remove",
icon: "icon-trash",
title: "Remove Container"
};
} else if (primary.context.type === 'flexible-layout') {
addContainer = {
control: "button",
domainObject: primary.context.item,
method: primary.context.addContainer,
key: "add",
icon: "icon-plus-in-rect",
title: 'Add Container'
};
}
let toolbar = [toggleContainer, addContainer, toggleFrame, separator, deleteFrame, deleteContainer];
return toolbar.filter(button => button !== undefined);
}
});
openmct.toolbars.addProvider(toolbar);
};
};
});

View File

@ -0,0 +1,215 @@
/*****************************************************************************
* 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.
*****************************************************************************/
function ToolbarProvider(openmct) {
return {
name: "Flexible Layout Toolbar",
key: "flex-layout",
description: "A toolbar for objects inside a Flexible Layout.",
forSelection: function (selection) {
let context = selection[0].context;
return (openmct.editor.isEditing() && context && context.type &&
(context.type === 'flexible-layout' || context.type === 'container' || context.type === 'frame'));
},
toolbar: function (selection) {
let primary = selection[0],
secondary = selection[1],
tertiary = selection[2],
deleteFrame,
toggleContainer,
deleteContainer,
addContainer,
toggleFrame,
separator;
separator = {
control: "separator",
domainObject: selection[0].context.item,
key: "separator"
};
toggleContainer = {
control: 'toggle-button',
key: 'toggle-layout',
domainObject: secondary ? secondary.context.item : primary.context.item,
property: 'configuration.rowsLayout',
options: [
{
value: false,
icon: 'icon-columns',
title: 'Columns'
},
{
value: true,
icon: 'icon-rows',
title: 'Rows'
}
]
};
if (primary.context.type === 'frame') {
let frameId = primary.context.frameId;
let layoutObject = tertiary.context.item;
let containers = layoutObject
.configuration
.containers;
let container = containers
.filter(c => c.frames.some(f => f.id === frameId))[0];
let frame = container
.frames
.filter((f => f.id === frameId))[0];
let containerIndex = containers.indexOf(container);
let frameIndex = container.frames.indexOf(frame);
deleteFrame = {
control: "button",
domainObject: primary.context.item,
method: function () {
let deleteFrameAction = tertiary.context.deleteFrame;
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
buttons: [
{
label: 'Ok',
emphasis: 'true',
callback: function () {
deleteFrameAction(primary.context.frameId);
prompt.dismiss();
}
},
{
label: 'Cancel',
callback: function () {
prompt.dismiss();
}
}
]
});
},
key: "remove",
icon: "icon-trash",
title: "Remove Frame"
};
toggleFrame = {
control: "toggle-button",
domainObject: secondary.context.item,
property: `configuration.containers[${containerIndex}].frames[${frameIndex}].noFrame`,
options: [
{
value: true,
icon: 'icon-frame-hide',
title: "Hide frame"
},
{
value: false,
icon: 'icon-frame-show',
title: "Show frame"
}
]
};
addContainer = {
control: "button",
domainObject: tertiary.context.item,
method: tertiary.context.addContainer,
key: "add",
icon: "icon-plus-in-rect",
title: 'Add Container'
};
} else if (primary.context.type === 'container') {
deleteContainer = {
control: "button",
domainObject: primary.context.item,
method: function () {
let removeContainer = secondary.context.deleteContainer,
containerId = primary.context.containerId;
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
message: `This action will permanently delete container ${containerIndex + 1} from this Flexible Layout`,
buttons: [
{
label: 'Ok',
emphasis: 'true',
callback: function () {
removeContainer(containerId);
prompt.dismiss();
}
},
{
label: 'Cancel',
callback: function () {
prompt.dismiss();
}
}
]
});
},
key: "remove",
icon: "icon-trash",
title: "Remove Container"
};
addContainer = {
control: "button",
domainObject: secondary.context.item,
method: secondary.context.addContainer,
key: "add",
icon: "icon-plus-in-rect",
title: 'Add Container'
};
} else if (primary.context.type === 'flexible-layout') {
addContainer = {
control: "button",
domainObject: primary.context.item,
method: primary.context.addContainer,
key: "add",
icon: "icon-plus-in-rect",
title: 'Add Container'
};
}
let toolbar = [
toggleContainer,
addContainer,
toggleFrame ? separator: undefined,
toggleFrame,
deleteFrame || deleteContainer ? separator: undefined,
deleteFrame,
deleteContainer
];
return toolbar.filter(button => button !== undefined);
}
};
}
export default ToolbarProvider;

View File

@ -1,13 +1,10 @@
import Frame from './frame';
import uuid from 'uuid';
class Container {
constructor (width) {
this.frames = [new Frame({}, '', 'c-fl-frame--first-in-container')];
this.width = width;
}
addFrame(frameObject) {
this.frames.push(frameObject);
constructor (size) {
this.id = uuid();
this.frames = [];
this.size = size;
}
}

View File

@ -1,8 +1,11 @@
import uuid from 'uuid';
class Frame {
constructor(domainObject, height, cssClass) {
this.domainObject = domainObject;
this.height = height;
this.cssClass = cssClass ? cssClass : '';
constructor(domainObjectIdentifier, size) {
this.id = uuid();
this.domainObjectIdentifier = domainObjectIdentifier;
this.size = size;
this.noFrame = false;
}
}

View File

@ -429,6 +429,8 @@ export default {
}
},
start: function (event) {
event.preventDefault(); // stop from firing drag event
this.startPosition = this.getPosition(event);
document.body.addEventListener('mousemove', this.updatePosition);
document.body.addEventListener('mouseup', this.end);

View File

@ -104,10 +104,10 @@ export default {
parentObject.composition.push(childObject.identifier);
this.openmct.objects.mutate(parentObject, 'composition', parentObject.composition);
}
}
event.preventDefault();
event.stopPropagation();
event.preventDefault();
event.stopPropagation();
}
}
}
}

View File

@ -9,8 +9,10 @@ export default {
}
},
mounted() {
// TODO: handle mobile contet menu listeners.
// TODO: handle mobile context menu listeners.
this.$el.addEventListener('contextmenu', this.showContextMenu);
this.objectPath.forEach((o, i) => {
let removeListener = this.openmct.objects.observe(
o,

View File

@ -1,56 +0,0 @@
<template>
<div class="c-frame-header">
<div class="c-frame-header__start">
<div class="c-frame-header__name" :class="cssClass">{{ domainObject.name }}</div>
<div class="c-frame-header__context-actions c-disclosure-button"></div>
</div>
<div class="c-frame-header__end">
<div class="c-button icon-expand local-controls--hidden"></div>
</div>
</div>
</template>
<style lang="scss">
@import '~styles/sass-base';
.c-frame-header {
display: flex;
align-items: center;
&__start,
&__end {
display: flex;
flex: 1 1 auto;
}
&__end {
justify-content: flex-end;
}
&__name {
display: flex;
&:before {
margin-right: $interiorMarginSm;
}
}
.no-frame & {
display: none;
}
}
</style>
<script>
export default {
inject: ['openmct'],
props:['domainObject'],
data () {
let type = this.openmct.types.get(this.domainObject.type);
return {
cssClass: type.definition.cssClass
}
}
}
</script>