Merge pull request #2829 from nasa/topic-conditionals-master-merge

Topic conditionals master merge
This commit is contained in:
Shefali Joshi 2020-03-31 14:25:16 -07:00 committed by GitHub
commit 3956cd1c06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 3781 additions and 1414 deletions

View File

@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@ -18,4 +18,4 @@
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.
-->
-->

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@ -18,4 +18,4 @@
* 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.
*****************************************************************************/
*****************************************************************************/

View File

@ -1,80 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
/
</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
</div>
<div class='status-line high'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div><div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Wed Dec 11 2019 13:15:10 GMT-0800 (Pacific Standard Time)
</div>
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
<script src="sorter.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 B

View File

@ -1,158 +0,0 @@
var addSorting = (function () {
"use strict";
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() { return document.querySelector('.coverage-summary'); }
// returns the thead element of the summary table
function getTableHeader() { return getTable().querySelector('thead tr'); }
// returns the tbody element of the summary table
function getTableBody() { return getTable().querySelector('tbody'); }
// returns the th element for nth column
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function (a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function (a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function () {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i =0 ; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function () {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData(cols);
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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="u-contents">
<div class="c-cdef__separator c-row-separator"></div>

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
.c-cs {
display: flex;
flex-direction: column;

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
.c-condition,
.c-test-datum {
@include discreteItem();

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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>
<li class="c-tree__item-h">
<div

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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="u-contents">
<div class="c-overlay__top-bar">

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
/********************************************* INSPECTOR STYLES TAB */
.c-inspect-styles {
> * + * {

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
export const TRIGGER = {
ANY: 'any',
ALL: 'all',

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
export const computeCondition = (resultMap, allMustBeTrue) => {
let result = false;
for (let key in resultMap) {

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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 _ from 'lodash';
export const OPERATIONS = [

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
export const getStyleProp = (key, defaultValue) => {
let styleProp = undefined;
switch(key) {

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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>
<a class="c-condition-widget"
:href="internalDomainObject.url"

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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.
*****************************************************************************/
.c-condition-widget {
$shdwSize: 3px;
@include userSelectNone();

View File

@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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 StyleRuleManager from "@/plugins/condition/StyleRuleManager";
export default {

View File

@ -0,0 +1,272 @@
<template>
<div class="c-snapshot c-ne__embed">
<div v-if="embed.snapshot"
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:class="embed.cssClass"
@click="changeLocation"
>{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"
></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div v-if="embed.snapshot"
class="c-ne__embed__time"
>
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
</div>
</template>
<script>
import Moment from 'moment';
import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro';
import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue';
export default {
inject: ['openmct'],
components: {
},
props: {
embed: {
type: Object,
default() {
return {};
}
},
removeActionString: {
type: String,
default() {
return 'Remove Embed';
}
}
},
data() {
return {
actions: [this.removeEmbedAction()],
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
}
},
watch: {
},
beforeMount() {
this.populateActionMenu();
},
methods: {
annotateSnapshot() {
const self = this;
let save = false;
let painterroInstance = {};
const annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
});
let annotateOverlay = self.openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
const snapshot = reader.result;
const snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
};
self.embed.snapshot = snapshotObject;
self.updateEmbed(self.embed);
};
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(this.embed.snapshot.src);
},
changeLocation() {
this.openmct.time.stopClock();
this.openmct.time.bounds({
start: this.embed.bounds.start,
end: this.embed.bounds.end
});
const link = this.embed.historicLink;
if (!link) {
return;
}
window.location.href = link;
const message = 'Time bounds changed to fixed timespan mode';
this.openmct.notifications.alert(message);
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
},
openSnapshot() {
const self = this;
const snapshot = new Vue({
data: () => {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot
},
template: SnapshotTemplate
});
const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) },
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: () => {
snapshotOverlay.dismiss();
}
}
]
});
},
populateActionMenu() {
const self = this;
const actions = [new PreviewAction(self.openmct)];
self.openmct.objects.get(self.embed.type)
.then((domainObject) => {
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke([domainObject].concat(self.openmct.router.path));
}
});
});
});
},
removeEmbed(id) {
this.$emit('removeEmbed', id);
},
removeEmbedAction() {
const self = this;
return {
name: self.removeActionString,
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
dialog.dismiss();
self.removeEmbed(embed.id);
}
}]
});
}
};
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateEmbed(embed) {
this.$emit('updateEmbed', embed);
}
}
}
</script>

View File

@ -0,0 +1,316 @@
<template>
<div class="c-notebook__entry c-ne has-local-controls"
@dragover="dragover"
@drop.capture="dropCapture"
@drop.prevent="dropOnEntry(entry.id, $event)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span>
<span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span>
</div>
<div class="c-ne__content">
<div :id="entry.id"
class="c-ne__text"
:class="{'c-input-inline' : !readOnly }"
:contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
>{{ entry.text.length ? entry.text : defaultText }}</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
</div>
</div>
<div v-if="!readOnly"
class="c-ne__local-controls--hidden"
>
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry"
>
</button>
</div>
<div v-if="readOnly"
class="c-ne__section-and-page"
>
<a class="c-click-link"
@click="navigateToSection()"
>
{{ result.section.name }}
</a>
<span class="icon-arrow-right"></span>
<a class="c-click-link"
@click="navigateToPage()"
>
{{ result.page.name }}
</a>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
import Moment from 'moment';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
entry: {
type: Object,
default() {
return {};
}
},
result: {
type: Object,
default() {
return {};
}
},
selectedPage: {
type: Object,
default() {
return {};
}
},
selectedSection: {
type: Object,
default() {
return {};
}
},
readOnly: {
type: Boolean,
default() {
return true;
}
}
},
data() {
return {
currentEntryValue: '',
defaultEntryStyle: {
fontStyle: 'italic',
color: '#6e6e6e'
},
defaultText: 'add description'
}
},
watch: {
entry() {
},
readOnly(readOnly) {
},
selectedSection(selectedSection) {
},
selectedPage(selectedSection) {
}
},
mounted() {
this.updateEntries = this.updateEntries.bind(this);
},
beforeDestory() {
},
methods: {
deleteEntry() {
const self = this;
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
return;
}
const entryPosById = this.entryPosById(this.entry.id);
if (entryPosById === -1) {
return;
}
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPosById, 1);
this.updateEntries(entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
dialog.dismiss();
}
}
]
});
},
dragover() {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(entryId, $event) {
event.stopImmediatePropagation();
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const snapshotId = $event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
this.moveSnapshot(snapshotId);
return;
}
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const entryPos = this.entryPosById(entryId);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
}
const newEmbed = createNewEmbed(snapshotMeta);
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const currentEntryEmbeds = entries[entryPos].embeds;
currentEntryEmbeds.push(newEmbed);
this.updateEntries(entries);
},
entryPosById(entryId) {
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
},
findPositionInArray(array, id) {
let position = -1;
array.some((item, index) => {
const found = item.id === id;
if (found) {
position = index;
}
return found;
});
return position;
},
formatTime(unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
},
moveSnapshot(snapshotId) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
this.updateEntry(this.entry);
this.snapshotContainer.removeSnapshot(snapshotId);
},
navigateToPage() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: this.result.page.id
});
},
navigateToSection() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: null
});
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry);
},
selectTextInsideElement(element) {
const range = document.createRange();
range.selectNodeContents(element);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
textBlur($event, entryId) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target;
if (!target) {
return;
}
const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim();
if (this.currentEntryValue !== value) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value;
this.updateEntries(entries);
}
},
textFocus($event) {
if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target
this.currentEntryValue = target ? target.innerText : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
},
updateEmbed(newEmbed) {
let embed = this.entry.embeds.find(e => e.id === newEmbed.id);
if (!embed) {
return;
}
embed = newEmbed;
this.updateEntry(this.entry);
},
updateEntry(newEntry) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.forEach(entry => {
if (entry.id === newEntry.id) {
entry = newEntry;
}
});
this.updateEntries(entries);
},
updateEntries(entries) {
this.$emit('updateEntries', entries);
}
}
}
</script>

View File

@ -0,0 +1,114 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
title="Switch view type"
@click="setNotebookTypes"
@click.stop="toggleMenu"
>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
import Snapshot from '../snapshot';
import { clearDefaultNotebook, getDefaultNotebook } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
notebookSnapshot: null,
notebookTypes: [],
showMenu: false
}
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
async setNotebookTypes() {
const notebookTypes = [];
let defaultPath = '';
const defaultNotebook = getDefaultNotebook();
if (defaultNotebook) {
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier)
.then(d => d);
if (!domainObject.location) {
clearDefaultNotebook();
} else {
defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
}
}
if (defaultPath.length !== 0) {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
type: NOTEBOOK_DEFAULT
});
}
notebookTypes.push({
cssClass: 'icon-notebook',
name: 'Save to Notebook Snapshots',
type: NOTEBOOK_SNAPSHOT
});
this.notebookTypes = notebookTypes;
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
},
snapshot(notebook) {
let element = document.getElementsByClassName("l-shell__main-container")[0];
const bounds = this.openmct.time.bounds();
const objectPath = this.openmct.router.path;
const snapshotMeta = {
bounds,
link: window.location.href,
objectPath,
openmct: this.openmct
};
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
}
}
}
</script>

View File

