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) {
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)) {
// an array of domain objects. [object, ...ancestors] representing
// a single object with a given chain of ancestors. We instantiate
@ -275,12 +281,6 @@ define([
let oldModel = objectUtils.toOldFormat(domainObject);
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) {
legacyActions
.filter(contextCategoryOnly)
.map(createContextMenuAction)
.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();
}
function contextualCategoryOnly(action) {
if (action.category === 'contextual') {
return true;
}
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) {
return action.category === 'contextual';
}
legacyActions.filter(contextualCategoryOnly)
.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',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./contextMenu/ContextMenuRegistry',
'./contextMenu/ContextMenuAPI',
'./Editor'
], function (
@ -39,7 +39,7 @@ define([
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
ContextMenuRegistry,
ContextMenuAPI,
EditorAPI
) {
return {
@ -51,6 +51,6 @@ define([
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
ContextMenuRegistry: ContextMenuRegistry.default
ContextMenuRegistry: ContextMenuAPI.default
};
});

View File

@ -20,10 +20,16 @@
* 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';
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() {
this._allActions = [];
this._activeContextMenu = undefined;
@ -32,37 +38,44 @@ class ContextMenuRegistry {
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) {
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
*/
_showContextMenuForObjectPath(event, objectPath) {
_showContextMenuForObjectPath(objectPath, x, y) {
let applicableActions = this._allActions.filter(
(action) => action.appliesTo(objectPath));
event.preventDefault();
if (this._activeContextMenu) {
this._hideActiveContextMenu();
}
@ -71,7 +84,7 @@ class ContextMenuRegistry {
this._activeContextMenu.$mount();
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.top = `${position.y}px`;
@ -81,24 +94,22 @@ class ContextMenuRegistry {
/**
* @private
*/
_calculatePopupPosition(event, menuElement) {
let x = event.clientX;
let y = event.clientY;
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let diffX = (x + menuDimensions.width) - document.body.clientWidth;
let diffY = (y + menuDimensions.height) - document.body.clientHeight;
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (diffX > 0) {
x = x - diffX;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (diffY > 0) {
y = y - diffY;
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: x,
y: y
x: eventPosX,
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.
// Do not include anything that renders to CSS!
@import "constants";
@import "constants-espresso"; // TEMP
//@import "constants-snow"; // TEMP
//@import "constants-espresso"; // TEMP
@import "constants-snow"; // TEMP
@import "mixins";

View File

@ -12,9 +12,10 @@
<script>
import ObjectLink from '../mixins/object-link';
import ContextMenuGesture from '../mixins/context-menu-gesture';
export default {
mixins: [ObjectLink],
mixins: [ObjectLink, ContextMenuGesture],
inject: ['openmct'],
props: {
domainObject: Object
@ -31,8 +32,6 @@ export default {
});
this.$once('hook:destroyed', removeListener);
}
let detachContextMenu = this.openmct.contextMenu.attachTo(this.$el, this.objectPath);
this.$once('hook:destroyed', detachContextMenu);
},
computed: {
typeClass() {

View File

@ -11,7 +11,7 @@
{{ domainObject.name }}
</span>
</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 class="l-browse-bar__end">
@ -81,6 +81,11 @@
this.openmct.notifications.error('Error saving objects');
console.error(error);
});
},
showContextMenu(event) {
event.preventDefault();
event.stopPropagation();
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
}
},
data: function () {

View File

@ -239,7 +239,6 @@
import MctTree from './mct-tree.vue';
import ObjectView from './ObjectView.vue';
import MctTemplate from '../legacy/mct-template.vue';
import ContextMenu from '../controls/ContextMenu.vue';
import CreateButton from '../controls/CreateButton.vue';
import search from '../controls/search.vue';
import multipane from '../controls/multipane.vue';
@ -283,7 +282,6 @@
MctTree,
ObjectView,
'mct-template': MctTemplate,
ContextMenu,
CreateButton,
search,
multipane,

View File

@ -52,6 +52,7 @@
this.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject;
this.node.objectPath.splice(0, 1, newObject);
});
this.$once('hook:destroyed', removeListener);
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);
}
}
};