Flexible Layout (#2201)

* first cut of flexible layout

* better drag handling

* add drop targets to every row

* enable drag and drop between columns and rows

* enable persistance

* add editing capability

* chage rows to frames and columns to containers, switch draggable to whole frame object

* Merge latest, resolve conflicts.

Need to just apply these changes to Deep's branch and push

* enhancements to drag targets

* WIP in flexibleLayout, container.vue files

- Refined classes and markup;
- min-width changed to flex-basis;
- Added toggle direction button;

* Significant progress but still WIP

- Refined classes and markup;
- Layout toggling working;
- Add Container working properly;
- TODOs: fix sizing in empty container, fix bordering, more refinements;

* add resizing of frames - still wip

* Significant enhancements

- Moved all CSS into flexibleLayout.vue;
- Layout now improved for empty container and drop hints;
- Proportional sizing now better for frames and containers;

* Resize handle WIP

* abstract splitter and logic into self contained component that will emit an event when mouse is moving

* Resize handle WIP

- Minor tweak to handle padding and hover;

* add container resize todo persist

* persist container resize

* add frame header, fix column resize on last column

* Refinements to resize-handle

- Fixed sizing;
- Transition on hover;
- TODOs: needs is-dragging to maintain hover style while dragging;

* fix drop hints showing after drop

* move header

* improve mouse move gesture

* Added frame size indicator

* add snapto functionality

* Refined container and frame size indicators

- Also added overflow handling to l-grid-view

* improve resizing logic

* add selection on frames

* Various resizing-frames related

- Fixed overflow - now frame widths can be collapsed to 5% minimum;
- Sizing indicators refined, better positioning and layout;
- Added grippy drag indicators to column heads;
- TODOs: add column head cursors and hover effects, hide indicators
when not in edit mode, handle nested layout and flex layouts while
editing

* Selecting and emtpy layout messaging

- Better empty layout message;
- Moved s-selected to proper element in c-fl-frame;

* Drop-hint and sizing related various

- Drop-hints for first placeholder container now display;
- Drop-hints moved into drag-wrapper;

* add delete frame

* Editing various

- Adjust Snow theme constants related to editing;
- Changed delete message wording;

* Updated icon and added description

* add toggle and remove container to toolbar

* miscellaneous cleanup

* add container button to toolbar

* improve toolbar

* code cleanup in plugin.js

* Various icons, toolbar separator

- Copied in c-toolbar__separator and associated changes in _controls
from Pegah's layout_alpha branch - may have conflicts later.
- Added separator to FL toolbar;
- Updated icons for grippy-ew, toolbar icons;

* add check for empty containers"

* logic to resize frames on drop

* fix delete frame and persisting toolbar

* Significant changes to edit / selection styling

- Both Flexible and fixed Display Layouts addressed;
- Both themes addressed;
- Changed drop-hint icon to icon-plus;

* add correct icons to frame header and fix toolbars showing up in wrong views

* Moving and resizing various

- Cursors;
- Grippy added to frame resize-handle, WIP!;

* add container reordering

* add frame/no frame support to toolbar'

* fix regression of resize handles showing after last frame in container

* force selection of flexible-layout when editing is first clicked, to apply correct toolbar

* make changes to simplify toolbar

* Modified sizing algorithm slightly

* make changes reviewer requested

* fix regression that causes top drop hint to not show

* remove unused variables and bind events to vue

* unsub selection before destroy
This commit is contained in:
Deep Tailor 2018-11-08 17:17:14 -08:00 committed by Pete Richards
parent d13d59bfa0
commit 1069a45cfc
19 changed files with 1530 additions and 38 deletions

View File

@ -78,6 +78,7 @@
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.FolderView());
openmct.install(openmct.plugins.Tabs());
openmct.install(openmct.plugins.FlexibleLayout());
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
openmct.time.timeSystem('utc');
openmct.start();

View File