@ -0,0 +1,152 @@
<template>
<div class="c-snapshots-h">
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w icon-notebook">
<div class="l-browse-bar__object-name">
Notebook Snapshots
<span v-if="snapshots.length"
class="l-browse-bar__object-details"
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span>
</div>
<a class="l-browse-bar__context-actions c-disclosure-button"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform()"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="l-browse-bar__end">
<button class="c-click-icon c-click-icon--major icon-x"
@click="close"
></button>
</div>
</div><!-- closes l-browse-bar -->
<div class="c-snapshots">
<span v-for="snapshot in snapshots"
:key="snapshot.id"
draggable="true"
@dragstart="startEmbedDrag(snapshot, $event)"
>
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
</span>
<div v-if="!snapshots.length > 0"
class="hint"
>
There are no Notebook Snapshots currently.
</div>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
toggleSnapshot: {
type: Function,
default() {
return () => {};
}
}
},
data() {
return {
actions: [this.removeAllSnapshotAction()],
snapshots: []
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots();
},
beforeDestory() {
},
methods: {
close() {
this.toggleSnapshot();
},
getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT;
},
removeAllSnapshotAction() {
const self = this;
return {
name: 'Delete All Snapshots',
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete all notebook snapshots. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.removeAllSnapshots();
dialog.dismiss();
}
}
]
});
}
};
},
removeAllSnapshots() {
this.snapshotContainer.removeAllSnapshots();
},
removeSnapshot(id) {
this.snapshotContainer.removeSnapshot(id);
},
snapshotsUpdated() {
this.snapshots = this.snapshotContainer.getSnapshots();
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
}
}
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<div class="c-indicator c-indicator--clickable icon-notebook"
:class="[
{ 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 },
{ 's-status-caution': snapshotCount === snapshotMaxCount },
{ 'has-new-snapshot': flashIndicator }
]"
>
<span class="label c-indicator__label">
{{ indicatorTitle }}
<button @click="toggleSnapshot">
{{ expanded ? 'Hide' : 'Show' }}
</button>
</span>
<span class="c-indicator__count">{{ snapshotCount }}</span>
</div>
</template>
<script>
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import Vue from 'vue';
export default {
inject: ['openmct','snapshotContainer'],
data() {
return {
expanded: false,
indicatorTitle: '',
snapshotCount: 0,
snapshotMaxCount: NOTEBOOK_SNAPSHOT_MAX_COUNT,
flashIndicator: false
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.updateSnapshotIndicatorTitle();
},
methods: {
notifyNewSnapshot() {
this.flashIndicator = true;
setTimeout(this.removeNotify, 15000);
},
removeNotify() {
this.flashIndicator = false;
},
snapshotsUpdated() {
if (this.snapshotContainer.getSnapshots().length > this.snapshotCount) {
this.notifyNewSnapshot();
}
this.updateSnapshotIndicatorTitle();
},
toggleSnapshot() {
this.expanded = !this.expanded;
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.classList.toggle('is-expanded');
this.updateSnapshotContainer();
},
updateSnapshotContainer() {
const { openmct, snapshotContainer } = this;
const toggleSnapshot = this.toggleSnapshot.bind(this);
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.innerHTML = '<div></div>';
const divElement = document.querySelector('.l-shell__drawer div');
this.component = new Vue({
provide: {
openmct,
snapshotContainer
},
el: divElement,
components: {
SnapshotContainerComponent
},
data() {
return {
toggleSnapshot
};
},
template: '<SnapshotContainerComponent :toggleSnapshot="toggleSnapshot"></SnapshotContainerComponent>'
}).$mount();
},
updateSnapshotIndicatorTitle() {
const snapshotCount = this.snapshotContainer.getSnapshots().length;
this.snapshotCount = snapshotCount;
const snapshotTitleSuffix = snapshotCount === 1
? 'Snapshot'
: 'Snapshots';
this.indicatorTitle = `${snapshotCount} ${snapshotTitleSuffix}`;
}
}
}
</script>

View File

@ -0,0 +1,528 @@
<template>
<div class="c-notebook">
<div class="c-notebook__head">
<Search class="c-notebook__search"
:value="search"
@input="throttledSearchItem"
@clear="throttledSearchItem"
/>
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:results="getSearchResults()"
@changeSectionPage="changeSelectedSection"
/>
<div v-if="!search.length"
class="c-notebook__body"
>
<Sidebar ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:default-section-id="defaultSectionId"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:pages="pages"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
@updatePage="updatePage"
@updateSection="updateSection"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
<div class="c-notebook__page-view__header">
<button class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
@click="toggleNav"
></button>
<div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item">
{{ getSelectedSection() ? getSelectedSection().name : '' }}
</span>
<span class="c-notebook__path__page c-path__item">
{{ getSelectedPage() ? getSelectedPage().name : '' }}
</span>
</div>
<div class="c-notebook__page-view__controls">
<select v-model="showTime"
class="c-notebook__controls__time"
>
<option value="0"
selected="selected"
>
Show all
</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select v-model="defaultSort"
class="c-notebook__controls__time"
>
<option value="newest"
:selected="defaultSort === 'newest'"
>Newest first</option>
<option value="oldest"
:selected="defaultSort === 'oldest'"
>Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@drop.capture="dropCapture"
@drop="dropOnEntry($event)"
>
<span class="c-notebook__drag-area__label">
To start a new entry, click here or drag and drop any object
</span>
</div>
<div v-if="selectedSection && selectedPage"
class="c-notebook__entries"
>
<NotebookEntry v-for="entry in filteredAndSortedEntries"
ref="notebookEntry"
:key="entry.id"
:entry="entry"
:domain-object="internalDomainObject"
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@updateEntries="updateEntries"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './search-results.vue';
import Sidebar from './sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { throttle } from 'lodash';
const DEFAULT_CLASS = 'is-notebook-default';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
components: {
NotebookEntry,
Search,
SearchResults,
Sidebar
},
data() {
return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
defaultSort: this.domainObject.configuration.defaultSort,
internalDomainObject: this.domainObject,
search: '',
showTime: 0,
showNav: false,
sidebarCoversEntries: false
}
},
computed: {
filteredAndSortedEntries() {
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries);
},
pages() {
return this.getPages() || [];
},
sections() {
return this.internalDomainObject.configuration.sections || [];
},
selectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
}
},
watch: {
},
beforeMount() {
this.throttledSearchItem = throttle(this.searchItem, 500);
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
window.addEventListener('orientationchange', this.formatSidebar);
this.navigateToSectionPage();
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
addDefaultClass() {
const classList = this.internalDomainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
this.mutateObject('classList', classList);
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
}
s.pages.forEach((p, i) => {
p.isSelected = false;
if (pageId && pageId === p.id) {
p.isSelected = true;
}
if (!pageId && i === 0) {
p.isSelected = true;
}
});
return s;
});
this.updateSection({ sections });
this.throttledSearchItem('');
},
dragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
const snapshotId = event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
this.snapshotContainer.removeSnapshot(snapshotId);
return;
}
const data = event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const embed = createNewEmbed(snapshotMeta);
this.newEntry(embed);
},
formatSidebar() {
/*
Determine if the sidebar should slide over content, or compress it
Slide over checks:
- phone (all orientations)
- tablet portrait
- in a layout frame (within .c-so-view)
*/
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const isPortrait = window.screen.orientation.type.includes('portrait');
const isInLayout = !!this.$el.closest('.c-so-view');
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d);
},
getPage(section, id) {
return section.pages.find(p => p.id === id);
},
getSection(id) {
return this.sections.find(s => s.id === id);
},
getSearchResults() {
if (!this.search.length) {
return [];
}
const output = [];
const entries = this.internalDomainObject.configuration.entries;
const sectionKeys = Object.keys(entries);
sectionKeys.forEach(sectionKey => {
const pages = entries[sectionKey];
const pageKeys = Object.keys(pages);
pageKeys.forEach(pageKey => {
const pageEntries = entries[sectionKey][pageKey];
pageEntries.forEach(entry => {
if (entry.text && entry.text.toLowerCase().includes(this.search.toLowerCase())) {
const section = this.getSection(sectionKey);
output.push({
section,
page: this.getPage(section, pageKey),
entry
});
}
});
});
});
return output;
},
getPages() {
const selectedSection = this.getSelectedSection();
if (!selectedSection || !selectedSection.pages.length) {
return [];
}
return selectedSection.pages;
},
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
mutateObject(key, value) {
this.openmct.objects.mutate(this.internalDomainObject, key, value);
},
navigateToSectionPage() {
const { pageId, sectionId } = this.openmct.router.getParams();
if(!pageId || !sectionId) {
return;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
this.updateSection({ sections });
},
newEntry(embed = null) {
const selectedSection = this.getSelectedSection();
const selectedPage = this.getSelectedPage();
this.search = '';
this.updateDefaultNotebook(selectedSection, selectedPage);
const notebookStorage = getDefaultNotebook();
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.$nextTick(() => {
const element = this.$el.querySelector(`#${id}`);
element.focus();
});
return id;
},
orientationChange() {
this.formatSidebar();
},
removeDefaultClass(domainObject) {
if (!domainObject) {
return;
}
const classList = domainObject.classList || [];
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
this.openmct.objects.mutate(domainObject, 'classList', classList);
},
searchItem(input) {
this.search = input;
},
sortEntries(right, left) {
return this.defaultSort === 'newest'
? left.createdOn - right.createdOn
: right.createdOn - left.createdOn;
},
toggleNav() {
this.showNav = !this.showNav;
},
async updateDefaultNotebook(selectedSection, selectedPage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.internalDomainObject, selectedSection, selectedPage);
this.addDefaultClass();
this.defaultSectionId = selectedSection.id;
this.defaultPageId = selectedPage.id;
},
updateDefaultNotebookPage(pages, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookPage = notebookStorage.page;
const page = pages.find(p => p.id === id);
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (id !== defaultNotebookPage.id) {
return;
}
setDefaultNotebookPage(page);
},
updateDefaultNotebookSection(sections, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookSection = notebookStorage.section;
const section = sections.find(s => s.id === id);
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (section.id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
},
updateEntries(entries) {
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
this.mutateObject('configuration.entries', notebookEntries);
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
updatePage({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
selectedSection.pages = pages;
const sections = this.sections.map(section => {
if (section.id === selectedSection.id) {
section = selectedSection;
}
return section;
});
this.updateSection({ sections });
this.updateDefaultNotebookPage(pages, id);
},
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
return;
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({
sectionId,
pageId
});
},
updateSection({ sections, id = null }) {
this.mutateObject('configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);
}
}
}
</script>

View File

@ -0,0 +1,132 @@
<template>
<ul class="c-list">
<li v-for="page in pages"
:key="page.id"
class="c-list__item-h"
>
<Page ref="pageComponent"
:default-page-id="defaultPageId"
:page="page"
:page-title="pageTitle"
@deletePage="deletePage"
@renamePage="updatePage"
@selectPage="selectPage"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import Page from './page-component.vue';
export default {
inject: ['openmct'],
components: {
Page
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.filter(p => p.id !== id);
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
const selectedPage = this.pages.find(p => p.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultpage = defaultNotebook && defaultNotebook.page;
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id);
if (isPageSelected && defaultpage) {
pages.forEach(s => {
s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) {
s.isSelected = true;
}
});
}
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
pages[0].isSelected = true;
}
this.$emit('updatePage', { pages, id });
},
selectPage(id) {
const pages = this.pages.map(page => {
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', { pages, id });
// Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) {
this.$emit('toggleNav');
}
},
updatePage(newPage) {
const id = newPage.id;
const pages = this.pages.map(page =>
page.id === id
? newPage
: page);
this.$emit('updatePage', { pages, id });
}
}
}
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id"
@click="selectPage"
>
<span class="c-list__item__name js-list__item__name"
:data-id="page.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(page.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
page: {
type: Object,
required: true
},
pageTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deletePage()]
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deletePage() {
const self = this;
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deletePage', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectPage(event) {
const target = event.target;
const page = target.closest('.js-list__item');
const input = page.querySelector('.js-list__item__name');
if (page.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectPage', id);
},
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected;
},
updateName(event) {
const target = event.target;
const name = target.textContent.toString();
target.contentEditable = false;
target.classList.remove('c-input-inline');
if (this.page.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renamePage', Object.assign(this.page, { name }));
}
}
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="c-notebook__search-results">
<div class="c-notebook__search-results__header">Search Results</div>
<div class="c-notebook__entries">
<NotebookEntry v-for="(result, index) in results"
:key="index"
:result="result"
:entry="result.entry"
:read-only="true"
:selected-page="null"
:selected-section="null"
@changeSectionPage="changeSectionPage"
/>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
export default {
inject: ['openmct', 'domainObject'],
components: {
NotebookEntry
},
props:{
results: {
type: Array,
default() {
return [];
}
}
},
data() {
return {}
},
watch: {
results(newResults) {}
},
destroyed() {
},
mounted() {
},
methods: {
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
}
}
}
</script>

