Notebook context menu (#2888)

Notebook popup menu fix
Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Nikhil 2020-04-10 15:17:01 -07:00 committed by GitHub
parent 99c095a69f
commit da7b93f9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 272 deletions

View File

@ -0,0 +1,21 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="(item, index) in popupMenuItems"
:key="index"
:class="item.cssClass"
:title="item.name"
@click="item.callback"
>
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['popupMenuItems']
}
</script>

View File

@ -12,24 +12,7 @@
:class="embed.cssClass" :class="embed.cssClass"
@click="changeLocation" @click="changeLocation"
>{{ embed.name }}</a> >{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@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>
<div v-if="embed.snapshot" <div v-if="embed.snapshot"
class="c-ne__embed__time" class="c-ne__embed__time"
@ -42,15 +25,17 @@
<script> <script>
import Moment from 'moment'; import Moment from 'moment';
import PopupMenu from './popup-menu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction'; import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro'; import Painterro from 'painterro';
import RemoveDialog from '../utils/removeDialog';
import SnapshotTemplate from './snapshot-template.html'; import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue'; import Vue from 'vue';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
PopupMenu
}, },
props: { props: {
embed: { embed: {
@ -62,23 +47,35 @@ export default {
removeActionString: { removeActionString: {
type: String, type: String,
default() { default() {
return 'Remove Embed'; return 'Remove This Embed';
} }
} }
}, },
data() { data() {
return { return {
actions: [this.removeEmbedAction()], popupMenuItems: []
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
} }
}, },
watch: { watch: {
}, },
beforeMount() { mounted() {
this.populateActionMenu(); this.addPopupMenuItems();
}, },
methods: { methods: {
addPopupMenuItems() {
const removeEmbed = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
const preview = {
cssClass: 'icon-eye-open',
name: 'Preview',
callback: this.previewEmbed.bind(this)
}
this.popupMenuItems = [removeEmbed, preview];
},
annotateSnapshot() { annotateSnapshot() {
const self = this; const self = this;
@ -183,6 +180,14 @@ export default {
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);
}, },
getRemoveDialog() {
const options = {
name: this.removeActionString,
callback: this.removeEmbed.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
openSnapshot() { openSnapshot() {
const self = this; const self = this;
const snapshot = new Vue({ const snapshot = new Vue({
@ -214,53 +219,17 @@ export default {
] ]
}); });
}, },
populateActionMenu() { previewEmbed() {
const self = this; const self = this;
const actions = [new PreviewAction(self.openmct)]; const previewAction = new PreviewAction(self.openmct);
previewAction.invoke(JSON.parse(self.embed.objectPath));
},
removeEmbed(success) {
if (!success) {
return;
}
actions.forEach((action) => { this.$emit('removeEmbed', this.embed.id);
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke(JSON.parse(self.embed.objectPath));
}
});
});
},
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) { updateEmbed(embed) {
this.$emit('updateEmbed', embed); this.$emit('updateEmbed', embed);

View File

@ -10,24 +10,9 @@
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} >&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span> </span>
</div> </div>
<a class="l-browse-bar__context-actions c-disclosure-button" <PopupMenu v-if="snapshots.length > 0"
@click="toggleActionMenu" :popup-menu-items="popupMenuItems"
></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> </div>
@ -62,14 +47,16 @@
<script> <script>
import NotebookEmbed from './notebook-embed.vue'; import NotebookEmbed from './notebook-embed.vue';
import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container'; import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants'; import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default { export default {
inject: ['openmct', 'snapshotContainer'], inject: ['openmct', 'snapshotContainer'],
components: { components: {
NotebookEmbed NotebookEmbed,
PopupMenu
}, },
props: { props: {
toggleSnapshot: { toggleSnapshot: {
@ -81,54 +68,47 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.removeAllSnapshotAction()], popupMenuItems: [],
removeActionString: 'Delete all snapshots',
snapshots: [] snapshots: []
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated); this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots(); this.snapshots = this.snapshotContainer.getSnapshots();
}, },
beforeDestory() { beforeDestory() {
}, },
methods: { methods: {
addPopupMenuItems() {
const removeSnapshot = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removeSnapshot];
},
close() { close() {
this.toggleSnapshot(); this.toggleSnapshot();
}, },
getNotebookSnapshotMaxCount() { getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT; return NOTEBOOK_SNAPSHOT_MAX_COUNT;
}, },
removeAllSnapshotAction() { getRemoveDialog() {
const self = this; const options = {
name: this.removeActionString,
return { callback: this.removeAllSnapshots.bind(this)
name: 'Delete All Snapshots', }
cssClass: 'icon-trash', const removeDialog = new RemoveDialog(this.openmct, options);
perform: function (embed) { removeDialog.show();
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() { removeAllSnapshots(success) {
if (!success) {
return;
}
this.snapshotContainer.removeAllSnapshots(); this.snapshotContainer.removeAllSnapshots();
}, },
removeSnapshot(id) { removeSnapshot(id) {
@ -141,9 +121,6 @@ export default {
event.dataTransfer.setData('text/plain', snapshot.id); event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id); event.dataTransfer.setData('snapshot/id', snapshot.id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) { updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot); this.snapshotContainer.updateSnapshot(snapshot);
} }

View File

@ -9,32 +9,19 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span> >{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@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> </div>
</template> </template>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultPageId: { defaultPageId: {
type: String, type: String,
@ -55,7 +42,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deletePage()] popupMenuItems: [],
removeActionString: `Delete ${this.pageTitle}`
} }
}, },
watch: { watch: {
@ -64,40 +52,37 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deletePage() { addPopupMenuItems() {
const self = this; const removePage = {
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error", }
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [ this.popupMenuItems = [removePage];
{ },
label: "No", deletePage(success) {
callback: () => { if (!success) {
dialog.dismiss(); return;
} }
},
{ this.$emit('deletePage', this.page.id);
label: "Yes", },
emphasis: true, getRemoveDialog() {
callback: () => { const message = 'This action will delete this page and all of its entries. Do you want to continue?';
self.$emit('deletePage', id); const options = {
dialog.dismiss(); name: this.removeActionString,
} callback: this.deletePage.bind(this),
} message
] }
}); const removeDialog = new RemoveDialog(this.openmct, options);
} removeDialog.show();
};
}, },
selectPage(event) { selectPage(event) {
const target = event.target; const target = event.target;
@ -117,10 +102,6 @@ export default {
this.$emit('selectPage', id); this.$emit('selectPage', id);
}, },
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) { toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span'); const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected; pageTitle.contentEditable = page.isSelected;

View File

@ -0,0 +1,93 @@
<template>
<button
class="c-popup-menu-button c-disclosure-button"
title="popup menu"
@click="showMenuItems"
>
</button>
</template>
<script>
import MenuItems from './menu-items.vue';
import Vue from 'vue';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
popupMenuItems: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
menuItems: null
}
},
mounted() {
},
methods: {
calculateMenuPosition(event, element) {
let eventPosX = event.clientX;
let eventPosY = event.clientY;
let menuDimensions = element.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
}
},
hideMenuItems() {
document.body.removeChild(this.menuItems.$el);
this.menuItems.$destroy();
this.menuItems = null;
document.removeEventListener('click', this.hideMenuItems);
return;
},
showMenuItems($event) {
const menuItems = new Vue({
components: {
MenuItems
},
provide: {
popupMenuItems: this.popupMenuItems
},
template: '<MenuItems />'
});
this.menuItems = menuItems;
menuItems.$mount();
const element = this.menuItems.$el;
document.body.appendChild(element);
const position = this.calculateMenuPosition($event, element);
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
setTimeout(() => {
document.addEventListener('click', this.hideMenuItems);
}, 0);
}
}
}
</script>

View File

@ -9,24 +9,7 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span> >{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@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> </div>
</template> </template>
@ -34,10 +17,14 @@
</style> </style>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultSectionId: { defaultSectionId: {
type: String, type: String,
@ -58,7 +45,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deleteSectionAction()] popupMenuItems: [],
removeActionString: `Delete ${this.sectionTitle}`
} }
}, },
watch: { watch: {
@ -67,40 +55,38 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deleteSectionAction() { addPopupMenuItems() {
const self = this; const removeSection = {
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error", }
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [ this.popupMenuItems = [removeSection];
{ },
label: "No", deleteSection(success) {
callback: () => { if (!success) {
dialog.dismiss(); return;
} }
},
{ this.$emit('deleteSection', this.section.id);
label: "Yes", },
emphasis: true, getRemoveDialog() {
callback: () => { const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
self.$emit('deleteSection', id); const options = {
dialog.dismiss(); name: this.removeActionString,
} callback: this.deleteSection.bind(this),
} message
] }
});
} const removeDialog = new RemoveDialog(this.openmct, options);
}; removeDialog.show();
}, },
selectSection(event) { selectSection(event) {
const target = event.target; const target = event.target;
@ -121,9 +107,6 @@ export default {
this.$emit('selectSection', id); this.$emit('selectSection', id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) { toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span'); const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected; sectionTitle.contentEditable = section.isSelected;

View File

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

View File

@ -0,0 +1,36 @@
export default class RemoveDialog {
constructor(openmct, options) {
this.name = options.name;
this.openmct = openmct;
this.callback = options.callback;
this.cssClass = options.cssClass || 'icon-trash';
this.description = options.description || 'Remove action dialog';
this.iconClass = "error";
this.key = 'remove';
this.message = options.message || `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`;
}
show() {
const dialog = this.openmct.overlays.dialog({
iconClass: this.iconClass,
message: this.message,
buttons: [
{
label: "Ok",
callback: () => {
this.callback(true);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
this.callback(false);
dialog.dismiss();
}
}
]
});
}
}