@ -0,0 +1,214 @@
/*****************************************************************************
* 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-container"
:style="[{'flex-basis': size}]"
:class="{'is-empty': frames.length === 1}">
<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>
</div>
<div class="c-fl-container__frames-holder">
<div class="u-contents"
v-for="(frame, i) in frames"
:key="i">
<frame-component
class="c-fl-container__frame"
:style="{
'flex-basis': `${frame.height}%`
}"
: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">
</frame-component>
<resize-handle
v-if="i !== 0 && (i !== frames.length - 1)"
v-show="isEditing"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
@init-move="startFrameResizing"
@move="frameResizing"
@end-move="endFrameResizing">
</resize-handle>
</div>
</div>
</div>
</template>
<script>
import FrameComponent from './frame.vue';
import Frame from '../utils/frame';
import ResizeHandle from './resizeHandle.vue';
const SNAP_TO_PERCENTAGE = 1;
const MIN_FRAME_SIZE = 5;
export default {
inject:['openmct', 'domainObject'],
props: ['size', 'frames', 'index', 'isEditing', 'isDragging', 'rowsLayout'],
components: {
FrameComponent,
ResizeHandle
},
data() {
return {
initialPos: 0,
frameIndex: 0,
maxMoveSize: 0
}
},
methods: {
frameDragFrom(frameIndex) {
this.$emit('frame-drag-from', this.index, frameIndex);
},
frameDropTo(frameIndex, event) {
let domainObject = event.dataTransfer.getData('domainObject'),
frameObject;
if (domainObject) {
frameObject = new Frame(JSON.parse(domainObject));
}
this.$emit('frame-drop-to', this.index, frameIndex, frameObject);
},
startFrameResizing(index) {
let beforeFrame = this.frames[index],
afterFrame = this.frames[index + 1];
this.maxMoveSize = beforeFrame.height + afterFrame.height;
},
frameResizing(index, delta, event) {
let percentageMoved = (delta / this.getElSize(this.$el))*100,
beforeFrame = this.frames[index],
afterFrame = this.frames[index + 1];
beforeFrame.height = this.snapToPercentage(beforeFrame.height + percentageMoved);
afterFrame.height = this.snapToPercentage(afterFrame.height - percentageMoved);
},
endFrameResizing(index, event) {
this.persist();
},
getElSize(el) {
if (this.rowsLayout) {
return el.offsetWidth;
} else {
return el.offsetHeight;
}
},
getFrameSize(size) {
if (size < MIN_FRAME_SIZE) {
return MIN_FRAME_SIZE
} else if (size > (this.maxMoveSize - MIN_FRAME_SIZE)) {
return (this.maxMoveSize - MIN_FRAME_SIZE);
} else {
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');
}
},
mounted() {
let context = {
item: this.domainObject,
method: this.deleteContainer,
addContainer: this.addContainer,
index: this.index,
type: 'container'
}
this.unsubscribeSelection = this.openmct.selection.selectable(this.$el, context, false);
},
beforeDestroy() {
this.unsubscribeSelection();
}
}
</script>

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* 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>
<div class="c-drop-hint c-drop-hint--always-show"
:class="{'is-mouse-over': isMouseOver}"
@dragenter="dragenter"
@dragleave="dragleave"
@drop="dropHandler">
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
export default {
props:['index'],
data() {
return {
isMouseOver: false
}
},
methods: {
dragenter() {
this.isMouseOver = true;
},
dragleave() {
this.isMouseOver = false;
},
dropHandler(event) {
this.$emit('object-drop-to', event, this.index);
}
}
}
</script>

View File

@ -0,0 +1,684 @@
<template>
<div class="c-fl">
<div class="c-fl__empty"
v-if="areAllContainersEmpty()">
<span class="c-fl__empty-message">This Flexible Layout is currently empty</span>
</div>
<div class="c-fl__container-holder"
:class="{
'c-fl--rows': rowsLayout === true
}">
<div class="u-contents"
v-for="(container, index) in containers"
:key="index">
<drop-hint
style="flex-basis: 15px;"
v-if="index === 0 && containers.length > 1"
v-show="isContainerDragging"
:index="-1"
@object-drop-to="containerDropTo">
</drop-hint>
<container-component
class="c-fl__container"
ref="containerComponent"
:index="index"
:size="`${Math.round(container.width)}%`"
:frames="container.frames"
:isEditing="isEditing"
:isDragging="isDragging"
: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">
</container-component>
<resize-handle
v-if="index !== (containers.length - 1)"
v-show="isEditing"
:index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'"
@init-move="startContainerResizing"
@move="containerResizing"
@end-move="endContainerResizing">
</resize-handle>
<drop-hint
style="flex-basis: 15px;"
v-if="containers.length > 1"
v-show="isContainerDragging"
:index="index"
@object-drop-to="containerDropTo">
</drop-hint>
</div>
</div>
</div>
</template>
<style lang="scss">
@import '~styles/sass-base';
.c-fl {
@include abs();
display: flex;
flex-direction: column; // TEMP: only needed to support temp-toolbar element
> * + * { margin-top: $interiorMargin; }
.temp-toolbar {
flex: 0 0 auto;
}
&__container-holder {
display: flex;
flex: 1 1 100%; // Must needs to be 100% to work
// Columns by default
flex-direction: row;
> * + * { margin-left: 1px; }
&[class*='--rows'] {
//@include test(blue, 0.1);
flex-direction: column;
> * + * {
margin-left: 0;
margin-top: 1px;
}
}
}
&__empty {
@include abs();
background: rgba($colorBodyFg, 0.1);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
> * {
font-style: italic;
opacity: 0.5;
}
}
}
.c-fl-container {
/***************************************************** CONTAINERS */
$headerSize: 16px;
border: 1px solid transparent;
display: flex;
flex-direction: column;
overflow: auto;
// flex-basis is set with inline style in code, controls size
flex-grow: 1;
flex-shrink: 1;
&__header {
// Only displayed when editing
background: $editSelectableColor;
color: $editSelectableColorFg;
cursor: move;
display: flex;
align-items: center;
flex: 0 0 $headerSize;
&:before {
// Drag grippy
font-size: 0.8em;
opacity: 0.5;
position: absolute;
left: 50%; top: 50%;
transform-origin: center;
transform: translate(-50%, -50%);
}
}
&__size-indicator {
position: absolute;
display: inline-block;
right: $interiorMargin;
}
&__frames-holder {
display: flex;
flex: 1 1 100%; // Must be 100% to work
flex-direction: column; // Default
align-content: stretch;
align-items: stretch;
overflow: hidden; // This sucks, but doing in the short-term
}
.is-editing & {
//background: $editCanvasColorBg;
border-color: $editSelectableColor;
&:hover {
border-color: $editSelectableColorHov;
}
&[s-selected] {
border-color: $editSelectableColorSelected;
.c-fl-container__header {
background: $editSelectableColorSelected;
color: $editSelectableColorSelectedFg;
}
}
}
/****** THEIR FRAMES */
// Frames get styled here because this is particular to their presence in this layout type
.c-fl-frame {
@include browserPrefix(margin-collapse, collapse);
margin: 1px;
//&__drag-wrapper {
// border: 1px solid $colorInteriorBorder; // Now handled by is-selectable
//}
}
/****** ROWS LAYOUT */
.c-fl--rows & {
// Layout is rows
flex-direction: row;
&__header {
flex-basis: $headerSize;
overflow: hidden;
&:before {
// Drag grippy
transform: rotate(90deg) translate(-50%, 50%);
}
}
&__size-indicator {
right: 0;
top: $interiorMargin;
transform-origin: top right;
transform: rotate(-90deg) translateY(-100%);
}
&__frames-holder {
flex-direction: row;
}
}
}
.c-fl-frame {
$sizeIndicatorM: 16px;
$dropHintSize: 15px;
display: flex;
justify-content: stretch;
align-items: stretch;
flex: 1 1;
flex-direction: column;
overflow: hidden; // Needed to allow frames to collapse when sized down
&__drag-wrapper {
flex: 1 1 auto;
overflow: auto;
.is-editing & {
> * {
pointer-events: none;
}
}
}
&__size-indicator {
$size: 35px;
@include ellipsize();
background: $colorBtnBg;
border-top-left-radius: $controlCr;
color: $colorBtnFg;
display: inline-block;
padding: $interiorMarginSm 0;
position: absolute;
pointer-events: none;
text-align: center;
width: $size;
z-index: 2;
// Changed when layout is different, see below
border-top-right-radius: $controlCr;
bottom: 1px;
right: $sizeIndicatorM;
}
&__drop-hint {
flex: 0 0 $dropHintSize;
.c-drop-hint {
border-radius: $smallCr;
}
}
&__resize-handle {
$size: 2px;
$margin: 3px;
$marginHov: 0;
$grippyThickness: $size + 6;
$grippyLen: $grippyThickness * 2;
display: flex;
flex-direction: column;
flex: 0 0 ($margin * 2) + $size;
transition: $transOut;
&:before {
// The visible resize line
background: $editColor;
content: '';
display: block;
flex: 1 1 auto;
min-height: $size; min-width: $size;
}
&:after {
// Grippy element
/*background: deeppink;*/
$c: black;
$a: 0.9;
$d: 5px;
background: $editColor;
color: $editColorBg;
border-radius: $smallCr;
content: $glyph-icon-grippy-ew;
font-family: symbolsfont;
font-size: 0.8em;
display: inline-block;
padding: 10px 0;
position: absolute;
left: 50%; top: 50%;
text-align: center;
transform-origin: center center;
transform: translate(-50%, -50%);
z-index: 10;
}
&.vertical {
padding: $margin $size;
&:hover{
// padding: $marginHov 0;
cursor: row-resize;
}
&:after {
transform: rotate(90deg) translate(-50%, -50%);
//top: $margin + $size - 2px;
//left: 50%;
// transform: translateX(-50%);
/*width: $grippyLen;
height: $grippyThickness;*/
}
}
&.horizontal {
padding: $size $margin;
&:hover{
// padding: 0 $marginHov;
cursor: col-resize;
}
&:after {
//left: $margin + $size - 2px;
//top: 50%;
//transform: translateY(-50%);
/* height: $grippyLen;
width: $grippyThickness;*/
}
}
&:hover {
transition: $transOut;
&:before {
// The visible resize line
background: $editColorHov;
}
}
}
// Hide the resize-handles in first and last c-fl-frame elements
&:first-child,
&:last-child {
.c-fl-frame__resize-handle {
display: none;
}
}
.c-fl--rows & {
flex-direction: row;
&__size-indicator {
border-bottom-left-radius: $controlCr;
border-top-right-radius: 0;
bottom: $sizeIndicatorM;
right: 1px;
}
}
&--first-in-container {
border: none;
flex: 0 0 0;
.c-fl-frame__drag-wrapper {
display: none;
}
&.is-dragging {
flex-basis: $dropHintSize;
}
}
.is-empty & {
&.c-fl-frame--first-in-container {
flex: 1 1 auto;
}
&__drop-hint {
flex: 1 1 100%;
margin: 0;
}
}
}
</style>
<script>
import ContainerComponent from './container.vue';
import Container from '../utils/container';
import ResizeHandle from './resizeHandle.vue';
import DropHint from './dropHint.vue';
const SNAP_TO_PERCENTAGE = 1,
MIN_CONTAINER_SIZE = 5;
export default {
inject: ['openmct', 'domainObject'],
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
}
},
computed: {
layoutDirectionStr() {
if (this.rowsLayout) {
return 'Rows'
} else {
return 'Columns'
}
}
},
methods: {
areAllContainersEmpty() {
return !!!this.containers.filter(container => container.frames.length > 1).length;
},
addContainer() {
let newSize = 100/(this.containers.length+1);
let container = new Container(newSize)
this.recalculateContainerSize(newSize);
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);
this.persist();
},
persist(index){
if (index) {
this.openmct.objects.mutate(this.domainObject, `.configuration.containers[${index}]`, this.containers[index]);
} else {
this.openmct.objects.mutate(this.domainObject, '.configuration.containers', this.containers);
}
},
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;
},
containerResizing(index, delta, event) {
let percentageMoved = (delta/this.getElSize(this.$el))*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));
},
endContainerResizing(event) {
this.persist();
},
getElSize(el) {
if (this.rowsLayout) {
return el.offsetHeight;
} else {
return el.offsetWidth;
}
},
getContainerSize(size) {
if (size < MIN_CONTAINER_SIZE) {
return MIN_CONTAINER_SIZE
} else if (size > (this.maxMoveSize - MIN_CONTAINER_SIZE)) {
return (this.maxMoveSize - MIN_CONTAINER_SIZE);
} else {
return size;
}
},
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 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();
}
},
mounted() {
let context = {
item: this.domainObject,
addContainer: this.addContainer,
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);
},
beforeDestroy() {
this.unsubscribeSelection();
this.openmct.editor.off('isEditing', this.isEditingHandler);
document.removeEventListener('dragstart', this.dragstartHandler);
document.removeEventListener('dragend', this.dragendHandler);
}
}
</script>

