[Notebook] V2.0 development #2666 (#2755)

* Notebook v2.0
Co-authored-by: charlesh88 <charlesh88@gmail.com>
This commit is contained in:
Nikhil
2020-03-31 12:11:11 -07:00
committed by GitHub
parent 7b060509f5
commit e7e5116773
59 changed files with 4022 additions and 1302 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<div class="c-notebook-snapshot">
<!-- parent container sets up this for flex column layout -->
<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="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass"
>
<span class="c-object-label__name">{{ embed.name }}</span>
</span>
</div>
</div>
<div class="l-browse-bar__end">
<div class="l-browse-bar__snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="l-browse-bar__annotate-button c-button icon-pencil" title="Annotate" @click="annotateSnapshot">
<span class="title-label">Annotate</span>
</a>
</div>
</div>
<div class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
>
</div>
</div>