Generate type declarations for CompositionAPI and publish with OpenMCT (#5838)

* add typescript

* update tsconfig

* convert to es6 class

* Convert more stuff to es6 class

* skip checking libs, test files

* more es6 classes!

* Fix some jsdocs

* Rename file

* Improve jsdoc types

* Rename references as well

* more types

* types for CompositionAPI

* Types for CompositionCollection

* Types for CompositionProvider

* type

* types for api

* nvm

* cleanup MCT

* Fix API type definition

* Generate types before publish

* fix openmct 👀

* rename PublicAPI -> OpenMCT and document methods

* try and fix visual test ?

* Make private methods private

* more private methods!!

* import all es6 api's so we get more types for free

* convert Selection to es6 class

* remove redundant docs

* fix Branding types

* fix openmct.start() types

* Remove useless `@memberof`

* Add parameter name

* [docs] Add a section on Types

* markdownlint

* word

* Add section on limitations / contibuting types

* Let these methods be private

* make private members private, fix a type

* fix another type

* Make method private

* Update docs for `skipMutate` and related methods

* Rename file and fix references

* `DefaultCompositionProvider` extends `CompositionProvider`

* Make private members private

* Type for `AbortSignal`

* `domainObject` must be accessible for perf tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Jesse Mazzella 2022-10-21 17:29:52 -07:00 committed by GitHub
parent b4554d2fc1
commit 41fc502564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1465 additions and 1184 deletions

594
API.md

File diff suppressed because it is too large Load Diff

View File

@ -30,8 +30,53 @@ if (document.currentScript) {
} }
} }
/**
* @typedef {object} BuildInfo
* @property {string} version
* @property {string} buildDate
* @property {string} revision
* @property {string} branch
*/
/**
* @typedef {object} OpenMCT
* @property {BuildInfo} buildInfo
* @property {*} selection
* @property {import('./src/api/time/TimeAPI').default} time
* @property {import('./src/api/composition/CompositionAPI').default} composition
* @property {*} objectViews
* @property {*} inspectorViews
* @property {*} propertyEditors
* @property {*} toolbars
* @property {*} types
* @property {import('./src/api/objects/ObjectAPI').default} objects
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
* @property {import('./src/api/user/UserAPI').default} user
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
* @property {import('./src/api/Editor').default} editor
* @property {import('./src/api/overlays/OverlayAPI')} overlays
* @property {import('./src/api/menu/MenuAPI').default} menus
* @property {import('./src/api/actions/ActionsAPI').default} actions
* @property {import('./src/api/status/StatusAPI').default} status
* @property {*} priority
* @property {import('./src/ui/router/ApplicationRouter')} router
* @property {import('./src/api/faultmanagement/FaultManagementAPI').default} faults
* @property {import('./src/api/forms/FormsAPI').default} forms
* @property {import('./src/api/Branding').default} branding
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
* @property {{(plugin: OpenMCTPlugin) => void}} install
* @property {{() => string}} getAssetPath
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
* @property {{() => void}} startHeadless
* @property {{() => void}} destroy
* @property {OpenMCTPlugin[]} plugins
* @property {OpenMCTComponent[]} components
*/
const MCT = require('./src/MCT'); const MCT = require('./src/MCT');
/** @type {OpenMCT} */
const openmct = new MCT(); const openmct = new MCT();
module.exports = openmct; module.exports = openmct;

View File