View File

@ -0,0 +1,113 @@
<template>
<ul class="c-list">
<li v-for="section in sections"
:key="section.id"
class="c-list__item-h"
>
<sectionComponent ref="sectionComponent"
:default-section-id="defaultSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import sectionComponent from './section-component.vue';
export default {
inject: ['openmct'],
components: {
sectionComponent
},
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deleteSection(id) {
const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id;
const isSectionDefault = defaultSection && defaultSection.id === id;
const sections = this.sections.filter(s => s.id !== id);
if (isSectionSelected && defaultSection) {
sections.forEach(s => {
s.isSelected = false;
if (defaultSection && defaultSection.id === s.id) {
s.isSelected = true;
}
});
}
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
sections[0].isSelected = true;
}
this.$emit('updateSection', { sections, id });
},
selectSection(id, newSections) {
const currentSections = newSections || this.sections;
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', { sections, id });
},
updateSection(newSection) {
const id = newSection.id;
const sections = this.sections.map(section =>
section.id === id
? newSection
: section);
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:data-id="section.id"
@click="selectSection"
>
<span class="c-list__item__name js-list__item__name"
:data-id="section.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(section.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
section: {
type: Object,
required: true
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deleteSectionAction()]
}
},
watch: {
section(newSection) {
this.toggleContentEditable(newSection);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deleteSectionAction() {
const self = this;
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deleteSection', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectSection(event) {
const target = event.target;
const section = target.closest('.js-list__item');
const input = section.querySelector('.js-list__item__name');
if (section.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectSection', id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected;
},
updateName(event) {
const target = event.target;
target.contentEditable = false;
target.classList.remove('c-input-inline');
const name = target.textContent.trim();
if (this.section.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renameSection', Object.assign(this.section, { name }));
}
}
}
</script>

View File

@ -0,0 +1,119 @@
.c-sidebar {
@include userSelectNone();
background: $sideBarBg;
display: flex;
justify-content: stretch;
max-width: 300px;
&.c-drawer--push.is-expanded {
margin-right: $interiorMargin;
width: 50%;
}
&.c-drawer--overlays.is-expanded {
width: 95%;
}
> * {
// Hardcoded for two columns
background: $sideBarBg;
display: flex;
flex: 1 1 50%;
flex-direction: column;
+ * {
margin-left: $interiorMarginSm;
}
> * + * {
// Add margin-top to first and second level children
margin-top: $interiorMargin;
}
}
&__pane {
> * + * { margin-top: $interiorMargin; }
}
&__header-w {
// Wraps header, used for page pane with collapse button
display: flex;
flex: 0 0 auto;
background: $sideBarHeaderBg;
align-items: center;
.c-icon-button {
font-size: 0.8em;
color: $colorBodyFg;
}
}
&__header {
color: $sideBarHeaderFg;
display: flex;
flex: 1 1 auto;
padding: $interiorMargin;
text-transform: uppercase;
> * {
@include ellipsize();
}
}
&__contents-and-controls {
// Encloses pane buttons and contents elements
display: flex;
flex-direction: column;
flex: 1 1 auto;
> * {
margin: auto $interiorMargin $interiorMargin $interiorMargin;
&:first-child {
border-bottom: 1px solid $colorInteriorBorder;
flex: 0 0 auto;
}
+ * {
margin-top: $interiorMargin;
}
}
}
&__contents {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
padding: auto $interiorMargin;
}
.c-list-button {
.c-button {
font-size: 0.8em;
}
}
.c-list__item {
@include hover() {
[class*="__menu-indicator"] {
opacity: 0.7;
transition: $transIn;
}
}
> * + * {
margin-left: $interiorMargin;
}
&__name {
flex: 0 1 auto;
}
&__menu-indicator {
flex: 0 0 auto;
font-size: 0.8em;
opacity: 0;
transition: $transOut;
}
}
}

View File

@ -0,0 +1,189 @@
<template>
<div class="c-sidebar c-drawer c-drawer--align-left">
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
</div>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addSection"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ sectionTitle }}</span>
</button>
<SectionCollection class="c-sidebar__contents"
:default-section-id="defaultSectionId"
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@updateSection="updateSection"
/>
</div>
</div>
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
</div>
<button class="c-click-icon c-click-icon--major icon-x-in-circle"
@click="toggleNav"
></button>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addPage"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ pageTitle }}</span>
</button>
<PageCollection ref="pageCollection"
class="c-sidebar__contents"
:default-page-id="defaultPageId"
:domain-object="domainObject"
:pages="pages"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
:page-title="pageTitle"
@toggleNav="toggleNav"
@updatePage="updatePage"
/>
</div>
</div>
</div>
</template>
<script>
import SectionCollection from './section-collection.vue';
import PageCollection from './page-collection.vue';
import uuid from 'uuid';
export default {
inject: ['openmct'],
components: {
SectionCollection,
PageCollection
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
pages(newpages) {
if (!newpages.length) {
this.addPage();
}
},
sections(newSections) {
if (!newSections.length) {
this.addSection();
}
}
},
mounted() {
if (!this.sections.length) {
this.addSection();
}
},
destroyed() {
},
methods: {
addPage() {
const pageTitle = this.pageTitle;
const id = uuid();
const page = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${pageTitle}`,
pageTitle
};
this.pages.forEach(p => p.isSelected = false);
const pages = this.pages.concat(page);
this.updatePage({ pages, id });
},
addSection() {
const sectionTitle = this.sectionTitle;
const id = uuid();
const section = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${sectionTitle}`,
pages : [],
sectionTitle
};
this.sections.forEach(s => s.isSelected = false);
const sections = this.sections.concat(section);
this.updateSection({ sections, id });
},
toggleNav() {
this.$emit('toggleNav');
},
updatePage({ pages, id }) {
this.$emit('updatePage', { pages, id });
},
updateSection({ sections, id }) {
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -3,10 +3,10 @@
<div class="c-notebook-snapshot__header l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="l-browse-bar__object-name"
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass"
>
{{embed.name}}
<span class="c-object-label__name">{{ embed.name }}</span>
</span>
</div>
</div>
@ -21,9 +21,8 @@
</div>
</div>
<div class="c-notebook-snapshot__image">
<div class="image-main s-image-main"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
></div>
<div class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
>
</div>
</div>

View File

@ -0,0 +1,3 @@
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';

View File

@ -1,99 +1,134 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import Notebook from './components/notebook.vue';
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
import SnapshotContainer from './snapshot-container';
import Vue from 'vue';
define([
"./src/controllers/NotebookController"
], function (
NotebookController
) {
var installed = false;
let installed = false;
function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
export default function NotebookPlugin() {
return function install(openmct) {
if (installed) {
return;
}
installed = true;
installed = true;
openmct.legacyRegistry.register('notebook', {
name: 'Notebook Plugin',
extensions: {
types: [
const notebookType = {
name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',
creatable: true,
cssClass: 'icon-notebook',
initialize: domainObject => {
domainObject.configuration = {
defaultSort: 'oldest',
entries: {},
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
};
},
form: [
{
key: 'defaultSort',
name: 'Entry Sorting',
control: 'select',
options: [
{
key: 'notebook',
name: 'Notebook',
cssClass: 'icon-notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',
features: 'creation',
model: {
entries: [],
entryTypes: [],
defaultSort: 'oldest'
},
properties: [
{
key: 'defaultSort',
name: 'Default Sort',
control: 'select',
options: [
{
name: 'Newest First',
value: "newest"
},
{
name: 'Oldest First',
value: "oldest"
}
],
cssClass: 'l-inline'
}
]
name: 'Newest First',
value: "newest"
},
{
name: 'Oldest First',
value: "oldest"
}
],
cssClass: 'l-inline',
property: [
"configuration",
"defaultSort"
]
},
{
key: 'type',
name: 'Note book Type',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"type"
]
},
{
key: 'sectionTitle',
name: 'Section Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"sectionTitle"
]
},
{
key: 'pageTitle',
name: 'Page Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"pageTitle"
]
}
});
]
};
openmct.types.addType('notebook', notebookType);
openmct.legacyRegistry.enable('notebook');
openmct.objectViews.addProvider({
key: 'notebook-vue',
name: 'Notebook View',
cssClass: 'icon-notebook',
canView: function (domainObject) {
return domainObject.type === 'notebook';
},
view: function (domainObject) {
var controller = new NotebookController (openmct, domainObject);
return {
show: controller.show,
destroy: controller.destroy
};
}
});
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
provide: {
openmct,
snapshotContainer
},
components: {
NotebookSnapshotIndicator
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
});
const indicator = {
element: notebookSnapshotIndicator.$mount().$el
};
}
openmct.indicators.add(indicator);
return NotebookPlugin;
});
openmct.objectViews.addProvider({
key: 'notebook-vue',
name: 'Notebook View',
cssClass: 'icon-notebook',
canView: function (domainObject) {
return domainObject.type === 'notebook';
},
view: function (domainObject) {
let component;
return {
show(container) {
component = new Vue({
el: container,
components: {
Notebook
},
provide: {
openmct,
domainObject,
snapshotContainer
},
template: '<Notebook></Notebook>'
});
},
destroy() {
component.$destroy();
}
};
}
});
};
}

View File

@ -1,33 +0,0 @@
<div class="c-ne__embed">
<div class="c-ne__embed__snap-thumb"
v-if="embed.snapshot"
v-on:click="openSnapshot(domainObject, entry, embed)">
<img v-bind:src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:href="objectLink"
:class="embed.cssClass">{{embed.name}}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed, entry)">
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div class="c-ne__embed__time" v-if="embed.snapshot">
{{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
</div>
</div>

View File

@ -1,34 +0,0 @@
<li class="c-notebook__entry c-ne has-local-controls"
@drop.prevent="dropOnEntry(entry.id, $event)">
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{formatTime(entry.createdOn, 'YYYY-MM-DD')}}</span>
<span>{{formatTime(entry.createdOn, 'HH:mm:ss')}}</span>
</div>
<div class="c-ne__content">
<div class="c-ne__text c-input-inline"
contenteditable="true"
ref="contenteditable"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
v-html="entry.text">
</div>
<div class="c-ne__embeds">
<notebook-embed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:objectPath="embed.objectPath"
:entry="entry">
</notebook-embed>
</div>
</div>
</div>
<div class="c-ne__local-controls--hidden">
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry">
</button>
</div>
</li>

View File

@ -1,35 +0,0 @@
<div class="c-notebook">
<div class="c-notebook__head">
<search class="c-notebook__search"
:value="entrySearch"
@input="search"
@clear="search">
</search>
<div class="c-notebook__controls ">
<select class="c-notebook__controls__time" v-model="showTime">
<option value="0" selected="selected">Show all</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select class="c-notebook__controls__time" v-model="sortEntries">
<option value="newest" :selected="sortEntries === 'newest'">Newest first</option>
<option value="oldest" :selected="sortEntries === 'oldest'">Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry($event)"
@drop="newEntry($event)">
<span class="c-notebook__drag-area__label">To start a new entry, click here or drag and drop any object</span>
</div>
<div class="c-notebook__entries">
<ul>
<notebook-entry
v-for="(entry,index) in filteredAndSortedEntries"
:key="entry.key"
:entry="entry">
</notebook-entry>
</ul>
</div>
</div>

View File

@ -1,50 +0,0 @@
<div class="abs overlay l-large-view">
<div class="abs blocker" v-on:click="close"></div>
<div class="abs outer-holder">
<a
class="close icon-x-in-circle"
v-on:click="close">
</a>
<div class="abs inner-holder l-flex-col">
<div class="t-contents flex-elem holder grows">
<div class="t-snapshot abs l-view-header">
<div class="abs object-browse-bar l-flex-row">
<div class="left flex-elem l-flex-row grows">
<div class="object-header flex-elem l-flex-row grows">
<div class="type-icon flex-elem embed-icon holder" v-bind:class="embed.cssClass"></div>
<div class="title-label flex-elem holder flex-can-shrink">{{embed.name}}</div>
</div>
</div>
</div>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<div class="flex-elem holder flex-can-shrink s-snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="s-button icon-pencil" title="Annotate">
<span class="title-label">Annotate</span>
</a>
</div>
<div class="abs object-holder t-image-holder s-image-holder">
<div
class="image-main s-image-main"
v-bind:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }">
</div>
</div>
</div>
<div
class="bottom-bar flex-elem holder"
v-on:click="close">
<a class="t-done s-button major">Done</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,82 @@
import EventEmitter from 'EventEmitter';
import { EVENT_SNAPSHOTS_UPDATED } from './notebook-constants';
const NOTEBOOK_SNAPSHOT_STORAGE = 'notebook-snapshot-storage';
export const NOTEBOOK_SNAPSHOT_MAX_COUNT = 5;
export default class SnapshotContainer extends EventEmitter {
constructor(openmct) {
super();
if (!SnapshotContainer.instance) {
SnapshotContainer.instance = this;
}
this.openmct = openmct;
return SnapshotContainer.instance;
}
addSnapshot(embedObject) {
const snapshots = this.getSnapshots();
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
snapshots.pop();
}
snapshots.unshift(embedObject);
return this.saveSnapshots(snapshots);
}
getSnapshot(id) {
const snapshots = this.getSnapshots();
return snapshots.find(s => s.id === id);
}
getSnapshots() {
const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]';
return JSON.parse(snapshots);
}
removeSnapshot(id) {
if (!id) {
return;
}
const snapshots = this.getSnapshots();
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
return this.saveSnapshots(filteredsnapshots);
}
removeAllSnapshots() {
return this.saveSnapshots([]);
}
saveSnapshots(snapshots) {
try {
window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots));
this.emit(EVENT_SNAPSHOTS_UPDATED, true);
return true;
} catch (e) {
const message = 'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!';
this.openmct.notifications.error(message);
return false;
}
}
updateSnapshot(snapshot) {
const snapshots = this.getSnapshots();
const updatedSnapshots = snapshots.map(s => {
return s.id === snapshot.id
? snapshot
: s;
});
return this.saveSnapshots(updatedSnapshots);
}
}