View File

@ -0,0 +1,129 @@
/*****************************************************************************
* 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-frame"
:class="{
'is-dragging': isDragging,
[frame.cssClass]: true
}"
@dragstart="initDrag"
@drag="continueDrag">
<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">
<frame-header
v-if="index !== 0"
ref="dragObject"
:domainObject="frame.domainObject">
</frame-header>
<object-view
class="c-object-view"
:object="frame.domainObject">
</object-view>
<div class="c-fl-frame__size-indicator"
v-if="isEditing"
v-show="frame.height && frame.height < 100">
{{frame.height}}%
</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';
export default {
inject: ['openmct', 'domainObject'],
props: ['frame', 'index', 'containerIndex', 'isEditing', 'isDragging'],
data() {
return {
noFrame: this.frame.noFrame
}
},
components: {
ObjectView,
DropHint,
ResizeHandle,
FrameHeader
},
methods: {
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;
}
},
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);
}
},
beforeDestroy() {
if (this.unsubscribeSelection) {
this.unsubscribeSelection();
}
}
}
</script>

View File

@ -0,0 +1,75 @@
/*****************************************************************************
* 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-frame__resize-handle"
:class="[orientation]"
@mousedown="mousedown">
</div>
</template>
<script>
export default {
props: ['orientation', 'index'],
data() {
return {
initialPos: 0
}
},
methods: {
mousedown(event) {
event.preventDefault();
this.$emit('init-move', this.index);
document.body.addEventListener('mousemove', this.mousemove);
document.body.addEventListener('mouseup', this.mouseup);
},
mousemove(event) {
event.preventDefault();
let delta = this.getMousePosition(event) - this.getElSizeFromRect(this.$el);
this.$emit('move', this.index, delta, event);
},
mouseup(event) {
this.$emit('end-move', event);
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;
}
},
}
}
</script>

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define([
'./components/flexibleLayout.vue',
'vue'
], function (
FlexibleLayoutComponent,
Vue
) {
function FlexibleLayoutViewProvider(openmct) {
return {
key: 'flexible-layout',
name: 'FlexibleLayout',
cssClass: 'icon-layout-view',
canView: function (domainObject) {
return domainObject.type === 'flexible-layout';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
components: {
FlexibleLayoutComponent: FlexibleLayoutComponent.default
},
provide: {
openmct,
domainObject
},
el: element,
template: '<flexible-layout-component></flexible-layout-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return FlexibleLayoutViewProvider;
});

View File

@ -0,0 +1,163 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define([
'./flexibleLayoutViewProvider',
'./utils/container'
], function (
FlexibleLayoutViewProvider,
Container
) {
return function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct));
openmct.types.addType('flexible-layout', {
name: "Flexible Layout",
creatable: true,
description: "A fluid, flexible layout canvas that can display multiple objects in rows or columns.",
cssClass: 'icon-flexible-layout',
initialize: function (domainObject) {
domainObject.configuration = {
containers: [new Container.default(50), new Container.default(50)],
rowsLayout: false
};
}
});
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;
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: false,
icon: 'icon-frame-hide',
title: "Hide frame"
},
{
value: true,
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);
}
});
};
};
});

View File

@ -0,0 +1,14 @@
import Frame from './frame';
class Container {
constructor (width) {
this.frames = [new Frame({}, '', 'c-fl-frame--first-in-container')];
this.width = width;
}
addFrame(frameObject) {
this.frames.push(frameObject);
}
}
export default Container;

View File

@ -0,0 +1,10 @@
class Frame {
constructor(domainObject, height, cssClass) {
this.domainObject = domainObject;
this.height = height;
this.cssClass = cssClass ? cssClass : '';
this.noFrame = false;
}
}
export default Frame;

View File

@ -33,6 +33,7 @@
.l-grid-view {
display: flex;
flex-flow: column nowrap;
overflow: auto;
&__item {
flex: 0 0 auto;

View File

@ -37,6 +37,7 @@ define([
'./notebook/plugin',
'./displayLayout/plugin',
'./folderView/plugin',
'./flexibleLayout/plugin',
'./tabs/plugin',
'../../platform/features/fixed/plugin'
], function (
@ -56,6 +57,7 @@ define([
Notebook,
DisplayLayoutPlugin,
FolderView,
FlexibleLayout,
Tabs,
FixedView
) {
@ -171,6 +173,7 @@ define([
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
plugins.FixedView = FixedView;
plugins.FlexibleLayout = FlexibleLayout;
return plugins;
});

View File

@ -88,17 +88,35 @@ $colorTimeSubtle: darken($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
// Edit Colors
/************************************************** EDITING */
// Base Colors
$dlSpread: 20%;
$editColor: #00c7c3;
$editColorFg: $colorBodyFg;
$editColorBg: darken($editColor, $dlSpread);
$editColorFg: lighten($editColor, $dlSpread);
$editColorHov: lighten($editColor, 20%);
// Canvas
$editCanvasColorBg: #002524;
$editCanvasColorGrid: darken($editCanvasColorBg, 2%);
// Selectable
$editSelectableColor: #006563;
$editSelectableColorFg: lighten($editSelectableColor, 20%);
$editSelectableColorHov: lighten($editSelectableColor, 10%);
// Selectable selected
$editSelectableColorSelected: $editSelectableColorHov;
$editSelectableColorSelectedFg: lighten($editSelectableColorSelected, 30%);
$editSelectableColorFg: darken($editSelectableColor, 40%);
$editSelectableBorder: 1px dotted $editSelectableColor;
$editSelectableBorderHov: 1px dotted $editColor;
$editSelectableBorderSelected: 1px solid $editColor;
$editMoveableSelectedShdw: rgba($editColor, 0.5) 0 0 10px;
$editBorderDrilledIn: 1px dashed #9971ff;
$colorGridLines: rgba($editColor, 0.2);
/************************************************** BROWSING */
$browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2);
$browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px;
$browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6);
$editBorderSelectable: 1px dotted rgba($editColor, 1);
$editBorderSelectableHov: 1px dashed rgba($editColor, 1);
$editBorderSelected: 1px solid rgba($editColor, 0.7);
$editBorderDrilledIn: 1px dashed #ff4d9a;
$colorGridLines: rgba($editColor, 0.2);
// Icons
$colorIconAlias: #4af6f3;

