Context menu actions (#2229)
* Adding jsdoc to Context Menu Registry * Remove attachTo function - make context menu gesture a mixin. Update object path when objects change. * Added context menu from arrow button. Some minor refactoring * Clarify variable naming * Moved Context Menu component * Reorder function definitions * Addressed code review comments
This commit is contained in:
committed by
Pete Richards
parent
f06427cb3e
commit
32a0baa7a3
12
src/MCT.js
12
src/MCT.js
@@ -255,6 +255,12 @@ define([
|
|||||||
MCT.prototype.legacyObject = function (domainObject) {
|
MCT.prototype.legacyObject = function (domainObject) {
|
||||||
let capabilityService = this.$injector.get('capabilityService');
|
let capabilityService = this.$injector.get('capabilityService');
|
||||||
|
|
||||||
|
function instantiate(model, keyString) {
|
||||||
|
var capabilities = capabilityService.getCapabilities(model, keyString);
|
||||||
|
model.id = keyString;
|
||||||
|
return new DomainObjectImpl(keyString, model, capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(domainObject)) {
|
if (Array.isArray(domainObject)) {
|
||||||
// an array of domain objects. [object, ...ancestors] representing
|
// an array of domain objects. [object, ...ancestors] representing
|
||||||
// a single object with a given chain of ancestors. We instantiate
|
// a single object with a given chain of ancestors. We instantiate
|
||||||
@@ -275,12 +281,6 @@ define([
|
|||||||
let oldModel = objectUtils.toOldFormat(domainObject);
|
let oldModel = objectUtils.toOldFormat(domainObject);
|
||||||
return instantiate(oldModel, keyString);
|
return instantiate(oldModel, keyString);
|
||||||
}
|
}
|
||||||
|
|
||||||
function instantiate(model, keyString) {
|
|
||||||
var capabilities = capabilityService.getCapabilities(model, keyString);
|
|
||||||
model.id = keyString;
|
|
||||||
return new DomainObjectImpl(keyString, model, capabilities);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 LegacyContextMenuAction from './LegacyContextMenuAction';
|
||||||
|
|
||||||
export default function LegacyActionAdapter(openmct, legacyActions) {
|
export default function LegacyActionAdapter(openmct, legacyActions) {
|
||||||
legacyActions
|
function contextualCategoryOnly(action) {
|
||||||
.filter(contextCategoryOnly)
|
if (action.category === 'contextual') {
|
||||||
.map(createContextMenuAction)
|
return true;
|
||||||
.forEach(openmct.contextMenu.registerAction);
|
|
||||||
|
|
||||||
function createContextMenuAction(LegacyAction) {
|
|
||||||
return {
|
|
||||||
name: LegacyAction.definition.name,
|
|
||||||
description: LegacyAction.definition.description,
|
|
||||||
cssClass: LegacyAction.definition.cssClass,
|
|
||||||
appliesTo(objectPath) {
|
|
||||||
let legacyObject = openmct.legacyObject(objectPath);
|
|
||||||
return LegacyAction.appliesTo({
|
|
||||||
domainObject: legacyObject
|
|
||||||
});
|
|
||||||
},
|
|
||||||
invoke(objectPath) {
|
|
||||||
let context = {
|
|
||||||
category: 'contextual',
|
|
||||||
domainObject: openmct.legacyObject(objectPath)
|
|
||||||
}
|
|
||||||
let legacyAction = new LegacyAction(context);
|
|
||||||
|
|
||||||
if (!legacyAction.getMetadata) {
|
|
||||||
let metadata = Object.create(LegacyAction.definition);
|
|
||||||
metadata.context = context;
|
|
||||||
legacyAction.getMetadata = function () {
|
|
||||||
return metadata;
|
|
||||||
}.bind(legacyAction);
|
|
||||||
}
|
|
||||||
legacyAction.perform();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
console.warn(`DEPRECATION WARNING: Action ${action.definition.key} in bundle ${action.bundle.path} is non-contextual and should be migrated.`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function contextCategoryOnly(action) {
|
legacyActions.filter(contextualCategoryOnly)
|
||||||
return action.category === 'contextual';
|
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
|
||||||
}
|
.forEach(openmct.contextMenu.registerAction);
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/adapter/actions/LegacyContextMenuAction.js
Normal file
57
src/adapter/actions/LegacyContextMenuAction.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { timingSafeEqual } from "crypto";
|
||||||
|
|
||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export default class LegacyContextMenuAction {
|
||||||
|
constructor(openmct, LegacyAction) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.name = LegacyAction.definition.name;
|
||||||
|
this.description = LegacyAction.definition.description;
|
||||||
|
this.cssClass = LegacyAction.definition.cssClass;
|
||||||
|
this.LegacyAction = LegacyAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath) {
|
||||||
|
let legacyObject = this.openmct.legacyObject(objectPath);
|
||||||
|
return this.LegacyAction.appliesTo({
|
||||||
|
domainObject: legacyObject
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke(objectPath) {
|
||||||
|
let context = {
|
||||||
|
category: 'contextual',
|
||||||
|
domainObject: this.openmct.legacyObject(objectPath)
|
||||||
|
}
|
||||||
|
let legacyAction = new this.LegacyAction(context);
|
||||||
|
|
||||||
|
if (!legacyAction.getMetadata) {
|
||||||
|
let metadata = Object.create(this.LegacyAction.definition);
|
||||||
|
metadata.context = context;
|
||||||
|
legacyAction.getMetadata = function () {
|
||||||
|
return metadata;
|
||||||
|
}.bind(legacyAction);
|
||||||
|
}
|
||||||
|
legacyAction.perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ define([
|
|||||||
'./telemetry/TelemetryAPI',
|
'./telemetry/TelemetryAPI',
|
||||||
'./indicators/IndicatorAPI',
|
'./indicators/IndicatorAPI',
|
||||||
'./notifications/NotificationAPI',
|
'./notifications/NotificationAPI',
|
||||||
'./contextMenu/ContextMenuRegistry',
|
'./contextMenu/ContextMenuAPI',
|
||||||
'./Editor'
|
'./Editor'
|
||||||
|
|
||||||
], function (
|
], function (
|
||||||
@@ -39,7 +39,7 @@ define([
|
|||||||
TelemetryAPI,
|
TelemetryAPI,
|
||||||
IndicatorAPI,
|
IndicatorAPI,
|
||||||
NotificationAPI,
|
NotificationAPI,
|
||||||
ContextMenuRegistry,
|
ContextMenuAPI,
|
||||||
EditorAPI
|
EditorAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@@ -51,6 +51,6 @@ define([
|
|||||||
IndicatorAPI: IndicatorAPI,
|
IndicatorAPI: IndicatorAPI,
|
||||||
NotificationAPI: NotificationAPI.default,
|
NotificationAPI: NotificationAPI.default,
|
||||||
EditorAPI: EditorAPI,
|
EditorAPI: EditorAPI,
|
||||||
ContextMenuRegistry: ContextMenuRegistry.default
|
ContextMenuRegistry: ContextMenuAPI.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,10 +20,16 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import ContextMenuComponent from '../../ui/components/controls/ContextMenu.vue';
|
import ContextMenuComponent from './ContextMenu.vue';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
class ContextMenuRegistry {
|
/**
|
||||||
|
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
|
||||||
|
* custom HTML elements.
|
||||||
|
* @interface ContextMenuAPI
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
class ContextMenuAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._allActions = [];
|
this._allActions = [];
|
||||||
this._activeContextMenu = undefined;
|
this._activeContextMenu = undefined;
|
||||||
@@ -32,37 +38,44 @@ class ContextMenuRegistry {
|
|||||||
this.registerAction = this.registerAction.bind(this);
|
this.registerAction = this.registerAction.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
|
||||||
|
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
|
||||||
|
*
|
||||||
|
* @interface ContextMenuAction
|
||||||
|
* @memberof module:openmct
|
||||||
|
* @property {string} name the human-readable name of this view
|
||||||
|
* @property {string} description a longer-form description (typically
|
||||||
|
* a single sentence or short paragraph) of this kind of view
|
||||||
|
* @property {string} cssClass the CSS class to apply to labels for this
|
||||||
|
* view (to add icons, for instance)
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @method appliesTo
|
||||||
|
* @memberof module:openmct.ContextMenuAction#
|
||||||
|
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
|
||||||
|
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Code to be executed when the action is selected from a context menu
|
||||||
|
* @method invoke
|
||||||
|
* @memberof module:openmct.ContextMenuAction#
|
||||||
|
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {ContextMenuAction} actionDefinition
|
||||||
|
*/
|
||||||
registerAction(actionDefinition) {
|
registerAction(actionDefinition) {
|
||||||
this._allActions.push(actionDefinition);
|
this._allActions.push(actionDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachTo(targetElement, objectPath, eventName) {
|
|
||||||
eventName = eventName || 'contextmenu';
|
|
||||||
|
|
||||||
if (eventName !== 'contextmenu' && eventName !== 'click') {
|
|
||||||
throw `'${eventName}' event not supported for context menu`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let showContextMenu = (event) => {
|
|
||||||
this._showContextMenuForObjectPath(event, objectPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
targetElement.addEventListener(eventName, showContextMenu);
|
|
||||||
|
|
||||||
return function detach() {
|
|
||||||
targetElement.removeEventListener(eventName, showContextMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_showContextMenuForObjectPath(event, objectPath) {
|
_showContextMenuForObjectPath(objectPath, x, y) {
|
||||||
let applicableActions = this._allActions.filter(
|
let applicableActions = this._allActions.filter(
|
||||||
(action) => action.appliesTo(objectPath));
|
(action) => action.appliesTo(objectPath));
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (this._activeContextMenu) {
|
if (this._activeContextMenu) {
|
||||||
this._hideActiveContextMenu();
|
this._hideActiveContextMenu();
|
||||||
}
|
}
|
||||||
@@ -71,7 +84,7 @@ class ContextMenuRegistry {
|
|||||||
this._activeContextMenu.$mount();
|
this._activeContextMenu.$mount();
|
||||||
document.body.appendChild(this._activeContextMenu.$el);
|
document.body.appendChild(this._activeContextMenu.$el);
|
||||||
|
|
||||||
let position = this._calculatePopupPosition(event, this._activeContextMenu.$el);
|
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
|
||||||
this._activeContextMenu.$el.style.left = `${position.x}px`;
|
this._activeContextMenu.$el.style.left = `${position.x}px`;
|
||||||
this._activeContextMenu.$el.style.top = `${position.y}px`;
|
this._activeContextMenu.$el.style.top = `${position.y}px`;
|
||||||
|
|
||||||
@@ -81,24 +94,22 @@ class ContextMenuRegistry {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_calculatePopupPosition(event, menuElement) {
|
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
|
||||||
let x = event.clientX;
|
|
||||||
let y = event.clientY;
|
|
||||||
let menuDimensions = menuElement.getBoundingClientRect();
|
let menuDimensions = menuElement.getBoundingClientRect();
|
||||||
let diffX = (x + menuDimensions.width) - document.body.clientWidth;
|
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
||||||
let diffY = (y + menuDimensions.height) - document.body.clientHeight;
|
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
||||||
|
|
||||||
if (diffX > 0) {
|
if (overflowX > 0) {
|
||||||
x = x - diffX;
|
eventPosX = eventPosX - overflowX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffY > 0) {
|
if (overflowY > 0) {
|
||||||
y = y - diffY;
|
eventPosY = eventPosY - overflowY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: x,
|
x: eventPosX,
|
||||||
y: y
|
y: eventPosY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -127,4 +138,4 @@ class ContextMenuRegistry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default ContextMenuRegistry;
|
export default ContextMenuAPI;
|
||||||
@@ -24,6 +24,6 @@
|
|||||||
// Meant for use as a single line import in Vue SFC's.
|
// Meant for use as a single line import in Vue SFC's.
|
||||||
// Do not include anything that renders to CSS!
|
// Do not include anything that renders to CSS!
|
||||||
@import "constants";
|
@import "constants";
|
||||||
@import "constants-espresso"; // TEMP
|
//@import "constants-espresso"; // TEMP
|
||||||
//@import "constants-snow"; // TEMP
|
@import "constants-snow"; // TEMP
|
||||||
@import "mixins";
|
@import "mixins";
|
||||||
@@ -12,9 +12,10 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import ObjectLink from '../mixins/object-link';
|
import ObjectLink from '../mixins/object-link';
|
||||||
|
import ContextMenuGesture from '../mixins/context-menu-gesture';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ObjectLink],
|
mixins: [ObjectLink, ContextMenuGesture],
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: Object
|
domainObject: Object
|
||||||
@@ -31,8 +32,6 @@ export default {
|
|||||||
});
|
});
|
||||||
this.$once('hook:destroyed', removeListener);
|
this.$once('hook:destroyed', removeListener);
|
||||||
}
|
}
|
||||||
let detachContextMenu = this.openmct.contextMenu.attachTo(this.$el, this.objectPath);
|
|
||||||
this.$once('hook:destroyed', detachContextMenu);
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
typeClass() {
|
typeClass() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{{ domainObject.name }}
|
{{ domainObject.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="l-browse-bar__context-actions c-disclosure-button"></div>
|
<div class="l-browse-bar__context-actions c-disclosure-button" @click="showContextMenu"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="l-browse-bar__end">
|
<div class="l-browse-bar__end">
|
||||||
@@ -81,6 +81,11 @@
|
|||||||
this.openmct.notifications.error('Error saving objects');
|
this.openmct.notifications.error('Error saving objects');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
showContextMenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
|||||||
@@ -239,7 +239,6 @@
|
|||||||
import MctTree from './mct-tree.vue';
|
import MctTree from './mct-tree.vue';
|
||||||
import ObjectView from './ObjectView.vue';
|
import ObjectView from './ObjectView.vue';
|
||||||
import MctTemplate from '../legacy/mct-template.vue';
|
import MctTemplate from '../legacy/mct-template.vue';
|
||||||
import ContextMenu from '../controls/ContextMenu.vue';
|
|
||||||
import CreateButton from '../controls/CreateButton.vue';
|
import CreateButton from '../controls/CreateButton.vue';
|
||||||
import search from '../controls/search.vue';
|
import search from '../controls/search.vue';
|
||||||
import multipane from '../controls/multipane.vue';
|
import multipane from '../controls/multipane.vue';
|
||||||
@@ -283,7 +282,6 @@
|
|||||||
MctTree,
|
MctTree,
|
||||||
ObjectView,
|
ObjectView,
|
||||||
'mct-template': MctTemplate,
|
'mct-template': MctTemplate,
|
||||||
ContextMenu,
|
|
||||||
CreateButton,
|
CreateButton,
|
||||||
search,
|
search,
|
||||||
multipane,
|
multipane,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
this.domainObject = this.node.object;
|
this.domainObject = this.node.object;
|
||||||
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
||||||
this.domainObject = newObject;
|
this.domainObject = newObject;
|
||||||
|
this.node.objectPath.splice(0, 1, newObject);
|
||||||
});
|
});
|
||||||
this.$once('hook:destroyed', removeListener);
|
this.$once('hook:destroyed', removeListener);
|
||||||
if (this.openmct.composition.get(this.node.object)) {
|
if (this.openmct.composition.get(this.node.object)) {
|
||||||
|
|||||||
24
src/ui/components/mixins/context-menu-gesture.js
Normal file
24
src/ui/components/mixins/context-menu-gesture.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
'objectPath': {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//TODO: touch support
|
||||||
|
this.$el.addEventListener('contextmenu', this.showContextMenu);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.$el.removeEventListener('contextMenu', this.showContextMenu);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showContextMenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.openmct.contextMenu._showContextMenuForObjectPath(this.objectPath, event.clientX, event.clientY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user