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:
Andrew Henry
2018-12-04 09:09:09 -08:00
committed by Pete Richards
parent f06427cb3e
commit 32a0baa7a3
12 changed files with 180 additions and 88 deletions

View File

@@ -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);
}
}; };
/** /**

View File

@@ -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);
} }

View 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();
}
}

View File

@@ -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
}; };
}); });

View File

@@ -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;

View File

@@ -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";

View File

@@ -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() {

View File

@@ -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 () {

View File

@@ -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,

View File

@@ -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)) {

View 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);
}
}
};