Compare commits

...

6 Commits

Author SHA1 Message Date
David Tsay
e1c5e8bfe1 disable prod source maps 2024-01-10 13:08:13 -08:00
David Tsay
ef957d6184 temporarily enable source maps 2024-01-10 12:42:37 -08:00
Jesse Mazzella
40373abfe3 fix: 2d canvas fallback logic (#7295) 2023-12-12 15:13:01 -08:00
Scott Bell
86d4244ace cherry-pick(#7262): Update API documentation for Visibility-Based Rendering (#7267)
Update API documentation for Visibility-Based Rendering (#7262)

update API with documentation for Visibility-Based Rendering
2023-12-01 10:33:09 -05:00
Jesse Mazzella
da299e9b95 chore: bump version to 3.2.0 (#7266) 2023-11-30 14:18:54 -08:00
Scott Bell
f0dcf2ba21 cherry-pick((#7241) Provide visibility based rendering as part of the view api (#7249)
Provide visibility based rendering as part of the view api (#7241)

* first draft

* in preview mode, just show it

* fix unit tests
2023-11-20 18:50:31 +01:00
30 changed files with 193 additions and 97 deletions

56
API.md
View File

@@ -1304,3 +1304,59 @@ View provider Example:
}
}
```
## Visibility-Based Rendering in View Providers
To enhance performance and resource efficiency in OpenMCT, a visibility-based rendering feature has been added. This feature is designed to defer the execution of rendering logic for views that are not currently visible. It ensures that views are only updated when they are in the viewport, similar to how modern browsers handle rendering of inactive tabs but optimized for the OpenMCT tabbed display. It also works when views are scrolled outside the viewport (e.g., in a Display Layout).
### Overview
The show function is responsible for the rendering of a view. An [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) is used internally to determine whether the view is visible. This observer drives the visibility-based rendering feature, accessed via the `renderWhenVisible` function provided in the `viewOptions` parameter.
### Implementing Visibility-Based Rendering
The `renderWhenVisible` function is passed to the show function as a required part of the `viewOptions` object. This function should be used for all rendering logic that would otherwise be executed within a `requestAnimationFrame` call. When called, `renderWhenVisible` will either execute the provided function immediately (via `requestAnimationFrame`) if the view is currently visible, or defer its execution until the view becomes visible.
Additionally, `renderWhenVisible` returns a boolean value indicating whether the provided function was executed immediately (`true`) or deferred (`false`).
Heres the signature for the show function:
`show(element, isEditing, viewOptions)`
* `element` (HTMLElement) - The DOM element where the view should be rendered.
* `isEditing` (boolean) - Indicates whether the view is in editing mode.
* `viewOptions` (Object) - A required object with configuration options for the view, including:
* `renderWhenVisible` (Function) - This function wraps the `requestAnimationFrame` and only triggers the provided render logic when the view is visible in the viewport.
### Example
An OpenMCT view provider might implement the show function as follows:
```js
// Define your view provider
const myViewProvider = {
// ... other properties and methods ...
show: function (element, isEditing, viewOptions) {
// Callback for rendering view content
const renderCallback = () => {
// Your view rendering logic goes here
};
// Use the renderWhenVisible function to ensure rendering only happens when view is visible
const wasRenderedImmediately = viewOptions.renderWhenVisible(renderCallback);
// Optionally handle the immediate rendering return value
if (wasRenderedImmediately) {
console.debug('🪞 Rendering triggered immediately as the view is visible.');
} else {
console.debug('🛑 Rendering has been deferred until the view becomes visible.');
}
}
};
```
Note that `renderWhenVisible` defers rendering while the view is not visible and caters to the latest execution call. This provides responsiveness for dynamic content while ensuring performance optimizations.
Ensure your view logic is prepared to handle potentially multiple deferrals if using this API, as only the last call to renderWhenVisible will be queued for execution upon the view becoming visible.

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "3.2.0-next",
"version": "3.2.0",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",

View File

@@ -34,7 +34,7 @@ export default class LADTableView {
this._destroy = null;
}
show(element) {
show(element, isEditing, { renderWhenVisible }) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount(
@@ -46,7 +46,8 @@ export default class LADTableView {
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
ladTableConfiguration,
renderWhenVisible
},
data: () => {
return {

View File

@@ -54,12 +54,11 @@ const BLANK_VALUE = '---';
import identifierToString from '/src/tools/url';
import PreviewAction from '@/ui/preview/PreviewAction.js';
import NicelyCalled from '../../../api/nice/NicelyCalled';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
export default {
mixins: [tooltipHelpers],
inject: ['openmct', 'currentView'],
inject: ['openmct', 'currentView', 'renderWhenVisible'],
props: {
domainObject: {
type: Object,
@@ -190,7 +189,6 @@ export default {
}
},
async mounted() {
this.nicelyCalled = new NicelyCalled(this.$refs.tableRow);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -239,12 +237,11 @@ export default {
this.previewAction.off('isVisible', this.togglePreviewState);
this.telemetryCollection.destroy();
this.nicelyCalled.destroy();
},
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = this.nicelyCalled.execute(() => {
this.updatingView = this.renderWhenVisible(() => {
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
this.updatingView = false;

View File

@@ -24,6 +24,7 @@ import {
getLatestTelemetry,
getMockObjects,
getMockTelemetry,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
@@ -225,7 +226,7 @@ describe('The LAD Table', () => {
(viewProvider) => viewProvider.key === ladTableKey
);
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true);
ladTableView.show(child, true, { renderWhenVisible });
await Promise.all([
telemetryRequestPromise,

View File

@@ -73,14 +73,18 @@ import {
} from '@/plugins/notebook/utils/notebook-storage.js';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import NicelyCalled from '../../../api/nice/NicelyCalled';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
import LayoutFrame from './LayoutFrame.vue';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = [
'copyToClipboard',
'copyToNotebook',
'viewHistoricalData',
'renderWhenVisible'
];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
@@ -106,7 +110,7 @@ export default {
LayoutFrame
},
mixins: [conditionalStylesMixin, stalenessMixin, tooltipHelpers],
inject: ['openmct', 'objectPath', 'currentView'],
inject: ['openmct', 'objectPath', 'currentView', 'renderWhenVisible'],
props: {
item: {
type: Object,
@@ -274,7 +278,6 @@ export default {
}
this.setObject(foundObject);
await this.$nextTick();
this.nicelyCalled = new NicelyCalled(this.$refs.telemetryViewWrapper);
},
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
@@ -291,7 +294,7 @@ export default {
},
updateView() {
if (!this.updatingView) {
this.updatingView = this.nicelyCalled.execute(() => {
this.updatingView = this.renderWhenVisible(() => {
this.datum = this.latestDatum;
this.updatingView = false;
});

View File

@@ -39,7 +39,7 @@ class DisplayLayoutView {
this.component = null;
}
show(container, isEditing) {
show(container, isEditing, { renderWhenVisible }) {
const { vNode, destroy } = mount(
{
el: container,
@@ -50,7 +50,8 @@ class DisplayLayoutView {
openmct: this.openmct,
objectPath: this.objectPath,
options: this.options,
currentView: this
currentView: this,
renderWhenVisible
},
data: () => {
return {

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';
import { nextTick } from 'vue';
import DisplayLayoutPlugin from './plugin';
@@ -114,7 +114,7 @@ describe('the plugin', function () {
let error;
try {
view.show(child, false);
view.show(child, false, { renderWhenVisible });
} catch (e) {
error = e;
}
@@ -161,7 +161,7 @@ describe('the plugin', function () {
(viewProvider) => viewProvider.key === 'layout.view'
);
const view = displayLayoutViewProvider.view(displayLayoutItem, displayLayoutItem);
view.show(child, false);
view.show(child, false, { renderWhenVisible });
nextTick(done);
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { debounce } from 'lodash';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';
import { nextTick } from 'vue';
let gaugeDomainObject = {
@@ -172,7 +172,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});
@@ -314,7 +314,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});
@@ -456,7 +456,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});
@@ -560,7 +560,7 @@ describe('Gauge plugin', () => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});
@@ -643,7 +643,7 @@ describe('Gauge plugin', () => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});
@@ -771,7 +771,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child);
gaugeView.show(child, false, { renderWhenVisible });
return nextTick();
});

View File

@@ -41,7 +41,7 @@ export default function GaugeViewProvider(openmct) {
let _destroy = null;
return {
show: function (element) {
show: function (element, isEditing, { renderWhenVisible }) {
const { destroy } = mount(
{
el: element,
@@ -51,7 +51,8 @@ export default function GaugeViewProvider(openmct) {
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject)
composition: openmct.composition.get(domainObject),
renderWhenVisible
},
template: '<gauge-component></gauge-component>'
},

View File

@@ -336,7 +336,6 @@
<script>
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import NicelyCalled from '../../../api/nice/NicelyCalled';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
@@ -345,7 +344,7 @@ const DEFAULT_CURRENT_VALUE = '--';
export default {
mixins: [stalenessMixin, tooltipHelpers],
inject: ['openmct', 'domainObject', 'composition'],
inject: ['openmct', 'domainObject', 'composition', 'renderWhenVisible'],
data() {
let gaugeController = this.domainObject.configuration.gaugeController;
@@ -539,7 +538,6 @@ export default {
}
},
mounted() {
this.nicelyCalled = new NicelyCalled(this.$refs.gaugeWrapper);
this.composition.on('add', this.addedToComposition);
this.composition.on('remove', this.removeTelemetryObject);
@@ -563,8 +561,6 @@ export default {
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.nicelyCalled.destroy();
},
methods: {
getLimitDegree: getLimitDegree,
@@ -737,7 +733,7 @@ export default {
return;
}
this.isRendering = this.nicelyCalled.execute(() => {
this.isRendering = this.renderWhenVisible(() => {
this.isRendering = false;
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);

View File

@@ -633,13 +633,22 @@ describe('The Imagery View Layouts', () => {
imageWrapper[2].dispatchEvent(mouseDownEvent);
await nextTick();
const timestamp = imageWrapper[2].id.replace('wrapper-', '');
expect(componentView.previewAction.invoke).toHaveBeenCalledWith(
[componentView.objectPath[0]],
{
timestamp: Number(timestamp),
objectPath: componentView.objectPath
}
);
const mockInvoke = componentView.previewAction.invoke;
// Make sure the function was called
expect(mockInvoke).toHaveBeenCalled();
// Get the arguments of the first call
const firstArg = mockInvoke.calls.mostRecent().args[0];
const secondArg = mockInvoke.calls.mostRecent().args[1];
// Compare the first argument
expect(firstArg).toEqual([componentView.objectPath[0]]);
// Compare the "timestamp" property of the second argument
expect(secondArg.timestamp).toEqual(Number(timestamp));
// Compare the "objectPath" property of the second argument
expect(secondArg.objectPath).toEqual(componentView.objectPath);
});
it('should remove images when clock advances', async () => {

View File

@@ -198,7 +198,7 @@ export default {
MctTicks,
MctChart
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: {
options: {
type: Object,

View File

@@ -65,7 +65,7 @@ export default function PlotViewProvider(openmct) {
let component = null;
return {
show: function (element) {
show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
{
@@ -76,7 +76,8 @@ export default function PlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
path: objectPath
path: objectPath,
renderWhenVisible
},
data() {
return {

View File

@@ -45,7 +45,6 @@
import mount from 'utils/mount';
import { toRaw } from 'vue';
import NicelyCalled from '../../../api/nice/NicelyCalled';
import configStore from '../configuration/ConfigStore';
import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
import { DrawLoader } from '../draw/DrawLoader';
@@ -100,7 +99,7 @@ const HANDLED_ATTRIBUTES = {
export default {
components: { LimitLine, LimitLabel },
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: {
rectangles: {
type: Array,
@@ -199,7 +198,6 @@ export default {
},
mounted() {
eventHelpers.extend(this);
this.nicelyCalled = new NicelyCalled(this.$refs.chart);
this.seriesModels = [];
this.config = this.getConfig();
this.isDestroyed = false;
@@ -258,7 +256,6 @@ export default {
},
beforeUnmount() {
this.destroy();
this.nicelyCalled.destroy();
},
methods: {
getConfig() {
@@ -509,6 +506,7 @@ export default {
this.overlay = overlayCanvas;
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$emit('plot-reinitialize-canvas');
console.warn(`📈 fallback to 2D canvas`);
},
removeChartElement(series) {
const elements = this.seriesElements.get(toRaw(series));
@@ -650,7 +648,7 @@ export default {
},
scheduleDraw() {
if (!this.drawScheduled) {
const called = this.nicelyCalled.execute(this.draw);
const called = this.renderWhenVisible(this.draw);
this.drawScheduled = called;
}
},

View File

@@ -154,14 +154,14 @@ DrawWebGL.prototype.initContext = function () {
DrawWebGL.prototype.destroy = function () {
// Lose the context and delete all associated resources
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#lose_contexts_eagerly
this.gl.getExtension('WEBGL_lose_context').loseContext();
this.gl.deleteBuffer(this.buffer);
this.gl?.getExtension('WEBGL_lose_context')?.loseContext();
this.gl?.deleteBuffer(this.buffer);
this.buffer = undefined;
this.gl.deleteProgram(this.program);
this.gl?.deleteProgram(this.program);
this.program = undefined;
this.gl.deleteShader(this.vertexShader);
this.gl?.deleteShader(this.vertexShader);
this.vertexShader = undefined;
this.gl.deleteShader(this.fragmentShader);
this.gl?.deleteShader(this.fragmentShader);
this.fragmentShader = undefined;
this.gl = undefined;

View File

@@ -47,7 +47,7 @@ export default function OverlayPlotViewProvider(openmct) {
let component = null;
return {
show: function (element) {
show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
{
@@ -58,7 +58,8 @@ export default function OverlayPlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
path: objectPath
path: objectPath,
renderWhenVisible
},
data() {
return {

View File

@@ -25,6 +25,7 @@ import mount from 'utils/mount';
import {
createMouseEvent,
createOpenMct,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
@@ -314,7 +315,8 @@ describe('the plugin', function () {
openmct,
domainObject: overlayPlotObject,
composition,
path: [overlayPlotObject]
path: [overlayPlotObject],
renderWhenVisible
},
template: '<plot ref="plotComponent"></plot>'
},
@@ -505,7 +507,8 @@ describe('the plugin', function () {
openmct: openmct,
domainObject: overlayPlotObject,
composition,
path: [overlayPlotObject]
path: [overlayPlotObject],
renderWhenVisible
},
template: '<plot ref="plotComponent"></plot>'
},

View File

@@ -25,6 +25,7 @@ import mount from 'utils/mount';
import {
createMouseEvent,
createOpenMct,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
@@ -372,7 +373,7 @@ describe('the plugin', function () {
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');
plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true);
plotView.show(child, true, { renderWhenVisible });
return nextTick();
});
@@ -654,7 +655,7 @@ describe('the plugin', function () {
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');
plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true);
plotView.show(child, true, { renderWhenVisible });
resizePromise = new Promise((resolve) => {
resizePromiseResolve = resolve;
@@ -811,7 +812,8 @@ describe('the plugin', function () {
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item]
path: [selection[0][0].context.item, selection[0][1].context.item],
renderWhenVisible
},
template: '<plot-options ref="root"/>'
},

View File

@@ -76,7 +76,7 @@ export default {
StackedPlotItem,
PlotLegend
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: {
options: {
type: Object,

View File

@@ -34,7 +34,7 @@ import conditionalStylesMixin from './mixins/objectStyles-mixin';
export default {
mixins: [conditionalStylesMixin, stalenessMixin],
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: {
childObject: {
type: Object,
@@ -217,7 +217,8 @@ export default {
provide: {
openmct,
domainObject: object,
path
path,
renderWhenVisible: this.renderWhenVisible
},
data() {
return {

View File

@@ -48,7 +48,7 @@ export default function StackedPlotViewProvider(openmct) {
let component = null;
return {
show: function (element) {
show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
@@ -60,7 +60,8 @@ export default function StackedPlotViewProvider(openmct) {
provide: {
openmct,
domainObject,
path: objectPath
path: objectPath,
renderWhenVisible
},
data() {
return {

View File

@@ -25,6 +25,7 @@ import mount from 'utils/mount';
import {
createMouseEvent,
createOpenMct,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
@@ -329,7 +330,8 @@ describe('the plugin', function () {
provide: {
openmct,
domainObject: stackedPlotObject,
path: [stackedPlotObject]
path: [stackedPlotObject],
renderWhenVisible
},
template: '<stacked-plot ref="stackedPlotRef"></stacked-plot>'
},
@@ -619,7 +621,8 @@ describe('the plugin', function () {
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item]
path: [selection[0][0].context.item],
renderWhenVisible
},
template: '<plot-options/>'
},
@@ -774,7 +777,8 @@ describe('the plugin', function () {
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item]
path: [selection[0][0].context.item, selection[0][1].context.item],
renderWhenVisible
},
template: '<plot-options />'
},

View File

@@ -65,7 +65,7 @@ export default class TelemetryTableView {
}
}
show(element, editMode) {
show(element, editMode, { renderWhenVisible }) {
const { vNode, destroy } = mount(
{
el: element,
@@ -76,7 +76,8 @@ export default class TelemetryTableView {
openmct: this.openmct,
objectPath: this.objectPath,
table: this.table,
currentView: this
currentView: this,
renderWhenVisible
},
data() {
return {

View File

@@ -280,7 +280,6 @@ import { toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import NicelyCalled from '../../../api/nice/NicelyCalled';
import CSVExporter from '../../../exporters/CSVExporter.js';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue';
@@ -306,7 +305,7 @@ export default {
ProgressBar
},
mixins: [stalenessMixin],
inject: ['openmct', 'objectPath', 'table', 'currentView'],
inject: ['openmct', 'objectPath', 'table', 'currentView', 'renderWhenVisible'],
props: {
isEditing: {
type: Boolean,
@@ -481,7 +480,6 @@ export default {
this.filterChanged = _.debounce(this.filterChanged, 500);
},
mounted() {
this.nicelyCalled = new NicelyCalled(this.$refs.root);
this.csvExporter = new CSVExporter();
this.rowsAdded = _.throttle(this.rowsAdded, 200);
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
@@ -547,13 +545,11 @@ export default {
this.table.configuration.destroy();
this.table.destroy();
this.nicelyCalled.destroy();
},
methods: {
updateVisibleRows() {
if (!this.updatingView) {
this.updatingView = this.nicelyCalled.execute(() => {
this.updatingView = this.renderWhenVisible(() => {
let start = 0;
let end = VISIBLE_ROW_COUNT;
let tableRows = this.table.tableRows.getRows();
@@ -829,7 +825,7 @@ export default {
let scrollTop = this.scrollable.scrollTop;
this.resizePollHandle = setInterval(() => {
this.nicelyCalled.execute(() => {
this.renderWhenVisible(() => {
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?

View File

@@ -22,6 +22,7 @@
import {
createMouseEvent,
createOpenMct,
renderWhenVisible,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
@@ -236,7 +237,7 @@ describe('the plugin', () => {
applicableViews = openmct.objectViews.get(testTelemetryObject, []);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
tableView.show(child, true);
tableView.show(child, true, { renderWhenVisible });
tableInstance = tableView.getTable();

View File

@@ -33,6 +33,8 @@ import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver';
export default {
mixins: [stalenessMixin],
inject: ['openmct'],
@@ -113,6 +115,9 @@ export default {
this.actionCollection.destroy();
delete this.actionCollection;
}
if (this.visibilityObserver) {
this.visibilityObserver.destroy();
}
this.$refs.objectViewWrapper.removeEventListener('dragover', this.onDragOver, {
capture: true
});
@@ -125,6 +130,7 @@ export default {
this.debounceUpdateView = _.debounce(this.updateView, 10);
},
mounted() {
this.visibilityObserver = new VisibilityObserver(this.$refs.objectViewWrapper);
this.updateView();
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
capture: true
@@ -290,7 +296,9 @@ export default {
}
}
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing());
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing(), {
renderWhenVisible: this.visibilityObserver.renderWhenVisible
});
if (immediatelySelect) {
this.removeSelectable = this.openmct.selection.selectable(

View File

@@ -22,6 +22,7 @@
<template>
<div class="l-preview-window js-preview-window">
<PreviewHeader
ref="previewHeader"
:current-view="currentViewProvider"
:action-collection="actionCollection"
:domain-object="domainObject"
@@ -48,7 +49,7 @@ export default {
viewOptions: {
type: Object,
default() {
return undefined;
return {};
}
},
existingView: {
@@ -147,6 +148,11 @@ export default {
if (isExistingView) {
this.viewContainer.appendChild(this.existingViewElement);
} else {
// in preview mode, we're always visible
this.viewOptions.renderWhenVisible = (func) => {
window.requestAnimationFrame(func);
return true;
};
this.view.show(this.viewContainer, false, this.viewOptions);
}

View File

@@ -279,6 +279,12 @@ export function getMockTelemetry(opts = {}) {
return telemetry;
}
// used to inject into tests that require a render
export function renderWhenVisible(func) {
func();
return true;
}
// copy objects a bit more easily
function copyObj(obj) {
return JSON.parse(JSON.stringify(obj));

View File

@@ -23,36 +23,38 @@
/**
* Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport.
*/
export default class NicelyCalled {
export default class VisibilityObserver {
#element;
#isIntersecting;
#observer;
#lastUnfiredFunc;
lastUnfiredFunc;
/**
* Constructs a NicelyCalled instance to manage visibility-based requestAnimationFrame calls.
* Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.
*
* @param {HTMLElement} element - The DOM element to observe for visibility changes.
* @throws {Error} If element is not provided.
*/
constructor(element) {
if (!element) {
throw new Error(`Nice visibility must be created with an element`);
throw new Error(`VisibilityObserver must be created with an element`);
}
// set the id to some random 4 letters
this.id = Math.random().toString(36).substring(2, 6);
this.#element = element;
this.#isIntersecting = true;
this.isIntersecting = true;
this.#observer = new IntersectionObserver(this.#observerCallback);
this.#observer.observe(this.#element);
this.#lastUnfiredFunc = null;
this.lastUnfiredFunc = null;
this.renderWhenVisible = this.renderWhenVisible.bind(this);
}
#observerCallback = ([entry]) => {
if (entry.target === this.#element) {
this.#isIntersecting = entry.isIntersecting;
if (this.#isIntersecting && this.#lastUnfiredFunc) {
window.requestAnimationFrame(this.#lastUnfiredFunc);
this.#lastUnfiredFunc = null;
this.isIntersecting = entry.isIntersecting;
if (this.isIntersecting && this.lastUnfiredFunc) {
window.requestAnimationFrame(this.lastUnfiredFunc);
this.lastUnfiredFunc = null;
}
}
};
@@ -65,12 +67,12 @@ export default class NicelyCalled {
* @param {Function} func - The function to execute.
* @returns {boolean} True if the function was executed immediately, false otherwise.
*/
execute(func) {
if (this.#isIntersecting) {
renderWhenVisible(func) {
if (this.isIntersecting) {
window.requestAnimationFrame(func);
return true;
} else {
this.#lastUnfiredFunc = func;
this.lastUnfiredFunc = func;
return false;
}
}
@@ -81,8 +83,8 @@ export default class NicelyCalled {
destroy() {
this.#observer.unobserve(this.#element);
this.#element = null;
this.#isIntersecting = null;
this.isIntersecting = null;
this.#observer = null;
this.#lastUnfiredFunc = null;
this.lastUnfiredFunc = null;
}
}