View File

@ -0,0 +1,74 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import SnapshotContainer from './snapshot-container';
export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, snapshotMeta);
}.bind(this);
}.bind(this));
}
/**
* @private
*/
_saveSnapShot(notebookType, imageUrl, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : '';
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return;
}
this._saveToNotebookSnapshots(embed);
}
/**
* @private
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg);
});
}
/**
* @private
*/
_saveToNotebookSnapshots(embed) {
const saved = this.snapshotContainer.addSnapshot(embed);
if (!saved) {
return;
}
const msg = 'Saved to Notebook Snapshots - click to view.';
this._showNotification(msg);
}
_showNotification(msg) {
this.openmct.notifications.info(msg);
}
}

View File

@ -1,302 +0,0 @@
/*****************************************************************************
* 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([
'moment',
'zepto',
'../../res/templates/snapshotTemplate.html',
'vue',
'painterro'
],
function (
Moment,
$,
SnapshotTemplate,
Vue,
Painterro
) {
function EmbedController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.popupService = openmct.$injector.get('popupService');
this.agentService = openmct.$injector.get('agentService');
this.exposedData = this.exposedData.bind(this);
this.exposedMethods = this.exposedMethods.bind(this);
this.toggleActionMenu = this.toggleActionMenu.bind(this);
}
EmbedController.prototype.openSnapshot = function (domainObject, entry, embed) {
function annotateSnapshot(openmct) {
return function () {
var save = false,
painterroInstance = {},
annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
}),
self = this;
let annotateOverlay = openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
var entryPos = self.findInArray(domainObject.entries, entry.id),
embedPos = self.findInArray(entry.embeds, embed.id);
if (entryPos !== -1 && embedPos !== -1) {
var url = image.asBlob(),
reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
var snapshot = reader.result,
snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
},
dirString = 'entries[' + entryPos + '].embeds[' + embedPos + '].snapshot';
openmct.objects.mutate(domainObject, dirString, snapshotObject);
};
}
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(embed.snapshot.src);
};
}
var self = this,
snapshot = new Vue({
data: function () {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: annotateSnapshot(self.openmct),
findInArray: self.findInArray
},
template: SnapshotTemplate
});
var snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => {snapshot.$destroy(true)},
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: function () {
snapshotOverlay.dismiss();
}
}
]
});
};
EmbedController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EmbedController.prototype.findInArray = function (array, id) {
var foundId = -1;
array.forEach(function (element, index) {
if (element.id === id) {
foundId = index;
return;
}
});
return foundId;
};
EmbedController.prototype.populateActionMenu = function (openmct, actions) {
return function () {
var self = this;
openmct.objects.get(self.embed.type).then(function (domainObject) {
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke([domainObject].concat(openmct.router.path));
}
});
});
});
};
};
EmbedController.prototype.removeEmbedAction = function () {
var self = this;
return {
name: 'Remove Embed',
cssClass: 'icon-trash',
perform: function (embed, entry) {
var entryPosition = self.findInArray(self.domainObject.entries, entry.id),
embedPosition = self.findInArray(entry.embeds, embed.id);
var dialog = self.openmct.overlays.dialog({
iconClass: "alert",
message: 'This Action will permanently delete this embed. Do you wish to continue?',
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
entry.embeds.splice(embedPosition, 1);
var dirString = 'entries[' + entryPosition + '].embeds';
self.openmct.objects.mutate(self.domainObject, dirString, entry.embeds);
dialog.dismiss();
}
}]
});
}
};
};
EmbedController.prototype.toggleActionMenu = function (event) {
event.preventDefault();
var body = $(document.body),
container = $(event.target.parentElement.parentElement),
initiatingEvent = this.agentService.isMobile() ?
'touchstart' : 'mousedown',
menu = container.find('.menu-element'),
dismissExistingMenu;
// Remove the context menu
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
this.popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
};
EmbedController.prototype.exposedData = function () {
return {
actions: [this.removeEmbedAction()],
showActionMenu: false
};
};
EmbedController.prototype.exposedMethods = function () {
var self = this;
return {
openSnapshot: self.openSnapshot,
formatTime: self.formatTime,
toggleActionMenu: self.toggleActionMenu,
findInArray: self.findInArray
};
};
return EmbedController;
});

View File

@ -1,151 +0,0 @@
/*****************************************************************************
* 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([
'moment'
],
function (
Moment
) {
function EntryController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.currentEntryValue = '';
this.exposedMethods = this.exposedMethods.bind(this);
this.exposedData = this.exposedData.bind(this);
}
EntryController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
EntryController.prototype.textFocus = function ($event) {
if ($event.target) {
this.currentEntryValue = $event.target.innerText;
} else {
$event.target.innerText = '';
}
};
EntryController.prototype.textBlur = function ($event, entryId) {
if ($event.target) {
var entryPos = this.entryPosById(entryId);
if (this.currentEntryValue !== $event.target.innerText) {
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].text', $event.target.innerText);
}
}
};
EntryController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EntryController.prototype.deleteEntry = function () {
var entryPos = this.entryPosById(this.entry.id),
domainObject = this.domainObject,
openmct = this.openmct;
if (entryPos !== -1) {
var dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: function () {
domainObject.entries.splice(entryPos, 1);
openmct.objects.mutate(domainObject, 'entries', domainObject.entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: function () {
dialog.dismiss();
}
}
]
});
}
};
EntryController.prototype.dropOnEntry = function (entryid, event) {
var data = event.dataTransfer.getData('openmct/domain-object-path');
if (data) {
var objectPath = JSON.parse(data),
domainObject = objectPath[0],
domainObjectKey = domainObject.identifier.key,
domainObjectType = this.openmct.types.get(domainObject.type),
cssClass = domainObjectType && domainObjectType.definition ?
domainObjectType.definition.cssClass : 'icon-object-unknown',
entryPos = this.entryPosById(entryid),
currentEntryEmbeds = this.domainObject.entries[entryPos].embeds,
newEmbed = {
id: '' + Date.now(),
domainObject: domainObject,
objectPath: objectPath,
type: domainObjectKey,
cssClass: cssClass,
name: domainObject.name,
snapshot: ''
};
currentEntryEmbeds.push(newEmbed);
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].embeds', currentEntryEmbeds);
}
};
EntryController.prototype.exposedData = function () {
return {
openmct: this.openmct,
domainObject: this.domainObject,
currentEntryValue: this.currentEntryValue
};
};
EntryController.prototype.exposedMethods = function () {
return {
entryPosById: this.entryPosById,
textFocus: this.textFocus,
textBlur: this.textBlur,
formatTime: this.formatTime,
deleteEntry: this.deleteEntry,
dropOnEntry: this.dropOnEntry
};
};
return EntryController;
});

View File

@ -1,237 +0,0 @@
/*****************************************************************************
* 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([
'vue',
'./EntryController',
'./EmbedController',
'../../res/templates/notebook.html',
'../../res/templates/entry.html',
'../../res/templates/embed.html',
'../../../../ui/components/search.vue',
'../../../../ui/preview/PreviewAction',
'../../../../ui/mixins/object-link'
],
function (
Vue,
EntryController,
EmbedController,
NotebookTemplate,
EntryTemplate,
EmbedTemplate,
search,
PreviewAction,
objectLinkMixin
) {
function NotebookController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.entrySearch = '';
this.previewAction = new PreviewAction.default(openmct);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.newEntry = this.newEntry.bind(this);
this.entryPosById = this.entryPosById.bind(this);
}
NotebookController.prototype.initializeVue = function (container) {
var self = this,
entryController = new EntryController(this.openmct, this.domainObject),
embedController = new EmbedController(this.openmct, this.domainObject);
this.container = container;
var notebookEmbed = {
inject:['openmct', 'domainObject'],
mixins:[objectLinkMixin.default],
props:['embed', 'entry'],
template: EmbedTemplate,
data: embedController.exposedData,
methods: embedController.exposedMethods(),
beforeMount: embedController.populateActionMenu(self.openmct, [self.previewAction])
};
var entryComponent = {
props:['entry'],
template: EntryTemplate,
components: {
'notebook-embed': notebookEmbed
},
data: entryController.exposedData,
methods: entryController.exposedMethods(),
mounted: self.focusOnEntry
};
var NotebookVue = Vue.extend({
provide: {openmct: self.openmct, domainObject: self.domainObject},
components: {
'notebook-entry': entryComponent,
'search': search.default
},
data: function () {
return {
entrySearch: self.entrySearch,
showTime: '0',
sortEntries: self.domainObject.defaultSort,
entries: self.domainObject.entries,
currentEntryValue: ''
};
},
computed: {
filteredAndSortedEntries() {
return this.sort(this.filterBySearch(this.entries, this.entrySearch), this.sortEntries);
}
},
methods: {
search(value) {
this.entrySearch = value;
},
newEntry: self.newEntry,
filterBySearch: self.filterBySearch,
sort: self.sort
},
template: NotebookTemplate
});
this.NotebookVue = new NotebookVue();
container.appendChild(this.NotebookVue.$mount().$el);
};
NotebookController.prototype.newEntry = function (event) {
this.NotebookVue.search('');
var date = Date.now(),
embed;
if (event.dataTransfer && event.dataTransfer.getData('openmct/domain-object-path')) {
var selectedObject = JSON.parse(event.dataTransfer.getData('openmct/domain-object-path'))[0],
selectedObjectId = selectedObject.identifier.key,
cssClass = this.openmct.types.get(selectedObject.type);
embed = {
type: selectedObjectId,
id: '' + date,
cssClass: cssClass,
name: selectedObject.name,
snapshot: ''
};
}
var entries = this.domainObject.entries,
lastEntryIndex = this.NotebookVue.sortEntries === 'newest' ? 0 : entries.length - 1,
lastEntry = entries[lastEntryIndex];
if (lastEntry === undefined || lastEntry.text || lastEntry.embeds.length) {
var createdEntry = {'id': 'entry-' + date, 'createdOn': date, 'embeds':[]};
if (embed) {
createdEntry.embeds.push(embed);
}
entries.push(createdEntry);
this.openmct.objects.mutate(this.domainObject, 'entries', entries);
} else {
lastEntry.createdOn = date;
if(embed) {
lastEntry.embeds.push(embed);
}
this.openmct.objects.mutate(this.domainObject, 'entries[entries.length-1]', lastEntry);
this.focusOnEntry.bind(this.NotebookVue.$children[lastEntryIndex+1])();
}
};
NotebookController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
NotebookController.prototype.focusOnEntry = function () {
if (!this.entry.text) {
this.$refs.contenteditable.focus();
}
};
NotebookController.prototype.filterBySearch = function (entryArray, filterString) {
if (filterString) {
var lowerCaseFilterString = filterString.toLowerCase();
return entryArray.filter(function (entry) {
if (entry.text) {
return entry.text.toLowerCase().includes(lowerCaseFilterString);
} else {
return false;
}
});
} else {
return entryArray;
}
};
NotebookController.prototype.sort = function (array, sortDirection) {
let oldest = (a,b) => {
if (a.createdOn < b.createdOn) {
return -1;
} else if (a.createdOn > b.createdOn) {
return 1;
} else {
return 0;
}
},
newest = (a,b) => {
if (a.createdOn < b.createdOn) {
return 1;
} else if (a.createdOn > b.createdOn) {
return -1;
} else {
return 0;
}
};
if (sortDirection === 'newest') {
return array.sort(newest);
} else {
return array.sort(oldest);
}
};
NotebookController.prototype.show = function (container) {
this.initializeVue(container);
};
NotebookController.prototype.destroy = function (container) {
this.NotebookVue.$destroy(true);
};
return NotebookController;
});

View File

@ -0,0 +1,194 @@
import objectLink from '../../../ui/mixins/object-link';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
}
export const getHistoricLinkInFixedMode = (openmct, bounds, historicLink) => {
if (historicLink.includes('tc.mode=fixed')) {
return historicLink;
}
openmct.time.getAllClocks().forEach(clock => {
if (historicLink.includes(`tc.mode=${clock.key}`)) {
historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed');
return;
}
});
const params = historicLink.split('&').map(param => {
if (param.includes(TIME_BOUNDS.START_BOUND)
|| param.includes(TIME_BOUNDS.START_DELTA)) {
param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`;
}
if (param.includes(TIME_BOUNDS.END_BOUND)
|| param.includes(TIME_BOUNDS.END_DELTA)) {
param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`;
}
return param;
});
return params.join('&');
}
export const getNotebookDefaultEntries = (notebookStorage, domainObject) => {
if (!notebookStorage || !domainObject) {
return null;
}
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[defaultSection.id];
if (!section) {
section = {};
entries[defaultSection.id] = section;
}
let page = entries[defaultSection.id][defaultPage.id];
if (!page) {
page = [];
entries[defaultSection.id][defaultPage.id] = [];
}
return entries[defaultSection.id][defaultPage.id];
}
export const createNewEmbed = (snapshotMeta, snapshot = '') => {
const {
bounds,
link,
objectPath,
openmct
} = snapshotMeta;
const domainObject = objectPath[0];
const domainObjectType = openmct.types.get(domainObject.type);
const cssClass = domainObjectType && domainObjectType.definition
? domainObjectType.definition.cssClass
: 'icon-object-unknown';
const date = Date.now();
const historicLink = link
? getHistoricLinkInFixedMode(openmct, bounds, link)
: objectLink.computed.objectLink.call({ objectPath, openmct });
const name = domainObject.name;
const type = domainObject.identifier.key;
return {
bounds,
createdOn: date,
cssClass,
domainObject,
historicLink,
id: 'embed-' + date,
name,
snapshot,
type
};
}
export const addNotebookEntry = (openmct, domainObject, notebookStorage, embed = null) => {
if (!openmct || !domainObject || !notebookStorage) {
return;
}
const date = Date.now();
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
if (!entries) {
return;
}
const embeds = embed
? [embed]
: [];
const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject);
const id = `entry-${date}`;
defaultEntries.push({
id,
createdOn: date,
text: '',
embeds
});
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id;
}
export const getNotebookEntries = (domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[selectedSection.id];
if (!section) {
return null;
}
let page = entries[selectedSection.id][selectedPage.id];
if (!page) {
return null;
}
return entries[selectedSection.id][selectedPage.id];
}
export const getEntryPosById = (entryId, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return;
}
const entries = getNotebookEntries(domainObject, selectedSection, selectedPage);
let foundId = -1;
entries.forEach((element, index) => {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
}
export const deleteNotebookEntries = (openmct, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection) {
return;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
// Delete entire section
if (!selectedPage) {
delete entries[selectedSection.id];
return;
}
let section = entries[selectedSection.id];
if (!section) {
return;
}
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
}

View File

@ -0,0 +1,40 @@
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
export function clearDefaultNotebook() {
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
}
export function getDefaultNotebook() {
const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE);
return JSON.parse(notebookStorage);
}
export function setDefaultNotebook(domainObject, section, page) {
const notebookMeta = {
name: domainObject.name,
identifier: domainObject.identifier
};
const notebookStorage = {
notebookMeta,
section,
page
}
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookSection(section) {
const notebookStorage = getDefaultNotebook();
notebookStorage.section = section;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookPage(page) {
const notebookStorage = getDefaultNotebook();
notebookStorage.page = page;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}

View File

@ -0,0 +1,45 @@
import $ from 'zepto';
export const togglePopupMenu = (event, openmct) => {
event.preventDefault();
const body = $(document.body);
const container = $(event.target.parentElement.parentElement);
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const initiatingEvent = isPhone || isTablet
? 'touchstart'
: 'mousedown';
const menu = container.find('.menu-element');
let dismissExistingMenu;
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
const popupService = openmct.$injector.get('popupService');
popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
}

View File

@ -175,7 +175,7 @@ define([
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook;
plugins.Notebook = Notebook.default;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;

View File

@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class RemoveAction {
constructor(openmct) {
this.name = 'Remove';
@ -90,6 +89,13 @@ export default class RemoveAction {
if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
const parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
const isAlias = parentKeyString !== child.location;
if (!isAlias) {
this.openmct.objects.mutate(child, 'location', null);
}
}
appliesTo(objectPath) {

View File

@ -87,6 +87,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 20%);
// Layout
$shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px;
$drawerBg: lighten($colorBodyBg, 5%);
$drawerFg: lighten($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba($colorBodyFg, 0.2);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999;
@ -333,7 +338,7 @@ $colorSummaryFg: rgba($colorBodyFg, 0.7);
$colorSummaryFgEm: $colorBodyFg;
// Plot
$colorPlotBg: rgba(black, 0.05);
$colorPlotBg: rgba(black, 0.1);
$colorPlotFg: $colorBodyFg;
$colorPlotHash: black;
$opacityPlotHash: 0.2;

View File

@ -91,6 +91,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 20%);
// Layout
$shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px;
$drawerBg: lighten($colorBodyBg, 5%);
$drawerFg: lighten($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba($colorBodyFg, 0.2);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999;

View File

@ -26,7 +26,7 @@
$mobileListIconSize: 30px;
$mobileTitleDescH: 35px;
$mobileOverlayMargin: 20px;
$mobileMenuIconD: 34px;
$mobileMenuIconD: 25px;
$phoneItemH: floor($gridItemMobile / 4);
$tabletItemH: floor($gridItemMobile / 3);

View File

@ -87,6 +87,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 10%);
// Layout
$shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px;
$drawerBg: darken($colorBodyBg, 5%);
$drawerFg: darken($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba(black, 0.25);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999;

View File

@ -147,6 +147,7 @@ $glyph-icon-suitcase: '\e928';
$glyph-icon-cursor-lock: '\e929';
$glyph-icon-flag: '\e92a';
$glyph-icon-eye-disabled: '\e92b';
$glyph-icon-notebook-page: '\e92c';
$glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02';

View File

@ -54,7 +54,6 @@ button {
background: $splitterBtnColorBg;
color: $splitterBtnColorFg;
border-radius: $smallCr;
font-size: 6px;
line-height: 90%;
padding: 3px 10px;
@ -63,6 +62,10 @@ button {
color: $colorBtnFgHov;
}
@include desktop() {
font-size: 6px;
}
&:before {
content: $glyph-icon-arrow-down;
font-size: 1.1em;
@ -158,6 +161,26 @@ button {
}
}
.c-list-button {
@include cControl();
color: $colorBodyFg;
cursor: pointer;
justify-content: start;
padding: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
@include hover() {
background: $colorItemTreeHoverBg;
}
.c-button {
flex: 0 0 auto;
}
}
/******************************************************** DISCLOSURE CONTROLS */
/********* Disclosure Button */
// Provides a downward arrow icon that when clicked displays additional options and/or info.
@ -709,7 +732,7 @@ select {
$pTB: 2px;
padding: $pTB $pLR;
&:hover {
@include hover() {
background: $editUIBaseColorHov !important;
color: $editUIBaseColorFg !important;
}
@ -724,7 +747,7 @@ select {
color: $colorBtnCautionBg;
}
&:hover {
@include hover() {
background: rgba($colorBtnCautionBgHov, 0.2);
:before {
color: $colorBtnCautionBgHov;

View File

@ -83,6 +83,7 @@
.icon-cursor-lock { @include glyphBefore($glyph-icon-cursor-lock); }
.icon-flag { @include glyphBefore($glyph-icon-flag); }
.icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); }
.icon-notebook-page { @include glyphBefore($glyph-icon-notebook-page); }
.icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); }
.icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); }

