mirror of
https://github.com/nasa/openmct.git
synced 2025-06-18 15:18:12 +00:00
* Notebook v2.0 Co-authored-by: charlesh88 <charlesh88@gmail.com>
This commit is contained in:
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);
|
||||
}
|
Reference in New Issue
Block a user