2018-05-11 17:07:51 +03:00
|
|
|
/*
|
|
|
|
Copyright 2018 New Vector Ltd.
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2018-07-12 20:43:49 +03:00
|
|
|
import React from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
|
|
|
|
import ResizeObserver from 'resize-observer-polyfill';
|
2018-05-11 17:07:51 +03:00
|
|
|
|
2020-05-14 05:41:41 +03:00
|
|
|
import dis from '../../../dispatcher/dispatcher';
|
2018-07-27 16:33:05 +03:00
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
|
|
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
|
|
|
// pass in a custom control as the actual body.
|
|
|
|
|
2018-07-11 20:07:32 +03:00
|
|
|
function getContainer(containerId) {
|
|
|
|
return document.getElementById(containerId);
|
|
|
|
}
|
|
|
|
|
2018-07-03 16:43:27 +03:00
|
|
|
function getOrCreateContainer(containerId) {
|
2018-07-11 20:07:32 +03:00
|
|
|
let container = getContainer(containerId);
|
2018-05-11 17:07:51 +03:00
|
|
|
|
|
|
|
if (!container) {
|
|
|
|
container = document.createElement("div");
|
2018-07-03 16:43:27 +03:00
|
|
|
container.id = containerId;
|
2018-05-11 17:07:51 +03:00
|
|
|
document.body.appendChild(container);
|
|
|
|
}
|
|
|
|
|
|
|
|
return container;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Class of component that renders its children in a separate ReactDOM virtual tree
|
|
|
|
* in a container element appended to document.body.
|
|
|
|
*
|
|
|
|
* This prevents the children from being unmounted when the parent of PersistedElement
|
|
|
|
* unmounts, allowing them to persist.
|
|
|
|
*
|
|
|
|
* When PE is unmounted, it hides the children using CSS. When mounted or updated, the
|
|
|
|
* children are made visible and are positioned into a div that is given the same
|
|
|
|
* bounding rect as the parent of PE.
|
|
|
|
*/
|
|
|
|
export default class PersistedElement extends React.Component {
|
2018-07-03 16:54:43 +03:00
|
|
|
static propTypes = {
|
|
|
|
// Unique identifier for this PersistedElement instance
|
|
|
|
// Any PersistedElements with the same persistKey will use
|
|
|
|
// the same DOM container.
|
|
|
|
persistKey: PropTypes.string.isRequired,
|
|
|
|
};
|
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.collectChildContainer = this.collectChildContainer.bind(this);
|
|
|
|
this.collectChild = this.collectChild.bind(this);
|
2018-07-27 16:33:05 +03:00
|
|
|
this._repositionChild = this._repositionChild.bind(this);
|
|
|
|
this._onAction = this._onAction.bind(this);
|
|
|
|
|
|
|
|
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
|
|
|
// Annoyingly, a resize observer is insufficient, since we also care
|
|
|
|
// about when the element moves on the screen without changing its
|
|
|
|
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
|
|
|
// for this, so we bodge it by listening for document resize and
|
|
|
|
// the timeline_resize action.
|
|
|
|
window.addEventListener('resize', this._repositionChild);
|
|
|
|
this._dispatcherRef = dis.register(this._onAction);
|
2018-05-11 17:07:51 +03:00
|
|
|
}
|
|
|
|
|
2018-07-11 20:07:32 +03:00
|
|
|
/**
|
|
|
|
* Removes the DOM elements created when a PersistedElement with the given
|
|
|
|
* persistKey was mounted. The DOM elements will be re-added if another
|
|
|
|
* PeristedElement is mounted in the future.
|
|
|
|
*
|
|
|
|
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
|
|
|
*/
|
|
|
|
static destroyElement(persistKey) {
|
|
|
|
const container = getContainer('mx_persistedElement_' + persistKey);
|
|
|
|
if (container) {
|
|
|
|
container.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static isMounted(persistKey) {
|
|
|
|
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
|
|
|
}
|
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
collectChildContainer(ref) {
|
2018-07-12 20:43:49 +03:00
|
|
|
if (this.childContainer) {
|
|
|
|
this.resizeObserver.unobserve(this.childContainer);
|
|
|
|
}
|
2018-05-11 17:07:51 +03:00
|
|
|
this.childContainer = ref;
|
2018-07-12 20:43:49 +03:00
|
|
|
if (ref) {
|
|
|
|
this.resizeObserver.observe(ref);
|
|
|
|
}
|
2018-05-11 17:07:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
collectChild(ref) {
|
|
|
|
this.child = ref;
|
|
|
|
this.updateChild();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.updateChild();
|
2020-04-01 23:30:57 +03:00
|
|
|
this.renderApp();
|
2018-05-11 17:07:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate() {
|
|
|
|
this.updateChild();
|
2020-04-01 23:30:57 +03:00
|
|
|
this.renderApp();
|
2018-05-11 17:07:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
this.updateChildVisibility(this.child, false);
|
2018-07-12 20:43:49 +03:00
|
|
|
this.resizeObserver.disconnect();
|
2018-07-27 16:33:05 +03:00
|
|
|
window.removeEventListener('resize', this._repositionChild);
|
|
|
|
dis.unregister(this._dispatcherRef);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onAction(payload) {
|
|
|
|
if (payload.action === 'timeline_resize') {
|
|
|
|
this._repositionChild();
|
|
|
|
}
|
2018-07-12 20:43:49 +03:00
|
|
|
}
|
|
|
|
|
2018-07-27 16:33:05 +03:00
|
|
|
_repositionChild() {
|
2018-07-12 20:43:49 +03:00
|
|
|
this.updateChildPosition(this.child, this.childContainer);
|
2018-05-11 17:07:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
updateChild() {
|
|
|
|
this.updateChildPosition(this.child, this.childContainer);
|
|
|
|
this.updateChildVisibility(this.child, true);
|
|
|
|
}
|
|
|
|
|
2020-04-01 23:30:57 +03:00
|
|
|
renderApp() {
|
|
|
|
const content = <div ref={this.collectChild} style={this.props.style}>
|
|
|
|
{this.props.children}
|
|
|
|
</div>;
|
|
|
|
|
|
|
|
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
|
|
|
}
|
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
updateChildVisibility(child, visible) {
|
|
|
|
if (!child) return;
|
|
|
|
child.style.display = visible ? 'block' : 'none';
|
|
|
|
}
|
|
|
|
|
2020-04-21 01:19:39 +03:00
|
|
|
/*
|
|
|
|
* Clip element bounding rectangle to that of the parent elements.
|
|
|
|
* This is not a full visibility check, but prevents the persisted
|
|
|
|
* element from overflowing parent containers when inside a scrolled
|
|
|
|
* area.
|
|
|
|
*/
|
|
|
|
_getClippedBoundingClientRect(element) {
|
|
|
|
let parentElement = element.parentElement;
|
|
|
|
let rect = element.getBoundingClientRect();
|
|
|
|
|
|
|
|
rect = new DOMRect(rect.left, rect.top, rect.width, rect.height);
|
|
|
|
|
|
|
|
while (parentElement) {
|
|
|
|
const parentRect = parentElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
if (parentRect.left > rect.left) {
|
|
|
|
rect.width = rect.width - (parentRect.left - rect.left);
|
|
|
|
rect.x = parentRect.x;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentRect.top > rect.top) {
|
|
|
|
rect.height = rect.height - (parentRect.top - rect.top);
|
|
|
|
rect.y = parentRect.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentRect.right < rect.right) {
|
|
|
|
rect.width = rect.width - (rect.right - parentRect.right);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentRect.bottom < rect.bottom) {
|
|
|
|
rect.height = rect.height - (rect.bottom - parentRect.bottom);
|
|
|
|
}
|
|
|
|
|
|
|
|
parentElement = parentElement.parentElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rect.width < 0) rect.width = 0;
|
|
|
|
if (rect.height < 0) rect.height = 0;
|
|
|
|
|
|
|
|
return rect;
|
|
|
|
}
|
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
updateChildPosition(child, parent) {
|
|
|
|
if (!child || !parent) return;
|
|
|
|
|
|
|
|
const parentRect = parent.getBoundingClientRect();
|
2020-04-21 01:19:39 +03:00
|
|
|
const clipRect = this._getClippedBoundingClientRect(parent);
|
|
|
|
|
|
|
|
Object.assign(child.parentElement.style, {
|
|
|
|
position: 'absolute',
|
|
|
|
top: clipRect.top + 'px',
|
|
|
|
left: clipRect.left + 'px',
|
|
|
|
width: clipRect.width + 'px',
|
|
|
|
height: clipRect.height + 'px',
|
|
|
|
overflow: "hidden",
|
|
|
|
});
|
|
|
|
|
2018-05-11 17:07:51 +03:00
|
|
|
Object.assign(child.style, {
|
|
|
|
position: 'absolute',
|
2020-04-21 01:19:39 +03:00
|
|
|
top: (parentRect.top - clipRect.top) + 'px',
|
|
|
|
left: (parentRect.left - clipRect.left) + 'px',
|
2018-05-11 17:07:51 +03:00
|
|
|
width: parentRect.width + 'px',
|
|
|
|
height: parentRect.height + 'px',
|
2020-04-21 01:19:39 +03:00
|
|
|
overflow: "hidden",
|
2018-05-11 17:07:51 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return <div ref={this.collectChildContainer}></div>;
|
|
|
|
}
|
|
|
|
}
|