From 32a0baa7a3cb1cc22ac0c4c11305e21618be8002 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 4 Dec 2018 09:09:09 -0800 Subject: [PATCH] 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 --- src/MCT.js | 12 +-- src/adapter/actions/LegacyActionAdapter.js | 67 +++++++-------- .../actions/LegacyContextMenuAction.js | 57 +++++++++++++ src/api/api.js | 6 +- .../contextMenu}/ContextMenu.vue | 0 ...ntextMenuRegistry.js => ContextMenuAPI.js} | 83 +++++++++++-------- src/styles-new/sass-base.scss | 4 +- src/ui/components/controls/ObjectLabel.vue | 5 +- src/ui/components/layout/BrowseBar.vue | 7 +- src/ui/components/layout/Layout.vue | 2 - src/ui/components/layout/tree-item.vue | 1 + .../components/mixins/context-menu-gesture.js | 24 ++++++ 12 files changed, 180 insertions(+), 88 deletions(-) create mode 100644 src/adapter/actions/LegacyContextMenuAction.js rename src/{ui/components/controls => api/contextMenu}/ContextMenu.vue (100%) rename src/api/contextMenu/{ContextMenuRegistry.js => ContextMenuAPI.js} (58%) create mode 100644 src/ui/components/mixins/context-menu-gesture.js diff --git a/src/MCT.js b/src/MCT.js index e12e6fcd63..85180a440f 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -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); - } }; /** diff --git a/src/adapter/actions/LegacyActionAdapter.js b/src/adapter/actions/LegacyActionAdapter.js index e3b5d69696..b91516d9b8 100644 --- a/src/adapter/actions/LegacyActionAdapter.js +++ b/src/adapter/actions/LegacyActionAdapter.js @@ -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); } diff --git a/src/adapter/actions/LegacyContextMenuAction.js b/src/adapter/actions/LegacyContextMenuAction.js new file mode 100644 index 0000000000..d72897786f --- /dev/null +++ b/src/adapter/actions/LegacyContextMenuAction.js @@ -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(); + } +} \ No newline at end of file diff --git a/src/api/api.js b/src/api/api.js index 81717f587a..c6c09d4474 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -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 }; }); diff --git a/src/ui/components/controls/ContextMenu.vue b/src/api/contextMenu/ContextMenu.vue similarity index 100% rename from src/ui/components/controls/ContextMenu.vue rename to src/api/contextMenu/ContextMenu.vue diff --git a/src/api/contextMenu/ContextMenuRegistry.js b/src/api/contextMenu/ContextMenuAPI.js similarity index 58% rename from src/api/contextMenu/ContextMenuRegistry.js rename to src/api/contextMenu/ContextMenuAPI.js index 4bc27a87c0..5a4104269d 100644 --- a/src/api/contextMenu/ContextMenuRegistry.js +++ b/src/api/contextMenu/ContextMenuAPI.js @@ -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; diff --git a/src/styles-new/sass-base.scss b/src/styles-new/sass-base.scss index f59b6dfc2f..f4cf83282e 100644 --- a/src/styles-new/sass-base.scss +++ b/src/styles-new/sass-base.scss @@ -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"; \ No newline at end of file diff --git a/src/ui/components/controls/ObjectLabel.vue b/src/ui/components/controls/ObjectLabel.vue index 1f793fe165..9199138ac2 100644 --- a/src/ui/components/controls/ObjectLabel.vue +++ b/src/ui/components/controls/ObjectLabel.vue @@ -12,9 +12,10 @@