View File

@ -88,17 +88,34 @@ $colorTimeSubtle: lighten($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
// Edit Colors
/************************************************** EDITING */
// Base Colors
$dlSpread: 20%;
$editColor: #00c7c3;
$editColorFg: $colorBodyFg;
$editColorBg: darken($editColor, $dlSpread);
$editColorFg: lighten($editColor, $dlSpread);
$editColorHov: lighten($editColor, 20%);
// Canvas
$editCanvasColorBg: #e6ffff;
$editCanvasColorGrid: darken($editCanvasColorBg, 10%);
// Selectable
$editSelectableColor: darken($colorBodyBg, 10%);
$editSelectableColorFg: darken($editSelectableColor, 20%);
$editSelectableColorHov: darken($editSelectableColor, 10%); //darken($colorBodyBg, 20%);
// Selectable selected
$editSelectableColorSelected: $editColor; //$editSelectableColorHov;
$editSelectableColorSelectedFg: lighten($editSelectableColorSelected, 50%);
$editSelectableColorFg: darken($editSelectableColor, 40%);
$editSelectableBorder: 1px dotted $editSelectableColor;
$editSelectableBorderHov: 1px dotted $editColor;
$editSelectableBorderSelected: 1px solid $editColor;
$editMoveableSelectedShdw: rgba($editColor, 0.5) 0 0 10px;
$editBorderDrilledIn: 1px dashed #9971ff;
/************************************************** BROWSING */
$browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2);
$browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px;
$browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6);
$editBorderSelectable: 1px dotted rgba($editColor, 1);
$editBorderSelectableHov: 1px dashed rgba($editColor, 1);
$editBorderSelected: 1px solid $editColor;
$editBorderDrilledIn: 1px dashed #ff4d9a;
$colorGridLines: rgba($editColor, 0.2);
// Icons
$colorIconAlias: #4af6f3;
@ -125,9 +142,9 @@ $colorClickIcon: $colorKey;
$colorClickIconBgHov: rgba($colorKey, 0.2);
$colorClickIconFgHov: $colorKeyHov;
$colorDropHint: $colorKey;
$colorDropHintBg: darken($colorDropHint, 10%);
$colorDropHintBgHov: $colorDropHint;
$colorDropHintFg: lighten($colorDropHint, 40%);
$colorDropHintBg: lighten($colorDropHint, 30%);
$colorDropHintBgHov: lighten($colorDropHint, 40%);
$colorDropHintFg: lighten($colorDropHint, 0);
// Menus
$colorMenuBg: lighten($colorBodyBg, 10%);

