/**
* ViewBase.tsx
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT license.
*
* A base class for the Web-specific implementation of the cross-platform View abstraction.
*/
import AppConfig from '../common/AppConfig';
import * as RX from '../common/Interfaces';
import { Defer } from '../common/utils/PromiseDefer';
import Timers from '../common/utils/Timers';
import FrontLayerViewManager from './FrontLayerViewManager';
import * as _ from './utils/lodashMini';
// We create a periodic timer to detect layout changes that are performed behind
// our back by the browser's layout engine. We do this more aggressively when
// the app is known to be active and in the foreground.
const _layoutTimerActiveDuration = 1000;
const _layoutTimerInactiveDuration = 10000;
export abstract class ViewBase
, S, C extends RX.View | RX.ScrollView> extends RX.ViewBase
{
private static _viewCheckingTimer: number | undefined;
private static _isResizeHandlerInstalled = false;
private static _viewCheckingList: ViewBase, any, RX.View | RX.ScrollView>[] = [];
private static _appActivationState = RX.Types.AppActivationState.Active;
abstract render(): JSX.Element;
protected abstract _getContainer(): HTMLElement | null;
protected _isMounted = false;
private _isPopupDisplayed = false;
// Sets the activation state so we can stop our periodic timer
// when the app is in the background.
static setActivationState(newState: RX.Types.AppActivationState) {
if (ViewBase._appActivationState !== newState) {
ViewBase._appActivationState = newState;
// Cancel any existing timers.
if (ViewBase._viewCheckingTimer) {
Timers.clearInterval(ViewBase._viewCheckingTimer);
ViewBase._viewCheckingTimer = undefined;
}
if (ViewBase._viewCheckingList.length > 0) {
// If we're becoming active, check and report layout changes immediately.
if (newState === RX.Types.AppActivationState.Active) {
ViewBase._checkViews();
}
ViewBase._viewCheckingTimer = Timers.setInterval(ViewBase._checkViews,
newState === RX.Types.AppActivationState.Active ?
_layoutTimerActiveDuration : _layoutTimerInactiveDuration);
}
}
}
UNSAFE_componentWillReceiveProps(nextProps: RX.Types.ViewPropsShared) {
if (!!this.props.onLayout !== !!nextProps.onLayout) {
if (this.props.onLayout) {
this._checkViewCheckerUnbuild();
}
if (nextProps.onLayout) {
this._checkViewCheckerBuild();
}
}
}
protected static _checkViews() {
_.each(ViewBase._viewCheckingList, view => {
view._checkAndReportLayout().catch(e => {
console.warn('ScrollView onLayout exception: ' + JSON.stringify(e));
});
});
}
private static _layoutReportList: Function[] = [];
private static _layoutReportingTimer: number | undefined;
private static _reportLayoutChange(func: Function) {
this._layoutReportList.push(func);
if (!ViewBase._layoutReportingTimer) {
ViewBase._layoutReportingTimer = Timers.setTimeout(() => {
ViewBase._layoutReportingTimer = undefined;
ViewBase._reportDeferredLayoutChanges();
}, 0);
}
}
protected static _reportDeferredLayoutChanges() {
const reportList = this._layoutReportList;
this._layoutReportList = [];
_.each(reportList, func => {
try {
func();
} catch (e) {
if (AppConfig.isDevelopmentMode()) {
console.error('Caught exception on onLayout response: ', e);
}
}
});
}
protected _lastX = 0;
protected _lastY = 0;
protected _lastWidth = 0;
protected _lastHeight = 0;
// Returns a promise to indicate when firing of onLayout event has completed (if any)
protected _checkAndReportLayout(): Promise {
if (!this._isMounted) {
return Promise.resolve(void 0);
}
const container = this._getContainer();
if (!container) {
return Promise.resolve(void 0);
}
const newX = container.offsetLeft;
const newY = container.offsetTop;
const marginTop = !container.style.marginTop ? 0 : parseInt(container.style.marginTop, 10) || 0;
const marginBottom = !container.style.marginBottom ? 0 : parseInt(container.style.marginBottom, 10) || 0;
const marginRight = !container.style.marginRight ? 0 : parseInt(container.style.marginRight, 10) || 0;
const marginLeft = !container.style.marginLeft ? 0 : parseInt(container.style.marginLeft, 10) || 0;
const newWidth = container.offsetWidth + marginRight + marginLeft;
const newHeight = container.offsetHeight + marginTop + marginBottom;
if (this._lastX !== newX || this._lastY !== newY || this._lastWidth !== newWidth || this._lastHeight !== newHeight) {
this._lastX = newX;
this._lastY = newY;
this._lastWidth = newWidth;
this._lastHeight = newHeight;
const deferred = new Defer();
ViewBase._reportLayoutChange(() => {
if (!this._isMounted || !this.props.onLayout) {
deferred.resolve(void 0);
return;
}
this.props.onLayout({
x: newX,
y: newY,
width: this._lastWidth,
height: this._lastHeight
});
deferred.resolve(void 0);
});
return deferred.promise();
}
return Promise.resolve(void 0);
}
private _checkViewCheckerBuild() {
// Enable the timer to check for layout changes. Use a different duration
// when the app is active versus inactive.
if (!ViewBase._viewCheckingTimer) {
ViewBase._viewCheckingTimer = Timers.setInterval(ViewBase._checkViews,
ViewBase._appActivationState === RX.Types.AppActivationState.Active ?
_layoutTimerActiveDuration : _layoutTimerInactiveDuration);
}
if (!ViewBase._isResizeHandlerInstalled) {
window.addEventListener('resize', ViewBase._onResize);
ViewBase._isResizeHandlerInstalled = true;
}
ViewBase._viewCheckingList.push(this);
}
private _checkViewCheckerUnbuild() {
ViewBase._viewCheckingList = _.filter(ViewBase._viewCheckingList, v => v !== this);
if (ViewBase._viewCheckingList.length === 0) {
if (ViewBase._viewCheckingTimer) {
Timers.clearInterval(ViewBase._viewCheckingTimer);
ViewBase._viewCheckingTimer = undefined;
}
if (ViewBase._isResizeHandlerInstalled) {
window.removeEventListener('resize', ViewBase._onResize);
ViewBase._isResizeHandlerInstalled = false;
}
}
}
componentDidMount() {
this._isMounted = true;
if (this.props.onLayout) {
this._checkViewCheckerBuild();
}
// Chain through to the same render-checking code
this.componentDidUpdate();
}
componentDidUpdate() {
const isPopupDisplayed = FrontLayerViewManager.isPopupDisplayed();
if (this.props.onLayout) {
if (isPopupDisplayed && !this._isPopupDisplayed) {
// A popup was just added to DOM. Checking layout now would stall script
// execution because the browser would have to do a reflow. Avoid that
// by deferring the work.
setTimeout(() => {
this._checkAndReportLayout().catch(e => {
console.warn('ScrollView onLayout exception: ' + JSON.stringify(e));
});
}, 0);
} else {
this._checkAndReportLayout().catch(e => {
console.warn('ScrollView onLayout exception: ' + JSON.stringify(e));
});
}
}
this._isPopupDisplayed = isPopupDisplayed;
}
private static _onResize() {
// Often views change size in response to an overall window resize. Rather than
// wait for the next timer to fire, do it immediately.
ViewBase._checkViews();
}
componentWillUnmount() {
this._isMounted = false;
if (this.props.onLayout) {
this._checkViewCheckerUnbuild();
}
}
}
export default ViewBase;