View File

@ -80,4 +80,8 @@
&__object-name {
flex: 0 1 auto;
}
&__object-details {
opacity: 0.5;
}
}

View File

@ -51,7 +51,7 @@
/************************** EFFECTS */
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
@keyframes pulse {
@keyframes #{$animName} {
0% { opacity: $opacity0; }
100% { opacity: $opacity100; }
}
@ -62,6 +62,18 @@
animation-timing-function: ease-in-out;
}
@mixin pulseProp($animName: pulseProp, $dur: 500ms, $iter: 5, $prop: opacity, $valStart: 0, $valEnd: 1) {
@keyframes #{$animName} {
0% { #{$prop}: $valStart; }
100% { #{$prop}: $valEnd; }
}
animation-name: $animName;
animation-duration: $dur;
animation-direction: alternate;
animation-iteration-count: $iter;
animation-timing-function: ease-in-out;
}
/************************** VISUALS */
@mixin ancillaryIcon($d, $c) {
// Used for small icons used in combination with larger icons,
@ -426,10 +438,15 @@
color: $colorBodyFg;
cursor: pointer;
padding: 4px; // Bigger hit area
opacity: 0.6;
opacity: 0.7;
transition: $transOut;
transform-origin: center;
&[class*="--major"] {
color: $colorBtnMajorBg !important;
opacity: 0.8;
}
@include hover() {
transform: scale(1.1);
transition: $transIn;

View File

@ -22,34 +22,61 @@
/*********************************************** NOTEBOOK */
.c-notebook {
$headerFontSize: 1.3em;
display: flex;
flex-direction: column;
overflow: hidden;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
height: 100%;
&-snapshot {
flex: 1 1 auto;
/****************************** CONTENT */
&__body {
// Holds __nav and __page-view
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: hidden;
}
> * + * {
margin-top: $interiorMargin;
}
&__header {
flex: 0 0 auto;
}
&__image {
flex: 1 1 auto;
&__nav {
flex: 0 0 auto;
* {
overflow: hidden;
}
}
> [class*="__"] + [class*="__"] {
.c-sidebar {
background: $sideBarBg;
.c-sidebar__pane {
flex-basis: 50%;
}
}
body.mobile & {
.c-list-button,
&-snapshot-menubutton {
display: none;
}
}
/****************************** CONTENT */
&__contents {
width: 70%;
}
&__page-view {
// Holds __header, __drag-area and __entries
display: flex;
flex: 1 1 auto;
flex-direction: column;
width: 100%;
> * {
flex: 0 0 auto;
+ * {
margin-top: $interiorMargin;
}
}
}
> * + * {
margin-top: $interiorMargin;
}
@ -111,17 +138,93 @@
}
}
/***** PAGE VIEW */
&__page-view {
&__header {
display: flex;
flex-wrap: wrap; // Allows wrapping in mobile portrait and narrow placements
line-height: 220%;
> * {
flex: 0 0 auto;
}
}
&__path {
flex: 1 1 auto;
margin: 0 $interiorMargin;
overflow: hidden;
white-space: nowrap;
font-size: $headerFontSize;
> * {
// Section
flex: 0 0 auto;
+ * {
// Page
display: inline;
flex: 1 1 auto;
@include ellipsize();
}
}
}
}
&__entries {
flex-direction: column;
flex: 1 1 auto;
padding-right: $interiorMarginSm;
overflow-x: hidden;
overflow-y: scroll;
@include desktop() {
padding-right: $interiorMarginSm; // Scrollbar kickoff
}
[class*="__entry"] + [class*="__entry"] {
margin-top: $interiorMarginSm;
}
}
/***** SEARCH RESULTS */
&__search-results {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * + * {
margin-top: 5px;
}
&__header {
font-size: $headerFontSize;
flex: 0 0 auto;
}
.c-notebook__entries {
flex: 1 1 auto;
}
.c-ne {
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
}
}
}
.is-notebook-default {
&:after {
color: $colorFilter;
content: $glyph-icon-notebook-page;
display: block;
font-family: symbolsfont;
font-size: 0.9em;
margin-left: $interiorMargin;
}
&.c-list__item:after {
flex: 1 0 auto;
text-align: right;
}
}
@ -183,10 +286,29 @@
}
&__embeds {
flex-wrap: wrap;
//flex-wrap: wrap;
> [class*="__embed"] {
margin: 0 $interiorMarginSm $interiorMarginSm 0;
//margin: 0 $interiorMarginSm $interiorMarginSm 0;
}
}
&__section-and-page {
// Shown when c-ne within search results
background: rgba($colorBodyFg, 0.1); //$colorInteriorBorder;
border-radius: $controlCr;
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
[class*='icon'] {
font-size: 0.8em;
opacity: 0.7;
}
}
}
@ -194,7 +316,7 @@
/****************************** EMBEDS */
@mixin snapThumb() {
// LEGACY: TODO: refactor when .snap-thumb in New Entry dialog is refactored
$d: 50px;
$d: 30px;
border: 1px solid $colorInteriorBorder;
cursor: pointer;
width: $d;
@ -269,6 +391,64 @@
.l-sticky-headers .l-tabular-body { overflow: auto; }
}
.c-notebook-snapshot {
flex: 1 1 auto;
display: flex;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
&__header {
flex: 0 0 auto;
}
&__image {
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
flex: 1 1 auto;
}
}
/****************************** SNAPSHOT CONTAINER */
.c-snapshots-h {
// Is hidden when the parent div l-shell__drawer is collapsed, so no worries about padding, etc.
display: flex;
flex-direction: column;
overflow: hidden;
padding: $interiorMarginLg;
> * {
flex: 1 1 auto;
&:first-child {
flex: 0 0 auto;
}
}
> * + * {
margin-top: $interiorMargin;
}
}
.c-snapshots {
flex-wrap: wrap;
overflow: auto;
.c-snapshot {
margin: 0 $interiorMarginSm $interiorMarginSm 0;
}
.hint {
font-size: 1.25em;
font-style: italic;
opacity: 0.7;
padding: $interiorMarginLg;
text-align: center;
}
}
/****************************** PAINTERRO OVERRIDES */
.annotation-dialog .abs.editor {
border-radius: 0;
@ -401,7 +581,8 @@ body.mobile {
&.phone.portrait {
.c-notebook__head,
.c-notebook__entry {
.c-notebook__entry,
.c-ne__time-and-content {
flex-direction: column;
> [class*="__"] + [class*="__"] {
@ -413,9 +594,14 @@ body.mobile {
.c-notebook__entry {
[class*="text"] {
min-height: 0;
padding: 0;
pointer-events: none;
}
}
}
}
/****************************** INDICATOR */
.c-indicator.has-new-snapshot {
$c: $colorOk;
@include pulseProp($animName: flashSnapshot, $dur: 500ms, $iter: infinite, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0));
}

View File

@ -47,3 +47,4 @@
@import "../ui/preview/preview.scss";
@import "../ui/toolbar/components/toolbar-checkbox.scss";
@import "./notebook.scss";
@import "../plugins/notebook/components/sidebar.scss";

View File

@ -29,7 +29,7 @@
>
<div class="c-so-view__header">
<div class="c-object-label"
:class="cssClass"
:class="[cssClass, classList]"
>
<div class="c-object-label__name">
{{ domainObject && domainObject.name }}
@ -99,6 +99,16 @@ export default {
complexContent
}
},
computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
}
},
methods: {
expand() {
let objectView = this.$refs.objectView,

View File

@ -1,6 +1,7 @@
<template>
<a
class="c-tree__item__label c-object-label"
:class="classList"
draggable="true"
:href="objectLink"
@dragstart="dragStart"
@ -43,6 +44,14 @@ export default {
};
},
computed: {
classList() {
const classList = this.observedObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
typeClass() {
let type = this.openmct.types.get(this.observedObject.type);
if (!type) {

View File

@ -47,6 +47,13 @@ export default {
)
}
},
watch: {
value(inputValue) {
if (!inputValue.length) {
this.clearInput();
}
}
},
methods: {
clearInput() {
// Clear the user's input and set 'active' to false

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, 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="u-contents"></div>
</template>

View File

@ -1,13 +1,13 @@
.c-path,
.c-location {
// Path is two or more items, not clickable
// Location used in Inspector, is clickable
display: flex;
flex-wrap: wrap;
&__item {
$m: $interiorMarginSm;
cursor: pointer;
display: flex;
align-items: center;
margin: 0 $m $m 0;
min-width: 0;
&:not(:last-child) {
&:after {
@ -15,10 +15,19 @@
content: $glyph-icon-arrow-right;
font-family: symbolsfont;
font-size: 0.7em;
margin-left: $m;
margin-left: $interiorMarginSm;
opacity: 0.8;
}
}
}
}
.c-location {
flex-wrap: wrap;
&__item {
cursor: pointer;
margin: 0 $interiorMarginSm $interiorMarginSm 0;
.c-object-label {
padding: 0;

View File

@ -8,7 +8,7 @@
></button>
<div
class="l-browse-bar__object-name--w c-object-label"
:class="type.cssClass"
:class="[ type.cssClass, classList ]"
>
<span
class="l-browse-bar__object-name c-object-label__name c-input-inline"
@ -33,13 +33,11 @@
@setView="setView"
/>
<!-- Action buttons -->
<NotebookMenuSwitcher v-if="notebookEnabled"
:domain-object="domainObject"
class="c-notebook-snapshot-menubutton"
/>
<div class="l-browse-bar__actions">
<button
v-if="notebookEnabled"
class="l-browse-bar__actions__notebook-entry c-button icon-notebook"
title="New Notebook entry"
@click="snapshot()"
></button>
<button
v-if="isViewEditable & !isEditing"
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
@ -91,26 +89,37 @@
</template>
<script>
import NotebookSnapshot from '../utils/notebook-snapshot';
import ViewSwitcher from './ViewSwitcher.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/notebook-menu-switcher.vue';
const PLACEHOLDER_OBJECT = {};
export default {
inject: ['openmct'],
components: {
NotebookMenuSwitcher,
ViewSwitcher
},
data: function () {
return {
notebookTypes: [],
showViewMenu: false,
showSaveMenu: false,
domainObject: PLACEHOLDER_OBJECT,
viewKey: undefined,
isEditing: this.openmct.editor.isEditing(),
notebookEnabled: false
notebookEnabled: this.openmct.types.get('notebook')
}
},
computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
},
@ -163,12 +172,6 @@ export default {
}
},
mounted: function () {
if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct);
this.notebookEnabled = true;
}
document.addEventListener('click', this.closeViewAndSaveMenu);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
@ -266,10 +269,6 @@ export default {
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
},
snapshot() {
let element = document.getElementsByClassName("l-shell__main-container")[0];
this.notebookSnapshot.capture(this.domainObject, element);
},
goToParent() {
window.location.hash = this.parentUrl;
}

View File

@ -34,6 +34,9 @@
</div>
<app-logo />
</div>
<div class="l-shell__drawer c-drawer c-drawer--push c-drawer--align-top"></div>
<multipane
class="l-shell__main"
type="horizontal"

View File

@ -6,6 +6,21 @@
flex-flow: column nowrap;
overflow: hidden;
&__drawer {
background: $drawerBg;
display: flex;
flex-direction: column;
height: 0;
min-height: 0;
max-height: 15%;
overflow: hidden;
transition: $transIn;
&.is-expanded {
height: max-content;
}
}
&__pane-tree {
width: 40%;
@ -15,7 +30,7 @@
@include cClickIconButton();
color: $colorKey !important;
position: absolute;
right: -2 * nth($shellPanePad, 2); // Needs to be -1 * when pane is collapsed
right: -18px;
top: 0;
transform: translateX(100%);
width: $mobileMenuIconD;
@ -51,13 +66,22 @@
}
}
&__pane-main {
.l-pane__header { display: none; }
}
body.mobile & {
&__pane-main,
&__pane-tree {
padding: $interiorMarginLg;
}
&__pane-tree {
background: linear-gradient(90deg, transparent 70%, rgba(black, 0.2) 99%, rgba(black, 0.3));
&[class*="--collapsed"] {
[class*="collapse-button"] {
right: -1 * nth($shellPanePad, 2);
right: -8px;
}
}
}
@ -190,14 +214,14 @@
&__main {
> .l-pane {
padding: nth($shellPanePad, 1) nth($shellPanePad, 2);
padding: nth($shellPanePad, 1) 0;
}
}
body.desktop & {
&__main {
// Top and bottom padding in container that holds tree, __pane-main and Inspector
padding: $shellMainPad;
padding: nth($shellPanePad, 1) 0;
min-height: 0;
> .l-pane {
@ -208,15 +232,17 @@
&__pane-tree,
&__pane-inspector {
max-width: 30%;
max-width: 70%;
}
&__pane-tree {
width: 300px;
padding-left: nth($shellPanePad, 2);
}
&__pane-inspector {
width: 200px;
padding-right: nth($shellPanePad, 2);
}
}
@ -242,3 +268,68 @@
}
}
}
.c-drawer {
/* New sliding overlay or push element to contain things
* Designed for mobile and compact desktop scenarios
* Variations:
* --overlays: position absolute, overlays neighboring elements
* --push: position relative, pushs/collapses neighboring elements
* --align-left, align-top: opens from left or top respectively
* &.is-expanded: applied when expanded.
*/
transition: $transOut;
min-height: 0;
min-width: 0;
overflow: hidden;
&:not(.is-expanded) {
// When collapsed, hide internal elements
> * {
display: none;
}
}
&.c-drawer--align-left {
height: 100%;
}
&.c-drawer--align-top {
// Need anything here?
}
&.c-drawer--overlays {
position: absolute;
z-index: 3;
&.is-expanded {
// Height and width must be set per usage
&.c-drawer--align-left {
box-shadow: rgba(black, 0.7) 3px 0 20px;
}
&.c-drawer--align-top {
box-shadow: rgba(black, 0.7) 0 3px 20px;
}
}
}
&.c-drawer--push {
position: relative;
&.is-expanded {
// Height and width must be set per usage
&.c-drawer--align-left {
box-shadow: rgba(black, 0.2) 3px 0 20px;
margin-right: $interiorMarginLg;
}
&.c-drawer--align-top {
box-shadow: rgba(black, 0.2) 0 3px 20px;
margin-bottom: $interiorMarginLg; // Not sure this is desired here
}
}
}
}

View File

@ -2,7 +2,8 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
padding-right: $interiorMarginSm;
//TODO: Do we need this???
//padding-right: $interiorMarginSm;
overflow: auto;
> * + * { margin-top: $interiorMargin; }
@ -23,22 +24,22 @@
&__tree {
flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix
padding-right: $interiorMarginSm;
}
}
.c-tree {
.c-tree,
.c-list {
@include userSelectNone();
overflow-x: hidden;
overflow-y: auto;
padding-right: $interiorMargin;
li {
position: relative;
&.c-tree__item-h { display: block; }
}
.c-tree {
margin-left: 15px;
&[class*="__item-h"] { display: block; }
+ li {
margin-top: 1px;
}
}
&__item {
@ -48,20 +49,13 @@
align-items: center;
cursor: pointer;
line-height: 110%;
padding: $interiorMargin - $aPad;
padding: $interiorMarginSm $interiorMargin;
transition: background 150ms ease;
> * + * {
margin-left: $interiorMarginSm;
}
&:hover {
background: $colorItemTreeHoverBg;
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
.c-tree__item__name {
[class*="__name"] {
color: $colorItemTreeHoverFg;
}
}
@ -69,12 +63,34 @@
&.is-navigated-object,
&.is-selected {
background: $colorItemTreeSelectedBg;
[class*="__name"] {
color: $colorItemTreeSelectedFg;
}
}
}
}
.c-tree {
.c-tree {
margin-left: 15px;
}
&__item {
> * + * {
margin-left: $interiorMarginSm;
}
&:hover {
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
}
.c-tree__item__name {
color: $colorItemTreeSelectedFg;
&.is-navigated-object,
&.is-selected {
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
}
@ -121,6 +137,19 @@
}
}
.c-list {
padding-right: $interiorMargin;
&__item {
&__name {
$p: $interiorMarginSm;
@include ellipsize();
padding-bottom: $p;
padding-top: $p;
}
}
}
.c-selector {
.c-tree-and-search__tree.c-tree {
border: 1px solid $colorInteriorBorder;

View File

@ -2,6 +2,7 @@
.l-multipane {
display: flex;
flex: 1 1 auto;
overflow: hidden;
&--horizontal,
> .l-pane {
@ -38,7 +39,7 @@
&__header {
display: flex;
align-items: center;
margin-bottom: $interiorMargin;
@include desktop() { margin-bottom: $interiorMargin; }
}
&--reacts {
@ -232,8 +233,11 @@
}
/************************** Horizontal Splitter Before */
// Inspector pane
// Example: Inspector pane
&[class*="-before"] {
margin-left: nth($shellPanePad, 2);
padding-left: nth($shellPanePad, 2);
> .l-pane__handle {
left: 0;
transform: translateX(floor($splitterHandleD / -2)); // Center over the pane edge
@ -247,8 +251,11 @@
}
/************************** Horizontal Splitter After */
// Tree pane
// Example: Tree pane
&[class*="-after"] {
margin-right: nth($shellPanePad, 2);
padding-right: nth($shellPanePad, 2);
> .l-pane__handle {
right: 0;
transform: translateX(floor($splitterHandleD / 2));

View File

@ -16,11 +16,10 @@
class="l-pane__handle"
@mousedown="start"
></div>
<div
v-if="label"
class="l-pane__header"
>
<span class="l-pane__label">{{ label }}</span>
<div class="l-pane__header">
<span v-if="label"
class="l-pane__label"
>{{ label }}</span>
<button
v-if="collapsable"
class="l-pane__collapse-button c-button"

View File

@ -1,14 +1,12 @@
.c-indicator {
@include cControl();
@include cClickIconButtonLayout();
button { text-transform: uppercase; }
background: none !important;
border-radius: $controlCr;
overflow: visible;
position: relative;
text-transform: uppercase;
button { text-transform: uppercase; }
&.no-minify {
// For items that cannot be minified
@ -42,7 +40,7 @@
a,
button,
s-button,
.s-button,
.c-button {
// Make <a> in label look like buttons
transition: $transIn;

View File

@ -57,8 +57,8 @@
<script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import Snapshot from '@/plugins/notebook/snapshot';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
import NotebookSnapshot from '../utils/notebook-snapshot';
export default {
components: {
@ -96,7 +96,7 @@ export default {
this.setView(view);
if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct);
this.notebookSnapshot = new Snapshot(this.openmct);
this.notebookEnabled = true;
}
},

View File

@ -9,22 +9,31 @@ define([
let browseObject;
let unobserve = undefined;
let currentObjectPath;
let isRoutingInProgress = false;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
isRoutingInProgress = true;
let navigatePath = results[1];
navigateToPath(navigatePath, params.view);
onParamsChanged(null, null, params);
});
openmct.router.on('change:params', function (newParams, oldParams, changed) {
openmct.router.on('change:params', onParamsChanged);
function onParamsChanged(newParams, oldParams, changed) {
if (isRoutingInProgress) {
return;
}
if (changed.view && browseObject) {
let provider = openmct
.objectViews
.getByProviderKey(changed.view);
viewObject(browseObject, provider);
}
});
}
function viewObject(object, viewProvider) {
currentObjectPath = openmct.router.path;
@ -49,6 +58,8 @@ define([
}
return pathToObjects(path).then((objects)=>{
isRoutingInProgress = false;
if (currentNavigation !== navigateCall) {
return; // Prevent race.
}

View File

@ -1,135 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
class NotebookSnapshot {
constructor(openmct) {
this.openmct = openmct;
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(domainObject, domElement) {
let type = this.openmct.types.get(domainObject.type),
embedObject = {
id: domainObject.identifier.key,
cssClass: type.cssClass,
name: domainObject.name
};
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot').then(function (blob) {
var reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(reader.result, embedObject);
}.bind(this);
}.bind(this));
}
/**
* @private
*/
_generateTaskForm(url) {
var taskForm = {
name: "Create a Notebook Entry",
hint: "Please select a Notebook",
sections: [{
rows: [
{
name: 'Entry',
key: 'entry',
control: 'textarea',
required: false,
cssClass: "l-textarea-sm"
},
{
name: 'Snap Preview',
key:"snapPreview",
control: "snap-view",
cssClass: "l-textarea-sm",
src: url
},
{
name: 'Save in Notebook',
key: 'notebook',
control: 'locator',
validate: validateLocation
}
]
}]
};
var overlayModel = {
title: taskForm.name,
message: 'Notebook Snapshot',
structure: taskForm,
value: {'entry': ""}
};
function validateLocation(newParentObj) {
return newParentObj.model.type === 'notebook';
}
return overlayModel;
}
/**
* @private
*/
_saveSnapShot(imageUrl, embedObject) {
let taskForm = this._generateTaskForm(imageUrl);
this.dialogService.getDialogResponse(
'overlay-dialog',
taskForm,
() => taskForm.value
).then(options => {
let snapshotObject = {
src: options.snapPreview || imageUrl
};
options.notebook.useCapability('mutation', function (model) {
var date = Date.now();
model.entries.push({
id: 'entry-' + date,
createdOn: date,
text: options.entry,
embeds: [{
name: embedObject.name,
cssClass: embedObject.cssClass,
type: embedObject.id,
id: 'embed-' + date,
createdOn: date,
snapshot: snapshotObject
}]
});
});
});
}
}
export default NotebookSnapshot;