View File

@ -512,13 +512,15 @@ input[type=number]::-webkit-outer-spin-button {
transition: $transOut;
z-index: 50;
opacity: 0; // Must use this (rather than display: none) to enable transition effects
pointer-events: none;
&:not(.c-drop-hint--always-show) {
opacity: 0; // Must use this (rather than display: none) to enable transition effects
pointer-events: none;
}
&:before {
$h: 80%;
$mh: 50px;
background: $bg-icon-activity; // TODO: change to $bg-icon-plus
$mh: 25px;
background: $bg-icon-plus;
background-size: contain;
background-position: center center;
background-repeat: no-repeat;
@ -529,13 +531,15 @@ input[type=number]::-webkit-outer-spin-button {
max-height: $mh; max-width: $mh;
}
.is-dragging & {
.is-dragging &,
&.is-dragging {
pointer-events: inherit;
transition: $transIn;
opacity: 0.8;
}
.is-mouse-over & {
.is-mouse-over &,
&.is-mouse-over {
transition: $transIn;
background-color: $colorDropHintBgHov;
opacity: 0.9;

View File

@ -235,8 +235,8 @@ body.desktop .has-local-controls {
.c-grid {
pointer-events: none;
&__x { @include bgTicks($colorGridLines, 'x'); }
&__y { @include bgTicks($colorGridLines, 'y'); }
&__x { @include bgTicks($editCanvasColorGrid, 'x'); }
&__y { @include bgTicks($editCanvasColorGrid, 'y'); }
}
/*************************** SELECTION */
@ -249,15 +249,15 @@ body.desktop .has-local-controls {
/**************************** EDITING */
.is-editing {
*:not(.is-drilled-in).is-selectable {
border: $editBorderSelectable;
border: $editSelectableBorder;
&:hover {
border: $editBorderSelectableHov;
border: $editSelectableBorderHov;
}
&[s-selected],
&.is-selected {
border: $editBorderSelected;
border: $editSelectableBorderSelected;
> .c-frame-edit {
display: block; // Show the editing rect and handles
@ -269,6 +269,10 @@ body.desktop .has-local-controls {
border: $editBorderDrilledIn;
}
*[s-selected] .is-moveable {
cursor: move;
}
.u-links {
// Applied in markup to objects that provide links. Disable while editing.
pointer-events: none;
@ -284,7 +288,7 @@ body.desktop .has-local-controls {
&__move {
@include abs();
box-shadow: rgba($editColor, 0.5) 0 0 10px;
box-shadow: $editMoveableSelectedShdw;
cursor: move;
z-index: $z;
}
@ -339,10 +343,11 @@ body.desktop .has-local-controls {
}
}
// TODO: move this into DisplayLayout and Fixed Position vue files respectively
.l-shell__main-container > .l-layout,
.l-shell__main-container > .c-object-view .l-fixed-position {
// Target the top-most layout container and color its background
background: rgba($editColor, 0.1);
background: $editCanvasColorBg;
}
// Layouts

View File

@ -128,10 +128,10 @@
}
@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) {
@include background-image(linear-gradient(-90deg,
background-image: linear-gradient(-90deg,
rgba($c, $a) 0%, rgba($c, $a) 50%,
transparent 50%, transparent 100%
));
);
background-repeat: repeat;
background-size: $d $d;
}

View File

@ -76,14 +76,18 @@ export default {
},
onDrop(event) {
let parentObject = this.currentObject;
let childObject = JSON.parse(event.dataTransfer.getData("domainObject"));
let d = event.dataTransfer.getData("domainObject");
if (this.openmct.composition.checkPolicy(parentObject, childObject)){
if (!this.openmct.editor.isEditing() && parentObject.type !== 'folder'){
this.openmct.editor.edit();
if (d) {
let childObject = JSON.parse(d);
if (this.openmct.composition.checkPolicy(parentObject, childObject)){
if (!this.openmct.editor.isEditing() && parentObject.type !== 'folder'){
this.openmct.editor.edit();
}
parentObject.composition.push(childObject.identifier);
this.openmct.objects.mutate(parentObject, 'composition', parentObject.composition);
}
parentObject.composition.push(childObject.identifier);
this.openmct.objects.mutate(parentObject, 'composition', parentObject.composition);
}
event.preventDefault();

View File

@ -0,0 +1,25 @@
<template>
<div class="c-frame__header">
<div class="c-frame__header__start">
<div class="c-frame__name" :class="cssClass">{{ domainObject.name }}</div>
<div class="c-frame__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>
<script>
export default {
inject: ['openmct'],
props:['domainObject'],
data () {
let type = this.openmct.types.get(this.domainObject.type);
return {
cssClass: type.definition.cssClass
}
}
}
</script>