@ -57,6 +57,7 @@
"sass-loader": "13.0.2", "sass-loader": "13.0.2",
"sinon": "14.0.0", "sinon": "14.0.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"typescript": "4.8.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"vue": "2.6.14", "vue": "2.6.14",
"vue-eslint-parser": "9.1.0", "vue-eslint-parser": "9.1.0",
@ -96,7 +97,7 @@
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", "cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable", "cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit", "cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod" "prepare": "npm run build:prod && npx tsc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/* eslint-disable no-undef */
define([ define([
'EventEmitter', 'EventEmitter',
'./api/api', './api/api',
@ -81,13 +81,11 @@ define([
/** /**
* The Open MCT application. This may be configured by installing plugins * The Open MCT application. This may be configured by installing plugins
* or registering extensions before the application is started. * or registering extensions before the application is started.
* @class MCT * @constructor
* @memberof module:openmct * @memberof module:openmct
* @augments {EventEmitter}
*/ */
function MCT() { function MCT() {
EventEmitter.call(this); EventEmitter.call(this);
/* eslint-disable no-undef */
this.buildInfo = { this.buildInfo = {
version: __OPENMCT_VERSION__, version: __OPENMCT_VERSION__,
buildDate: __OPENMCT_BUILD_DATE__, buildDate: __OPENMCT_BUILD_DATE__,
@ -101,7 +99,7 @@ define([
* Tracks current selection state of the application. * Tracks current selection state of the application.
* @private * @private
*/ */
['selection', () => new Selection(this)], ['selection', () => new Selection.default(this)],
/** /**
* MCT's time conductor, which may be used to synchronize view contents * MCT's time conductor, which may be used to synchronize view contents
@ -125,7 +123,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name composition * @name composition
*/ */
['composition', () => new api.CompositionAPI(this)], ['composition', () => new api.CompositionAPI.default(this)],
/** /**
* Registry for views of domain objects which should appear in the * Registry for views of domain objects which should appear in the

View File

@ -23,8 +23,7 @@
let brandingOptions = {}; let brandingOptions = {};
/** /**
* @typedef {Object} BrandingOptions * @typedef {object} BrandingOptions
* @memberOf openmct/branding
* @property {string} smallLogoImage URL to the image to use as the applications logo. * @property {string} smallLogoImage URL to the image to use as the applications logo.
* This logo will appear on every screen and when clicked will launch the about dialog. * This logo will appear on every screen and when clicked will launch the about dialog.
* @property {string} aboutHtml Custom content for the about screen. When defined the * @property {string} aboutHtml Custom content for the about screen. When defined the

View File

@ -37,7 +37,9 @@ define([
'./types/TypeRegistry', './types/TypeRegistry',
'./user/UserAPI', './user/UserAPI',
'./annotation/AnnotationAPI' './annotation/AnnotationAPI'
], function ( ],
function (
ActionsAPI, ActionsAPI,
CompositionAPI, CompositionAPI,
EditorAPI, EditorAPI,

View File

@ -20,34 +20,41 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import DefaultCompositionProvider from './DefaultCompositionProvider';
'lodash', import CompositionCollection from './CompositionCollection';
'EventEmitter',
'./DefaultCompositionProvider', /**
'./CompositionCollection' * @typedef {import('./CompositionProvider').default} CompositionProvider
], function ( */
_,
EventEmitter, /**
DefaultCompositionProvider, * @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
CompositionCollection */
) {
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain objects
* it "contains" (for instance, that should be displayed beneath it
* in the tree.)
* @constructor
*/
export default class CompositionAPI {
/** /**
* An interface for interacting with the composition of domain objects. * @param {OpenMCT} publicAPI
* The composition of a domain object is the list of other domain objects
* it "contains" (for instance, that should be displayed beneath it
* in the tree.)
*
* @interface CompositionAPI
* @returns {module:openmct.CompositionCollection}
* @memberof module:openmct
*/ */
function CompositionAPI(publicAPI) { constructor(publicAPI) {
/** @type {CompositionProvider[]} */
this.registry = []; this.registry = [];
/** @type {CompositionPolicy[]} */
this.policies = []; this.policies = [];
this.addProvider(new DefaultCompositionProvider(publicAPI, this)); this.addProvider(new DefaultCompositionProvider(publicAPI, this));
/** @type {OpenMCT} */
this.publicAPI = publicAPI; this.publicAPI = publicAPI;
} }
/** /**
* Add a composition provider. * Add a composition provider.
* *
@ -55,21 +62,19 @@ define([
* behavior for certain domain objects. * behavior for certain domain objects.
* *
* @method addProvider * @method addProvider
* @param {module:openmct.CompositionProvider} provider the provider to add * @param {CompositionProvider} provider the provider to add
* @memberof module:openmct.CompositionAPI#
*/ */
CompositionAPI.prototype.addProvider = function (provider) { addProvider(provider) {
this.registry.unshift(provider); this.registry.unshift(provider);
}; }
/** /**
* Retrieve the composition (if any) of this domain object. * Retrieve the composition (if any) of this domain object.
* *
* @method get * @method get
* @returns {module:openmct.CompositionCollection} * @param {DomainObject} domainObject
* @memberof module:openmct.CompositionAPI# * @returns {CompositionCollection}
*/ */
CompositionAPI.prototype.get = function (domainObject) { get(domainObject) {
const provider = this.registry.find(p => { const provider = this.registry.find(p => {
return p.appliesTo(domainObject); return p.appliesTo(domainObject);
}); });
@ -79,8 +84,7 @@ define([
} }
return new CompositionCollection(domainObject, provider, this.publicAPI); return new CompositionCollection(domainObject, provider, this.publicAPI);
}; }
/** /**
* A composition policy is a function which either allows or disallows * A composition policy is a function which either allows or disallows
* placing one object in another's composition. * placing one object in another's composition.
@ -90,52 +94,51 @@ define([
* generally be written to return true in the default case. * generally be written to return true in the default case.
* *
* @callback CompositionPolicy * @callback CompositionPolicy
* @memberof module:openmct.CompositionAPI~ * @param {DomainObject} containingObject the object which
* @param {module:openmct.DomainObject} containingObject the object which
* would act as a container * would act as a container
* @param {module:openmct.DomainObject} containedObject the object which * @param {DomainObject} containedObject the object which
* would be contained * would be contained
* @returns {boolean} false if this composition should be disallowed * @returns {boolean} false if this composition should be disallowed
*/ */
/** /**
* Add a composition policy. Composition policies may disallow domain * Add a composition policy. Composition policies may disallow domain
* objects from containing other domain objects. * objects from containing other domain objects.
* *
* @method addPolicy * @method addPolicy
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy * @param {CompositionPolicy} policy
* the policy to add * the policy to add
* @memberof module:openmct.CompositionAPI#
*/ */
CompositionAPI.prototype.addPolicy = function (policy) { addPolicy(policy) {
this.policies.push(policy); this.policies.push(policy);
}; }
/** /**
* Check whether or not a domain object is allowed to contain another * Check whether or not a domain object is allowed to contain another
* domain object. * domain object.
* *
* @private * @private
* @method checkPolicy * @method checkPolicy
* @param {module:openmct.DomainObject} containingObject the object which * @param {DomainObject} container the object which
* would act as a container * would act as a container
* @param {module:openmct.DomainObject} containedObject the object which * @param {DomainObject} containee the object which
* would be contained * would be contained
* @returns {boolean} false if this composition should be disallowed * @returns {boolean} false if this composition should be disallowed
* @param {CompositionPolicy} policy
* @param {module:openmct.CompositionAPI~CompositionPolicy} policy
* the policy to add * the policy to add
* @memberof module:openmct.CompositionAPI#
*/ */
CompositionAPI.prototype.checkPolicy = function (container, containee) { checkPolicy(container, containee) {
return this.policies.every(function (policy) { return this.policies.every(function (policy) {
return policy(container, containee); return policy(container, containee);
}); });
}; }
CompositionAPI.prototype.supportsComposition = function (domainObject) { /**
* Check whether or not a domainObject supports composition
*
* @param {DomainObject} domainObject
* @returns {boolean} true if the domainObject supports composition
*/
supportsComposition(domainObject) {
return this.get(domainObject) !== undefined; return this.get(domainObject) !== undefined;
}; }
}
return CompositionAPI;
});

View File

@ -1,325 +1,319 @@
define([ import CompositionAPI from './CompositionAPI';
'./CompositionAPI', import CompositionCollection from './CompositionCollection';
'./CompositionCollection'
], function (
CompositionAPI,
CompositionCollection
) {
describe('The Composition API', function () { describe('The Composition API', function () {
let publicAPI; let publicAPI;
let compositionAPI; let compositionAPI;
let topicService; let topicService;
let mutationTopic; let mutationTopic;
beforeEach(function () {
mutationTopic = jasmine.createSpyObj('mutationTopic', [
'listen'
]);
topicService = jasmine.createSpy('topicService');
topicService.and.returnValue(mutationTopic);
publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get',
'mutate',
'observe',
'areIdsEqual'
]);
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) {
return id1.namespace === id2.namespace && id1.key === id2.key;
});
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [
'checkPolicy'
]);
publicAPI.composition.checkPolicy.and.returnValue(true);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on'
]);
publicAPI.objects.get.and.callFake(function (identifier) {
return Promise.resolve({identifier: identifier});
});
publicAPI.$injector = jasmine.createSpyObj('$injector', [
'get'
]);
publicAPI.$injector.get.and.returnValue(topicService);
compositionAPI = new CompositionAPI(publicAPI);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
});
describe('default composition', function () {
let domainObject;
let composition;
beforeEach(function () { beforeEach(function () {
domainObject = {
mutationTopic = jasmine.createSpyObj('mutationTopic', [ name: 'test folder',
'listen' identifier: {
]); namespace: 'test',
topicService = jasmine.createSpy('topicService'); key: '1'
topicService.and.returnValue(mutationTopic); },
publicAPI = {}; composition: [
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [ {
'get', namespace: 'test',
'mutate', key: 'a'
'observe', },
'areIdsEqual' {
]); namespace: 'test',
key: 'b'
publicAPI.objects.areIdsEqual.and.callFake(function (id1, id2) { },
return id1.namespace === id2.namespace && id1.key === id2.key; {
}); namespace: 'test',
key: 'c'
publicAPI.composition = jasmine.createSpyObj('CompositionAPI', [ }
'checkPolicy' ]
]); };
publicAPI.composition.checkPolicy.and.returnValue(true); composition = compositionAPI.get(domainObject);
publicAPI.objects.eventEmitter = jasmine.createSpyObj('eventemitter', [
'on'
]);
publicAPI.objects.get.and.callFake(function (identifier) {
return Promise.resolve({identifier: identifier});
});
publicAPI.$injector = jasmine.createSpyObj('$injector', [
'get'
]);
publicAPI.$injector.get.and.returnValue(topicService);
compositionAPI = new CompositionAPI(publicAPI);
}); });
it('returns falsy if an object does not support composition', function () { it('returns composition collection', function () {
expect(compositionAPI.get({})).toBeFalsy(); expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
}); });
describe('default composition', function () { it('correctly reflects composability', function () {
let domainObject; expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
let composition; delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
beforeEach(function () { it('loads composition from domain object', function () {
domainObject = { const listener = jasmine.createSpy('addListener');
name: 'test folder', composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: { identifier: {
namespace: 'test', namespace: 'test',
key: '1' key: 'a'
}, }
composition: [
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {
namespace: 'test',
key: 'a'
}
});
}); });
}); });
describe('supports reordering of composition', function () { });
let listener; describe('supports reordering of composition', function () {
beforeEach(function () { let listener;
listener = jasmine.createSpy('reorderListener'); beforeEach(function () {
composition.on('reorder', listener); listener = jasmine.createSpy('reorderListener');
composition.on('reorder', listener);
return composition.load(); return composition.load();
}); });
it('', function () { it('', function () {
composition.reorder(1, 0); composition.reorder(1, 0);
let newComposition = let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2]; publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0]; let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(1); expect(reorderPlan.oldIndex).toBe(1);
expect(reorderPlan.newIndex).toBe(0); expect(reorderPlan.newIndex).toBe(0);
expect(newComposition[0].key).toEqual('b'); expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a'); expect(newComposition[1].key).toEqual('a');
expect(newComposition[2].key).toEqual('c'); expect(newComposition[2].key).toEqual('c');
}); });
it('', function () { it('', function () {
composition.reorder(0, 2); composition.reorder(0, 2);
let newComposition = let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2]; publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0]; let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(0); expect(reorderPlan.oldIndex).toBe(0);
expect(reorderPlan.newIndex).toBe(2); expect(reorderPlan.newIndex).toBe(2);
expect(newComposition[0].key).toEqual('b'); expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('c'); expect(newComposition[1].key).toEqual('c');
expect(newComposition[2].key).toEqual('a'); expect(newComposition[2].key).toEqual('a');
});
});
it('supports adding an object to composition', function () {
let addListener = jasmine.createSpy('addListener');
let mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.on('add', addListener);
composition.add(mockChildObject);
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
composition.on('add', addListener);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
expect(addListener).toHaveBeenCalled();
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
}); });
}); });
it('supports adding an object to composition', function () { });
let addListener = jasmine.createSpy('addListener'); describe('Calling add or remove', function () {
let mockChildObject = { let mockChildObject;
beforeEach(function () {
mockChildObject = {
identifier: { identifier: {
key: 'mock-key', key: 'mock-key',
namespace: '' namespace: ''
} }
}; };
composition.on('add', addListener);
composition.add(mockChildObject); composition.add(mockChildObject);
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
}); });
it('supports listening and loading', function () { it('calls add on the provider', function () {
const addListener = jasmine.createSpy('addListener'); expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
composition.on('add', addListener);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
expect(addListener).toHaveBeenCalled();
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
describe('Calling add or remove', function () {
let mockChildObject;
beforeEach(function () {
mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.add(mockChildObject);
});
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
});
});
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
}); });
it('supports listening and loading', function () { it('calls remove on the provider', function () {
const addListener = jasmine.createSpy('addListener'); composition.remove(mockChildObject);
const removeListener = jasmine.createSpy('removeListener'); expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
return composition.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
return addPromise;
}).then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
return removePromise;
}).then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
}); });
}); });
}); });
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
const removeListener = jasmine.createSpy('removeListener');
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
return composition.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
return addPromise;
}).then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
return removePromise;
}).then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
});
}); });

