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
This commit is contained in:
Scott Bell 2023-11-20 18:50:31 +01:00 committed by GitHub
parent 15ee8303e4
commit f0dcf2ba21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 130 additions and 91 deletions

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() {
@ -650,7 +647,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

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