mirror of
https://github.com/nasa/openmct.git
synced 2025-01-31 16:36:13 +00:00
* Notebook v2.0 Co-authored-by: charlesh88 <charlesh88@gmail.com>
This commit is contained in:
parent
7b060509f5
commit
e7e5116773
272
src/plugins/notebook/components/notebook-embed.vue
Normal file
272
src/plugins/notebook/components/notebook-embed.vue
Normal 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>
|
316
src/plugins/notebook/components/notebook-entry.vue
Normal file
316
src/plugins/notebook/components/notebook-entry.vue
Normal 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>
|
114
src/plugins/notebook/components/notebook-menu-switcher.vue
Normal file
114
src/plugins/notebook/components/notebook-menu-switcher.vue
Normal 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>
|
152
src/plugins/notebook/components/notebook-snapshot-container.vue
Normal file
152
src/plugins/notebook/components/notebook-snapshot-container.vue
Normal 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"
|
||||
> {{ 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>
|
@ -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>
|
528
src/plugins/notebook/components/notebook.vue
Normal file
528
src/plugins/notebook/components/notebook.vue
Normal 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>
|
132
src/plugins/notebook/components/page-collection.vue
Normal file
132
src/plugins/notebook/components/page-collection.vue
Normal 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>
|
146
src/plugins/notebook/components/page-component.vue
Normal file
146
src/plugins/notebook/components/page-component.vue
Normal 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>
|
50
src/plugins/notebook/components/search-results.vue
Normal file
50
src/plugins/notebook/components/search-results.vue
Normal 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>
|
113
src/plugins/notebook/components/section-collection.vue
Normal file
113
src/plugins/notebook/components/section-collection.vue
Normal 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>
|
149
src/plugins/notebook/components/section-component.vue
Normal file
149
src/plugins/notebook/components/section-component.vue
Normal 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>
|
119
src/plugins/notebook/components/sidebar.scss
Normal file
119
src/plugins/notebook/components/sidebar.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
189
src/plugins/notebook/components/sidebar.vue
Normal file
189
src/plugins/notebook/components/sidebar.vue
Normal 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>
|
@ -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>
|
3
src/plugins/notebook/notebook-constants.js
Normal file
3
src/plugins/notebook/notebook-constants.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
|
||||
export const NOTEBOOK_DEFAULT = 'DEFAULT';
|
||||
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
82
src/plugins/notebook/snapshot-container.js
Normal file
82
src/plugins/notebook/snapshot-container.js
Normal 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);
|
||||
}
|
||||
}
|
74
src/plugins/notebook/snapshot.js
Normal file
74
src/plugins/notebook/snapshot.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
@ -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;
|
||||
});
|
@ -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;
|
||||
});
|
194
src/plugins/notebook/utils/notebook-entries.js
Normal file
194
src/plugins/notebook/utils/notebook-entries.js
Normal 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);
|
||||
}
|
40
src/plugins/notebook/utils/notebook-storage.js
Normal file
40
src/plugins/notebook/utils/notebook-storage.js
Normal 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));
|
||||
}
|
45
src/plugins/notebook/utils/popup-menu.js
Normal file
45
src/plugins/notebook/utils/popup-menu.js
Normal 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);
|
||||
}
|
@ -171,7 +171,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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
@ -331,7 +336,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;
|
||||
|
@ -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;
|
||||
|
@ -26,7 +26,7 @@
|
||||
$mobileListIconSize: 30px;
|
||||
$mobileTitleDescH: 35px;
|
||||
$mobileOverlayMargin: 20px;
|
||||
$mobileMenuIconD: 34px;
|
||||
$mobileMenuIconD: 25px;
|
||||
$phoneItemH: floor($gridItemMobile / 4);
|
||||
$tabletItemH: floor($gridItemMobile / 3);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -148,6 +148,8 @@ $glyph-icon-filter-outline: '\e927';
|
||||
$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';
|
||||
|
@ -54,7 +54,6 @@ button {
|
||||
background: $splitterBtnColorBg;
|
||||
color: $splitterBtnColorFg;
|
||||
border-radius: $smallCr;
|
||||
font-size: 6px;
|
||||
line-height: 90%;
|
||||
padding: 3px 10px;
|
||||
|
||||
@ -62,6 +61,10 @@ button {
|
||||
background: $colorBtnBgHov;
|
||||
color: $colorBtnFgHov;
|
||||
}
|
||||
|
||||
@include desktop() {
|
||||
font-size: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@ -139,6 +142,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.
|
||||
@ -642,7 +665,7 @@ select {
|
||||
$pTB: 2px;
|
||||
padding: $pTB $pLR;
|
||||
|
||||
&:hover {
|
||||
@include hover() {
|
||||
background: $editUIBaseColorHov !important;
|
||||
color: $editUIBaseColorFg !important;
|
||||
}
|
||||
@ -657,7 +680,7 @@ select {
|
||||
color: $colorBtnCautionBg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include hover() {
|
||||
background: rgba($colorBtnCautionBgHov, 0.2);
|
||||
:before {
|
||||
color: $colorBtnCautionBgHov;
|
||||
|
@ -82,6 +82,8 @@
|
||||
.icon-suitcase { @include glyphBefore($glyph-icon-suitcase); }
|
||||
.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); }
|
||||
|
@ -83,4 +83,8 @@
|
||||
&__object-name {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
&__object-details {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
@ -408,10 +420,15 @@
|
||||
@include cControl();
|
||||
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;
|
||||
|
File diff suppressed because it is too large
Load Diff
2
src/styles/fonts/Open-MCT-Symbols-16px.svg
Executable file → Normal file
2
src/styles/fonts/Open-MCT-Symbols-16px.svg
Executable file → Normal file
@ -50,6 +50,8 @@
|
||||
<glyph unicode="" glyph-name="icon-suitcase" d="M768 704c-0.080 70.66-57.34 127.92-127.993 128h-256.007c-70.66-0.080-127.92-57.34-128-127.993v-128.007h-64v-768h640v768h-64zM384 703.88l0.12 0.12 255.88-0.12v-127.88h-256zM0 512v-640c0.102-35.305 28.695-63.898 63.99-64h64.010v768h-64c-35.305-0.102-63.898-28.695-64-63.99v-0.010zM960 576h-64v-768h64c35.305 0.102 63.898 28.695 64 63.99v640.010c-0.102 35.305-28.695 63.898-63.99 64h-0.010z" />
|
||||
<glyph unicode="" glyph-name="icon-cursor-locked" horiz-adv-x="768" d="M704 512h-64v64c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-64h-64c-35.301-0.113-63.887-28.699-64-63.989v-576.011c0.113-35.301 28.699-63.887 63.989-64h640.011c35.301 0.113 63.887 28.699 64 63.989v576.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 576c0 70.692 57.308 128 128 128s128-57.308 128-128v0-64h-256zM533.4-64l-128 128-43-85-170.4 383.6 383.6-170.2-85-43 128-128z" />
|
||||
<glyph unicode="" glyph-name="icon-flag" d="M192 192h832l-192 320 192 320h-896c-70.606-0.215-127.785-57.394-128-127.979v-896.021h192z" />
|
||||
<glyph unicode="" glyph-name="icon-eye-disabled" d="M209.46 223.32q-7.46 9.86-14.26 20.28c-14.737 21.984-27.741 47.184-37.759 73.847l-0.841 2.553c11.078 29.259 24.068 54.443 39.51 77.869l-0.91-1.469c23.221 34.963 50.705 64.8 82.207 89.793l0.793 0.607c57.663 45.719 130.179 75.053 209.311 79.947l1.069 0.053 114.48 140.88c-27.366 5.017-58.869 7.898-91.041 7.92h-0.019c-245.8 0-452.2-168-510.8-395.6 21.856-82.93 60.906-154.847 113.325-214.773l-0.525 0.613zM814.76 416.92q7.52-10 14.44-20.52c14.737-21.984 27.741-47.184 37.759-73.847l0.841-2.553c-10.859-29.216-23.863-54.416-39.447-77.748l0.847 1.348c-23.221-34.963-50.705-64.8-82.207-89.793l-0.793-0.607c-57.762-45.834-130.437-75.216-209.743-80.049l-1.057-0.051-114.46-140.86c27.346-4.988 58.817-7.84 90.955-7.84 0.037 0 0.074 0 0.111 0h-0.005c245.8 0 452.2 168 510.8 395.6-21.856 82.93-60.906 154.847-113.325 214.773l0.525-0.613zM832 832l-832-1024h192l832 1024h-192z" />
|
||||
<glyph unicode="" glyph-name="icon-notebook-page" d="M830 770h-830l-4-702c0-106.6 87.4-194 194-194h640c106.6 0 194 87.4 194 194v508c0 106.8-87.4 194-194 194zM832 386l-384-384-192 192v256l192-192 384 384v-256z" />
|
||||
<glyph unicode="" glyph-name="icon-arrows-right-left" d="M1024 320l-448-512v1024zM448 832l-448-512 448-512z" />
|
||||
<glyph unicode="" glyph-name="icon-arrows-up-down" d="M512 832l512-448h-1024zM0 256l512-448 512 448z" />
|
||||
<glyph unicode="" glyph-name="icon-bullet" d="M832 80c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" />
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
BIN
src/styles/fonts/Open-MCT-Symbols-16px.ttf
Executable file → Normal file
BIN
src/styles/fonts/Open-MCT-Symbols-16px.ttf
Executable file → Normal file
Binary file not shown.
BIN
src/styles/fonts/Open-MCT-Symbols-16px.woff
Executable file → Normal file
BIN
src/styles/fonts/Open-MCT-Symbols-16px.woff
Executable file → Normal file
Binary file not shown.
@ -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));
|
||||
}
|
||||
|
@ -43,3 +43,4 @@
|
||||
@import "../ui/preview/preview.scss";
|
||||
@import "../ui/toolbar/components/toolbar-checkbox.scss";
|
||||
@import "./notebook.scss";
|
||||
@import "../plugins/notebook/components/sidebar.scss";
|
||||
|
@ -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 }}
|
||||
@ -98,6 +98,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,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
@ -45,13 +60,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,14 +208,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 {
|
||||
@ -202,15 +226,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,3 +262,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
.c-tree-and-search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: $interiorMarginSm;
|
||||
overflow: auto;
|
||||
|
||||
> * + * { margin-top: $interiorMargin; }
|
||||
@ -22,22 +21,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 {
|
||||
@ -47,20 +46,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;
|
||||
}
|
||||
}
|
||||
@ -68,12 +60,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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,3 +150,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-list {
|
||||
padding-right: $interiorMargin;
|
||||
|
||||
&__item {
|
||||
&__name {
|
||||
$p: $interiorMarginSm;
|
||||
@include ellipsize();
|
||||
padding-bottom: $p;
|
||||
padding-top: $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
.l-multipane {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&--horizontal,
|
||||
> .l-pane {
|
||||
@ -35,7 +36,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));
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user