View File

@ -20,75 +20,98 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ /**
'lodash' * @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
], function ( */
_
) { /**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* @typedef {object} ListenerMap
* @property {Array.<any>} add
* @property {Array.<any>} remove
* @property {Array.<any>} load
* @property {Array.<any>} reorder
*/
/**
* A CompositionCollection represents the list of domain objects contained
* by another domain object. It provides methods for loading this
* list asynchronously, modifying this list, and listening for changes to
* this list.
*
* Usage:
* ```javascript
* var myViewComposition = MCT.composition.get(myViewObject);
* myViewComposition.on('add', addObjectToView);
* myViewComposition.on('remove', removeObjectFromView);
* myViewComposition.load(); // will trigger `add` for all loaded objects.
* ```
*/
export default class CompositionCollection {
domainObject;
#provider;
#publicAPI;
#listeners;
#mutables;
/** /**
* A CompositionCollection represents the list of domain objects contained * @constructor
* by another domain object. It provides methods for loading this * @param {DomainObject} domainObject the domain object
* list asynchronously, modifying this list, and listening for changes to
* this list.
*
* Usage:
* ```javascript
* var myViewComposition = MCT.composition.get(myViewObject);
* myViewComposition.on('add', addObjectToView);
* myViewComposition.on('remove', removeObjectFromView);
* myViewComposition.load(); // will trigger `add` for all loaded objects.
* ```
*
* @interface CompositionCollection
* @param {module:openmct.DomainObject} domainObject the domain object
* whose composition will be contained * whose composition will be contained
* @param {module:openmct.CompositionProvider} provider the provider * @param {import('./CompositionProvider').default} provider the provider
* to use to retrieve other domain objects * to use to retrieve other domain objects
* @param {module:openmct.CompositionAPI} api the composition API, for * @param {OpenMCT} publicAPI the composition API, for
* policy checks * policy checks
* @memberof module:openmct
*/ */
function CompositionCollection(domainObject, provider, publicAPI) { constructor(domainObject, provider, publicAPI) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.provider = provider; /** @type {import('./CompositionProvider').default} */
this.publicAPI = publicAPI; this.#provider = provider;
this.listeners = { /** @type {OpenMCT} */
this.#publicAPI = publicAPI;
/** @type {ListenerMap} */
this.#listeners = {
add: [], add: [],
remove: [], remove: [],
load: [], load: [],
reorder: [] reorder: []
}; };
this.onProviderAdd = this.onProviderAdd.bind(this); this.onProviderAdd = this.#onProviderAdd.bind(this);
this.onProviderRemove = this.onProviderRemove.bind(this); this.onProviderRemove = this.#onProviderRemove.bind(this);
this.mutables = {}; this.#mutables = {};
if (this.domainObject.isMutable) { if (this.domainObject.isMutable) {
this.returnMutables = true; this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => { let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.mutables).forEach(mutable => { Object.values(this.#mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable); this.#publicAPI.objects.destroyMutable(mutable);
}); });
unobserve(); unobserve();
}); });
} }
} }
/** /**
* Listen for changes to this composition. Supports 'add', 'remove', and * Listen for changes to this composition. Supports 'add', 'remove', and
* 'load' events. * 'load' events.
* *
* @param event event to listen for, either 'add', 'remove' or 'load'. * @param {string} event event to listen for, either 'add', 'remove' or 'load'.
* @param callback to trigger when event occurs. * @param {(...args: any[]) => void} callback to trigger when event occurs.
* @param [context] context to use when invoking callback, optional. * @param {any} [context] to use when invoking callback, optional.
*/ */
CompositionCollection.prototype.on = function (event, callback, context) { on(event, callback, context) {
if (!this.listeners[event]) { if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event); throw new Error('Event not supported by composition: ' + event);
} }
if (this.provider.on && this.provider.off) { if (this.#provider.on && this.#provider.off) {
if (event === 'add') { if (event === 'add') {
this.provider.on( this.#provider.on(
this.domainObject, this.domainObject,
'add', 'add',
this.onProviderAdd, this.onProviderAdd,
@ -97,7 +120,7 @@ define([
} }
if (event === 'remove') { if (event === 'remove') {
this.provider.on( this.#provider.on(
this.domainObject, this.domainObject,
'remove', 'remove',
this.onProviderRemove, this.onProviderRemove,
@ -106,36 +129,34 @@ define([
} }
if (event === 'reorder') { if (event === 'reorder') {
this.provider.on( this.#provider.on(
this.domainObject, this.domainObject,
'reorder', 'reorder',
this.onProviderReorder, this.#onProviderReorder,
this this
); );
} }
} }
this.listeners[event].push({ this.#listeners[event].push({
callback: callback, callback: callback,
context: context context: context
}); });
}; }
/** /**
* Remove a listener. Must be called with same exact parameters as * Remove a listener. Must be called with same exact parameters as
* `off`. * `off`.
* *
* @param event * @param {string} event
* @param callback * @param {(...args: any[]) => void} callback
* @param [context] * @param {any} [context]
*/ */
off(event, callback, context) {
CompositionCollection.prototype.off = function (event, callback, context) { if (!this.#listeners[event]) {
if (!this.listeners[event]) {
throw new Error('Event not supported by composition: ' + event); throw new Error('Event not supported by composition: ' + event);
} }
const index = this.listeners[event].findIndex(l => { const index = this.#listeners[event].findIndex(l => {
return l.callback === callback && l.context === context; return l.callback === callback && l.context === context;
}); });
@ -143,125 +164,116 @@ define([
throw new Error('Tried to remove a listener that does not exist'); throw new Error('Tried to remove a listener that does not exist');
} }
this.listeners[event].splice(index, 1); this.#listeners[event].splice(index, 1);
if (this.listeners[event].length === 0) { if (this.#listeners[event].length === 0) {
this._destroy(); this._destroy();
// Remove provider listener if this is the last callback to // Remove provider listener if this is the last callback to
// be removed. // be removed.
if (this.provider.off && this.provider.on) { if (this.#provider.off && this.#provider.on) {
if (event === 'add') { if (event === 'add') {
this.provider.off( this.#provider.off(
this.domainObject, this.domainObject,
'add', 'add',
this.onProviderAdd, this.onProviderAdd,
this this
); );
} else if (event === 'remove') { } else if (event === 'remove') {
this.provider.off( this.#provider.off(
this.domainObject, this.domainObject,
'remove', 'remove',
this.onProviderRemove, this.onProviderRemove,
this this
); );
} else if (event === 'reorder') { } else if (event === 'reorder') {
this.provider.off( this.#provider.off(
this.domainObject, this.domainObject,
'reorder', 'reorder',
this.onProviderReorder, this.#onProviderReorder,
this this
); );
} }
} }
} }
}; }
/** /**
* Add a domain object to this composition. * Add a domain object to this composition.
* *
* A call to [load]{@link module:openmct.CompositionCollection#load} * A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method. * must have resolved before using this method.
* *
* @param {module:openmct.DomainObject} child the domain object to add * **TODO:** Remove `skipMutate` parameter.
* @param {boolean} skipMutate true if the underlying provider should *
* not be updated * @param {DomainObject} child the domain object to add
* @memberof module:openmct.CompositionCollection# * @param {boolean} skipMutate
* @name add * **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
*/ */
CompositionCollection.prototype.add = function (child, skipMutate) { add(child, skipMutate) {
if (!skipMutate) { if (!skipMutate) {
if (!this.publicAPI.composition.checkPolicy(this.domainObject, child)) { if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`; throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
} }
this.provider.add(this.domainObject, child.identifier); this.#provider.add(this.domainObject, child.identifier);
} else { } else {
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) { if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.publicAPI.objects.makeKeyString(child.identifier); let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.publicAPI.objects.toMutable(child); child = this.#publicAPI.objects.toMutable(child);
this.mutables[keyString] = child; this.#mutables[keyString] = child;
} }
this.emit('add', child); this.#emit('add', child);
} }
}; }
/** /**
* Load the domain objects in this composition. * Load the domain objects in this composition.
* *
* @returns {Promise.<Array.<module:openmct.DomainObject>>} a promise for * @param {AbortSignal} abortSignal
* @returns {Promise.<Array.<DomainObject>>} a promise for
* the domain objects in this composition * the domain objects in this composition
* @memberof {module:openmct.CompositionCollection#} * @memberof {module:openmct.CompositionCollection#}
* @name load * @name load
*/ */
CompositionCollection.prototype.load = function (abortSignal) { async load(abortSignal) {
this.cleanUpMutables(); this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
return this.provider.load(this.domainObject) const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
.then(function (children) { childObjects.forEach(c => this.add(c, true));
return Promise.all(children.map((c) => this.publicAPI.objects.get(c, abortSignal))); this.#emit('load');
}.bind(this))
.then(function (childObjects) {
childObjects.forEach(c => this.add(c, true));
return childObjects;
}.bind(this))
.then(function (children) {
this.emit('load');
return children;
}.bind(this));
};
return childObjects;
}
/** /**
* Remove a domain object from this composition. * Remove a domain object from this composition.
* *
* A call to [load]{@link module:openmct.CompositionCollection#load} * A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method. * must have resolved before using this method.
* *
* @param {module:openmct.DomainObject} child the domain object to remove * **TODO:** Remove `skipMutate` parameter.
* @param {boolean} skipMutate true if the underlying provider should *
* not be updated * @param {DomainObject} child the domain object to remove
* @memberof module:openmct.CompositionCollection# * @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
* @name remove * @name remove
*/ */
CompositionCollection.prototype.remove = function (child, skipMutate) { remove(child, skipMutate) {
if (!skipMutate) { if (!skipMutate) {
this.provider.remove(this.domainObject, child.identifier); this.#provider.remove(this.domainObject, child.identifier);
} else { } else {
if (this.returnMutables) { if (this.returnMutables) {
let keyString = this.publicAPI.objects.makeKeyString(child); let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) { if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.publicAPI.objects.destroyMutable(this.mutables[keyString]); this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.mutables[keyString]; delete this.#mutables[keyString];
} }
} }
this.emit('remove', child); this.#emit('remove', child);
} }
}; }
/** /**
* Reorder the domain objects in this composition. * Reorder the domain objects in this composition.
* *
@ -270,67 +282,75 @@ define([
* *
* @param {number} oldIndex * @param {number} oldIndex
* @param {number} newIndex * @param {number} newIndex
* @memberof module:openmct.CompositionCollection#
* @name remove * @name remove
*/ */
CompositionCollection.prototype.reorder = function (oldIndex, newIndex, skipMutate) { reorder(oldIndex, newIndex, _skipMutate) {
this.provider.reorder(this.domainObject, oldIndex, newIndex); this.#provider.reorder(this.domainObject, oldIndex, newIndex);
}; }
/** /**
* Handle reorder from provider. * Destroy mutationListener
* @private
*/ */
CompositionCollection.prototype.onProviderReorder = function (reorderMap) { _destroy() {
this.emit('reorder', reorderMap);
};
/**
* Handle adds from provider.
* @private
*/
CompositionCollection.prototype.onProviderAdd = function (childId) {
return this.publicAPI.objects.get(childId).then(function (child) {
this.add(child, true);
return child;
}.bind(this));
};
/**
* Handle removal from provider.
* @private
*/
CompositionCollection.prototype.onProviderRemove = function (child) {
this.remove(child, true);
};
CompositionCollection.prototype._destroy = function () {
if (this.mutationListener) { if (this.mutationListener) {
this.mutationListener(); this.mutationListener();
delete this.mutationListener; delete this.mutationListener;
} }
}; }
/**
* Handle reorder from provider.
* @private
* @param {object} reorderMap
*/
#onProviderReorder(reorderMap) {
this.#emit('reorder', reorderMap);
}
/**
* Handle adds from provider.
* @private
* @param {import('../objects/ObjectAPI').Identifier} childId
* @returns {DomainObject}
*/
#onProviderAdd(childId) {
return this.#publicAPI.objects.get(childId).then(function (child) {
this.add(child, true);
return child;
}.bind(this));
}
/**
* Handle removal from provider.
* @param {DomainObject} child
*/
#onProviderRemove(child) {
this.remove(child, true);
}
/** /**
* Emit events. * Emit events.
*
* @private * @private
* @param {string} event
* @param {...args.<any>} payload
*/ */
CompositionCollection.prototype.emit = function (event, ...payload) { #emit(event, ...payload) {
this.listeners[event].forEach(function (l) { this.#listeners[event].forEach(function (l) {
if (l.context) { if (l.context) {
l.callback.apply(l.context, payload); l.callback.apply(l.context, payload);
} else { } else {
l.callback(...payload); l.callback(...payload);
} }
}); });
}; }
CompositionCollection.prototype.cleanUpMutables = function () { /**
Object.values(this.mutables).forEach(mutable => { * Destroy all mutables.
this.publicAPI.objects.destroyMutable(mutable); * @private
*/
#cleanUpMutables() {
Object.values(this.#mutables).forEach(mutable => {
this.#publicAPI.objects.destroyMutable(mutable);
}); });
}; }
}
return CompositionCollection;
});

View File

@ -0,0 +1,262 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 _ from 'lodash';
import objectUtils from "../objects/object-utils";
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
/**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
*/
export default class CompositionProvider {
#publicAPI;
#listeningTo;
/**
* @param {OpenMCT} publicAPI
* @param {CompositionAPI} compositionAPI
*/
constructor(publicAPI, compositionAPI) {
this.#publicAPI = publicAPI;
this.#listeningTo = {};
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
}
get listeningTo() {
return this.#listeningTo;
}
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
get publicAPI() {
return this.#publicAPI;
}
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @method appliesTo
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @method load
*/
load(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
}
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}
/**
* @private
* @param {DomainObject} parent
* @param {DomainObject} child
* @returns {boolean}
*/
#cannotContainItself(parent, child) {
return !(parent.identifier.namespace === child.identifier.namespace
&& parent.identifier.key === child.identifier.key);
}
/**
* @private
* @param {DomainObject} parent
* @returns {boolean}
*/
#supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent);
}
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
}

View File

@ -19,102 +19,79 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import objectUtils from "../objects/object-utils";
import CompositionProvider from './CompositionProvider';
define([ /**
'lodash', * @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
'objectUtils' */
], function (
_,
objectUtils
) {
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
* @interface CompositionProvider
* @memberof module:openmct
*/
function DefaultCompositionProvider(publicAPI, compositionAPI) { /**
this.publicAPI = publicAPI; * @typedef {import('../objects/ObjectAPI').Identifier} Identifier
this.listeningTo = {}; */
this.onMutation = this.onMutation.bind(this);
this.cannotContainItself = this.cannotContainItself.bind(this); /**
this.supportsComposition = this.supportsComposition.bind(this); * @typedef {import('./CompositionAPI').default} CompositionAPI
*/
compositionAPI.addPolicy(this.cannotContainItself); /**
compositionAPI.addPolicy(this.supportsComposition); * @typedef {import('../../../openmct').OpenMCT} OpenMCT
} */
/**
* @private
*/
DefaultCompositionProvider.prototype.cannotContainItself = function (parent, child) {
return !(parent.identifier.namespace === child.identifier.namespace
&& parent.identifier.key === child.identifier.key);
};
/**
* @private
*/
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
return this.publicAPI.composition.supportsComposition(parent);
};
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
* @extends CompositionProvider
*/
export default class DefaultCompositionProvider extends CompositionProvider {
/** /**
* Check if this provider should be used to load composition for a * Check if this provider should be used to load composition for a
* particular domain object. * particular domain object.
* @param {module:openmct.DomainObject} domainObject the domain object * @override
* @param {DomainObject} domainObject the domain object
* to check * to check
* @returns {boolean} true if this provider can provide * @returns {boolean} true if this provider can provide composition for a given domain object
* composition for a given domain object
* @memberof module:openmct.CompositionProvider#
* @method appliesTo
*/ */
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) { appliesTo(domainObject) {
return Boolean(domainObject.composition); return Boolean(domainObject.composition);
}; }
/** /**
* Load any domain objects contained in the composition of this domain * Load any domain objects contained in the composition of this domain
* object. * object.
* @param {module:openmct.DomainObject} domainObject the domain object * @override
* @param {DomainObject} domainObject the domain object
* for which to load composition * for which to load composition
* @returns {Promise.<Array.<module:openmct.Identifier>>} a promise for * @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition * the Identifiers in this composition
* @memberof module:openmct.CompositionProvider#
* @method load
*/ */
DefaultCompositionProvider.prototype.load = function (domainObject) { load(domainObject) {
return Promise.all(domainObject.composition); return Promise.all(domainObject.composition);
}; }
/** /**
* Attach listeners for changes to the composition of a given domain object. * Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events. * Supports `add` and `remove` events.
* *
* @param {module:openmct.DomainObject} domainObject to listen to * @override
* @param String event the event to bind to, either `add` or `remove`. * @param {DomainObject} domainObject to listen to
* @param Function callback callback to invoke when event is triggered. * @param {string} event the event to bind to, either `add` or `remove`.
* @param [context] context to use when invoking callback. * @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/ */
DefaultCompositionProvider.prototype.on = function ( on(domainObject,
domainObject,
event, event,
callback, callback,
context context) {
) {
this.establishTopicListener(); this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier); const keyString = objectUtils.makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString]; let objectListeners = this.listeningTo[keyString];
@ -131,24 +108,24 @@ define([
callback: callback, callback: callback,
context: context context: context
}); });
}; }
/** /**
* Remove a listener that was previously added for a given domain object. * Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener * event name, callback, and context must be the same as when the listener
* was originally attached. * was originally attached.
* *
* @param {module:openmct.DomainObject} domainObject to remove listener for * @override
* @param String event event to stop listening to: `add` or `remove`. * @param {DomainObject} domainObject to remove listener for
* @param Function callback callback to remove. * @param {string} event event to stop listening to: `add` or `remove`.
* @param [context] context of callback to remove. * @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/ */
DefaultCompositionProvider.prototype.off = function ( off(domainObject,
domainObject,
event, event,
callback, callback,
context context) {
) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier); const keyString = objectUtils.makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString]; const objectListeners = this.listeningTo[keyString];
@ -160,57 +137,64 @@ define([
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) { if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
delete this.listeningTo[keyString]; delete this.listeningTo[keyString];
} }
}; }
/** /**
* Remove a domain object from another domain object's composition. * Remove a domain object from another domain object's composition.
* *
* This method is optional; if not present, adding to a domain object's * This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed. * composition using this provider will be disallowed.
* *
* @param {module:openmct.DomainObject} domainObject the domain object * @override
* @param {DomainObject} domainObject the domain object
* which should have its composition modified * which should have its composition modified
* @param {module:openmct.DomainObject} child the domain object to remove * @param {Identifier} childId the domain object to remove
* @memberof module:openmct.CompositionProvider#
* @method remove * @method remove
*/ */
DefaultCompositionProvider.prototype.remove = function (domainObject, childId) { remove(domainObject, childId) {
let composition = domainObject.composition.filter(function (child) { let composition = domainObject.composition.filter(function (child) {
return !(childId.namespace === child.namespace return !(childId.namespace === child.namespace
&& childId.key === child.key); && childId.key === child.key);
}); });
this.publicAPI.objects.mutate(domainObject, 'composition', composition); this.publicAPI.objects.mutate(domainObject, 'composition', composition);
}; }
/** /**
* Add a domain object to another domain object's composition. * Add a domain object to another domain object's composition.
* *
* This method is optional; if not present, adding to a domain object's * This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed. * composition using this provider will be disallowed.
* *
* @param {module:openmct.DomainObject} domainObject the domain object * @override
* @param {DomainObject} parent the domain object
* which should have its composition modified * which should have its composition modified
* @param {module:openmct.DomainObject} child the domain object to add * @param {Identifier} childId the domain object to add
* @memberof module:openmct.CompositionProvider#
* @method add * @method add
*/ */
DefaultCompositionProvider.prototype.add = function (parent, childId) { add(parent, childId) {
if (!this.includes(parent, childId)) { if (!this.includes(parent, childId)) {
parent.composition.push(childId); parent.composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', parent.composition); this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
} }
}; }
/** /**
* @private * @override
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/ */
DefaultCompositionProvider.prototype.includes = function (parent, childId) { includes(parent, childId) {
return parent.composition.some(composee => return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
this.publicAPI.objects.areIdsEqual(composee, childId)); }
};
DefaultCompositionProvider.prototype.reorder = function (domainObject, oldIndex, newIndex) { /**
* @override
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
let newComposition = domainObject.composition.slice(); let newComposition = domainObject.composition.slice();
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex; let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex; let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
@ -241,6 +225,7 @@ define([
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition); this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier); let id = objectUtils.makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id]; const listeners = this.listeningTo[id];
@ -257,66 +242,5 @@ define([
listener.callback(reorderPlan); listener.callback(reorderPlan);
} }
} }
}; }
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
*
* @private
*/
DefaultCompositionProvider.prototype.establishTopicListener = function () {
if (this.topicListener) {
return;
}
this.publicAPI.objects.eventEmitter.on('mutation', this.onMutation);
this.topicListener = () => {
this.publicAPI.objects.eventEmitter.off('mutation', this.onMutation);
};
};
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
*/
DefaultCompositionProvider.prototype.onMutation = function (oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
};
return DefaultCompositionProvider;
});

View File

@ -33,7 +33,7 @@ import InMemorySearchProvider from './InMemorySearchProvider';
/** /**
* Uniquely identifies a domain object. * Uniquely identifies a domain object.
* *
* @typedef Identifier * @typedef {object} Identifier
* @property {string} namespace the namespace to/from which this domain * @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored. * object should be loaded/stored.
* @property {string} key a unique identifier for the domain object * @property {string} key a unique identifier for the domain object
@ -50,8 +50,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* A few common properties are defined for domain objects. Beyond these, * A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit. * individual types of domain objects may add more as they see fit.
* *
* @typedef DomainObject * @typedef {object} DomainObject
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which * @property {Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object * uniquely identifies this domain object
* @property {string} type the type of domain object * @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object * @property {string} name the human-readable name for this domain object
@ -59,19 +59,19 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* object * object
* @property {number} [modified] the time, in milliseconds since the UNIX * @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified * epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if * @property {Identifier[]} [composition] if
* present, this will be used by the default composition provider * present, this will be used by the default composition provider
* to load domain objects * to load domain objects
* @memberof module:openmct * @memberof module:openmct.ObjectAPI~
*/ */
/** /**
* @readonly * @readonly
* @enum {String} SEARCH_TYPES * @enum {string} SEARCH_TYPES
* @property {String} OBJECTS Search for objects * @property {string} OBJECTS Search for objects
* @property {String} ANNOTATIONS Search for annotations * @property {string} ANNOTATIONS Search for annotations
* @property {String} TAGS Search for tags * @property {string} TAGS Search for tags
*/ */
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.

View File

@ -20,250 +20,225 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define( import EventEmitter from 'EventEmitter';
[ import _ from 'lodash';
'EventEmitter',
'lodash'
],
function (
EventEmitter,
_
) {
/**
* Manages selection state for Open MCT
* @private
*/
function Selection(openmct) {
EventEmitter.call(this);
this.openmct = openmct; /**
this.selected = []; * Manages selection state for Open MCT
* @private
*/
export default class Selection extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.selected = [];
}
/**
* Gets the selected object.
* @public
*/
get() {
return this.selected;
}
/**
* Selects the selectable object and emits the 'change' event.
*
* @param {object} selectable an object with element and context properties
* @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not
* @private
*/
select(selectable, isMultiSelectEvent) {
if (!Array.isArray(selectable)) {
selectable = [selectable];
} }
Selection.prototype = Object.create(EventEmitter.prototype); let multiSelect = isMultiSelectEvent
&& this.parentSupportsMultiSelect(selectable)
&& this.isPeer(selectable)
&& !this.selectionContainsParent(selectable);
/** if (multiSelect) {
* Gets the selected object. this.handleMultiSelect(selectable);
* @public } else {
*/ this.handleSingleSelect(selectable);
Selection.prototype.get = function () { }
return this.selected; }
}; /**
* @private
*/
handleMultiSelect(selectable) {
if (this.elementSelected(selectable)) {
this.remove(selectable);
} else {
this.addSelectionAttributes(selectable);
this.selected.push(selectable);
}
/** this.emit('change', this.selected);
* Selects the selectable object and emits the 'change' event. }
* /**
* @param {object} selectable an object with element and context properties * @private
* @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not */
* @private handleSingleSelect(selectable) {
*/ if (!_.isEqual([selectable], this.selected)) {
Selection.prototype.select = function (selectable, isMultiSelectEvent) { this.setSelectionStyles(selectable);
if (!Array.isArray(selectable)) { this.selected = [selectable];
selectable = [selectable];
}
let multiSelect = isMultiSelectEvent
&& this.parentSupportsMultiSelect(selectable)
&& this.isPeer(selectable)
&& !this.selectionContainsParent(selectable);
if (multiSelect) {
this.handleMultiSelect(selectable);
} else {
this.handleSingleSelect(selectable);
}
};
/**
* @private
*/
Selection.prototype.handleMultiSelect = function (selectable) {
if (this.elementSelected(selectable)) {
this.remove(selectable);
} else {
this.addSelectionAttributes(selectable);
this.selected.push(selectable);
}
this.emit('change', this.selected); this.emit('change', this.selected);
}
}
/**
* @private
*/
elementSelected(selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable));
}
/**
* @private
*/
remove(selectable) {
this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable));
if (this.selected.length === 0) {
this.removeSelectionAttributes(selectable);
selectable[1].element.click(); // Select the parent if there is no selection.
} else {
this.removeSelectionAttributes(selectable, true);
}
}
/**
* @private
*/
setSelectionStyles(selectable) {
this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath));
this.addSelectionAttributes(selectable);
}
removeSelectionAttributes(selectionPath, keepParentStyle) {
if (selectionPath[0] && selectionPath[0].element) {
selectionPath[0].element.removeAttribute('s-selected');
}
if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) {
selectionPath[1].element.removeAttribute('s-selected-parent');
}
}
/**
* Adds selection attributes to the selected element and its parent.
* @private
*/
addSelectionAttributes(selectable) {
if (selectable[0] && selectable[0].element) {
selectable[0].element.setAttribute('s-selected', "");
}
if (selectable[1] && selectable[1].element) {
selectable[1].element.setAttribute('s-selected-parent', "");
}
}
/**
* @private
*/
parentSupportsMultiSelect(selectable) {
return selectable[1] && selectable[1].context.supportsMultiSelect;
}
/**
* @private
*/
selectionContainsParent(selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1]));
}
/**
* @private
*/
isPeer(selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1]));
}
/**
* @private
*/
isSelectable(element) {
if (!element) {
return false;
}
return Boolean(element.closest('[data-selectable]'));
}
/**
* @private
*/
capture(selectable) {
let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable);
if (!this.capturing || capturingContainsSelectable) {
this.capturing = [];
}
this.capturing.push(selectable);
}
/**
* @private
*/
selectCapture(selectable, event) {
if (!this.capturing) {
return;
}
let reversedCapturing = this.capturing.reverse();
delete this.capturing;
this.select(reversedCapturing, event.shiftKey);
}
/**
* Attaches the click handlers to the element.
*
* @param element an html element
* @param context object which defines item or other arbitrary properties.
* e.g. {
* item: domainObject,
* elementProxy: element,
* controller: fixedController
* }
* @param select a flag to select the element if true
* @returns a function that removes the click handlers from the element
* @public
*/
selectable(element, context, select) {
if (!this.isSelectable(element)) {
return () => { };
}
let selectable = {
context: context,
element: element
}; };
/** const capture = this.capture.bind(this, selectable);
* @private const selectCapture = this.selectCapture.bind(this, selectable);
*/ let removeMutable = false;
Selection.prototype.handleSingleSelect = function (selectable) {
if (!_.isEqual([selectable], this.selected)) {
this.setSelectionStyles(selectable);
this.selected = [selectable];
this.emit('change', this.selected); element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture);
if (context.item && context.item.isMutable !== true) {
removeMutable = true;
context.item = this.openmct.objects.toMutable(context.item);
}
if (select) {
if (typeof select === 'object') {
element.dispatchEvent(select);
} else if (typeof select === 'boolean') {
element.click();
} }
}; }
/** return (function () {
* @private element.removeEventListener('click', capture, true);
*/ element.removeEventListener('click', selectCapture);
Selection.prototype.elementSelected = function (selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable));
};
/** if (context.item !== undefined && context.item.isMutable && removeMutable === true) {
* @private this.openmct.objects.destroyMutable(context.item);
*/
Selection.prototype.remove = function (selectable) {
this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable));
if (this.selected.length === 0) {
this.removeSelectionAttributes(selectable);
selectable[1].element.click(); // Select the parent if there is no selection.
} else {
this.removeSelectionAttributes(selectable, true);
} }
}; }).bind(this);
}
/** }
* @private
*/
Selection.prototype.setSelectionStyles = function (selectable) {
this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath));
this.addSelectionAttributes(selectable);
};
Selection.prototype.removeSelectionAttributes = function (selectionPath, keepParentStyle) {
if (selectionPath[0] && selectionPath[0].element) {
selectionPath[0].element.removeAttribute('s-selected');
}
if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) {
selectionPath[1].element.removeAttribute('s-selected-parent');
}
};
/*
* Adds selection attributes to the selected element and its parent.
* @private
*/
Selection.prototype.addSelectionAttributes = function (selectable) {
if (selectable[0] && selectable[0].element) {
selectable[0].element.setAttribute('s-selected', "");
}
if (selectable[1] && selectable[1].element) {
selectable[1].element.setAttribute('s-selected-parent', "");
}
};
/**
* @private
*/
Selection.prototype.parentSupportsMultiSelect = function (selectable) {
return selectable[1] && selectable[1].context.supportsMultiSelect;
};
/**
* @private
*/
Selection.prototype.selectionContainsParent = function (selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1]));
};
/**
* @private
*/
Selection.prototype.isPeer = function (selectable) {
return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1]));
};
/**
* @private
*/
Selection.prototype.isSelectable = function (element) {
if (!element) {
return false;
}
return Boolean(element.closest('[data-selectable]'));
};
/**
* @private
*/
Selection.prototype.capture = function (selectable) {
let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable);
if (!this.capturing || capturingContainsSelectable) {
this.capturing = [];
}
this.capturing.push(selectable);
};
/**
* @private
*/
Selection.prototype.selectCapture = function (selectable, event) {
if (!this.capturing) {
return;
}
let reversedCapturing = this.capturing.reverse();
delete this.capturing;
this.select(reversedCapturing, event.shiftKey);
};
/**
* Attaches the click handlers to the element.
*
* @param element an html element
* @param context object which defines item or other arbitrary properties.
* e.g. {
* item: domainObject,
* elementProxy: element,
* controller: fixedController
* }
* @param select a flag to select the element if true
* @returns a function that removes the click handlers from the element
* @public
*/
Selection.prototype.selectable = function (element, context, select) {
if (!this.isSelectable(element)) {
return () => {};
}
let selectable = {
context: context,
element: element
};
const capture = this.capture.bind(this, selectable);
const selectCapture = this.selectCapture.bind(this, selectable);
let removeMutable = false;
element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture);
if (context.item && context.item.isMutable !== true) {
removeMutable = true;
context.item = this.openmct.objects.toMutable(context.item);
}
if (select) {
if (typeof select === 'object') {
element.dispatchEvent(select);
} else if (typeof select === 'boolean') {
element.click();
}
}
return (function () {
element.removeEventListener('click', capture, true);
element.removeEventListener('click', selectCapture);
if (context.item !== undefined && context.item.isMutable && removeMutable === true) {
this.openmct.objects.destroyMutable(context.item);
}
}).bind(this);
};
return Selection;
});

View File

@ -7,19 +7,27 @@
"baseUrl": "./", "baseUrl": "./",
"allowJs": true, "allowJs": true,
"checkJs": false, "checkJs": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationMap": true,
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "dist", "outDir": "dist",
"skipLibCheck": true,
"paths": { "paths": {
// matches the alias in webpack config, so that types for those imports are visible. // matches the alias in webpack config, so that types for those imports are visible.
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": [
"src/api/**/*.js"
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist" "dist",
"**/*Spec.js"
] ]
} }