flipper-plugin
Version:
Flipper Desktop plugin SDK and components
537 lines • 18.9 kB
JavaScript
"use strict";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Interactive = void 0;
const styled_1 = __importDefault(require("@emotion/styled"));
const react_1 = __importDefault(require("react"));
const LowPassFilter_1 = __importDefault(require("../utils/LowPassFilter"));
const snap_1 = require("../utils/snap");
const WINDOW_CURSOR_BOUNDARY = 5;
const ALL_RESIZABLE = {
bottom: true,
left: true,
right: true,
top: true,
};
const InteractiveContainer = styled_1.default.div({
willChange: 'transform, height, width, z-index',
});
InteractiveContainer.displayName = 'Interactive:InteractiveContainer';
class Interactive extends react_1.default.Component {
constructor(props, context) {
super(props, context);
this.onMouseMove = (event) => {
if (this.state.moving) {
this.calculateMove(event);
}
else if (this.state.resizing) {
this.calculateResize(event);
}
else {
this.calculateResizable(event);
}
};
this.startAction = (event) => {
this.globalMouse = true;
window.addEventListener('pointerup', this.endAction, { passive: true });
window.addEventListener('pointermove', this.onMouseMove, { passive: true });
const { isMovableAnchor } = this.props;
if (isMovableAnchor && isMovableAnchor(event)) {
this.startTitleAction(event);
}
else {
this.startWindowAction(event);
}
};
this.endAction = () => {
this.globalMouse = false;
window.removeEventListener('pointermove', this.onMouseMove);
window.removeEventListener('pointerup', this.endAction);
if (this.state.moving) {
this.resetMoving();
}
if (this.state.resizing) {
this.resetResizing();
}
};
this.onMouseLeave = () => {
if (!this.state.resizing && !this.state.moving) {
this.setState({
cursor: undefined,
});
}
};
this.onClick = (e) => {
if (this.state.couldResize) {
e.stopPropagation();
}
};
this.setRef = (ref) => {
this.ref = ref;
const { innerRef } = this.props;
if (innerRef) {
innerRef(ref);
}
};
this.onLocalMouseMove = (event) => {
if (!this.globalMouse) {
this.onMouseMove(event.nativeEvent);
}
};
this.state = {
couldResize: false,
cursor: undefined,
moving: false,
movingInitialCursor: null,
movingInitialProps: null,
resizing: false,
resizingInitialCursor: null,
resizingInitialRect: null,
resizingSides: null,
};
this.globalMouse = false;
}
startTitleAction(event) {
if (this.state.couldResize) {
this.startResizeAction(event);
}
else if (this.props.movable === true) {
this.startMoving(event);
}
}
startMoving(event) {
const { onMoveStart } = this.props;
if (onMoveStart) {
onMoveStart();
}
if (this.context.os) {
// pause OS timers to avoid lag when dragging
this.context.os.timers.pause();
}
const topLpf = new LowPassFilter_1.default();
const leftLpf = new LowPassFilter_1.default();
this.nextTop = null;
this.nextLeft = null;
this.nextEvent = null;
const onFrame = () => {
if (!this.state.moving) {
return;
}
const { nextEvent, nextTop, nextLeft } = this;
if (nextEvent && nextTop != null && nextLeft != null) {
if (topLpf.hasFullBuffer()) {
const newTop = topLpf.next(nextTop);
const newLeft = leftLpf.next(nextLeft);
this.move(newTop, newLeft, nextEvent);
}
else {
this.move(nextTop, nextLeft, nextEvent);
}
}
requestAnimationFrame(onFrame);
};
this.setState({
cursor: 'move',
moving: true,
movingInitialCursor: {
left: event.clientX,
top: event.clientY,
},
movingInitialProps: this.props,
}, onFrame);
}
getPossibleTargetWindows(rect) {
const closeWindows = [];
const { siblings } = this.props;
if (siblings) {
for (const key in siblings) {
if (key === this.props.id) {
// don't target ourselves
continue;
}
const win = siblings[key];
if (win) {
const distance = (0, snap_1.getDistanceTo)(rect, win);
if (distance <= snap_1.SNAP_SIZE) {
closeWindows.push(win);
}
}
}
}
return closeWindows;
}
startWindowAction(event) {
if (this.state.couldResize) {
this.startResizeAction(event);
}
}
startResizeAction(event) {
event.stopPropagation();
event.preventDefault();
const { onResizeStart } = this.props;
if (onResizeStart) {
onResizeStart();
}
this.setState({
resizing: true,
resizingInitialCursor: {
left: event.clientX,
top: event.clientY,
},
resizingInitialRect: this.getRect(),
});
}
componentDidUpdate(_prevProps, prevState) {
if (prevState.cursor !== this.state.cursor) {
const { updateCursor } = this.props;
if (updateCursor) {
updateCursor(this.state.cursor);
}
}
}
resetMoving() {
const { onMoveEnd } = this.props;
if (onMoveEnd) {
onMoveEnd();
}
if (this.context.os) {
// resume os timers
this.context.os.timers.resume();
}
this.setState({
cursor: undefined,
moving: false,
movingInitialProps: undefined,
resizingInitialCursor: undefined,
});
}
resetResizing() {
const { onResizeEnd } = this.props;
if (onResizeEnd) {
onResizeEnd();
}
this.setState({
resizing: false,
resizingInitialCursor: undefined,
resizingInitialRect: undefined,
resizingSides: undefined,
});
}
componentWillUnmount() {
this.endAction();
}
calculateMove(event) {
const { movingInitialCursor, movingInitialProps } = this.state;
const { clientX: cursorLeft, clientY: cursorTop } = event;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const movedLeft = movingInitialCursor.left - cursorLeft;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const movedTop = movingInitialCursor.top - cursorTop;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newLeft = (movingInitialProps.left || 0) - movedLeft;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newTop = (movingInitialProps.top || 0) - movedTop;
if (event.altKey) {
const snapProps = this.getRect();
const windows = this.getPossibleTargetWindows(snapProps);
newLeft = (0, snap_1.maybeSnapLeft)(snapProps, windows, newLeft);
newTop = (0, snap_1.maybeSnapTop)(snapProps, windows, newTop);
}
this.nextTop = newTop;
this.nextLeft = newLeft;
this.nextEvent = event;
}
resize(width, height) {
if (width === this.props.width && height === this.props.height) {
// noop
return;
}
const { onResize } = this.props;
if (!onResize) {
return;
}
width = Math.max(this.props.minWidth || 0, width);
height = Math.max(this.props.minHeight || 0, height);
const { maxHeight, maxWidth } = this.props;
if (maxWidth != null) {
width = Math.min(maxWidth, width);
}
if (maxHeight != null) {
height = Math.min(maxHeight, height);
}
onResize(width, height);
}
move(top, left, event) {
top = Math.max(this.props.minTop || 0, top);
left = Math.max(this.props.minLeft || 0, left);
if (top === this.props.top && left === this.props.left) {
// noop
return;
}
const { onMove } = this.props;
if (onMove) {
onMove(top, left, event);
}
}
calculateResize(event) {
const { resizingInitialCursor, resizingInitialRect, resizingSides } = this.state;
if (!resizingSides || !resizingInitialCursor) {
return;
}
const deltaLeft = resizingInitialCursor.left - event.clientX;
const deltaTop = resizingInitialCursor.top - event.clientY;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newLeft = resizingInitialRect.left;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newTop = resizingInitialRect.top;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newWidth = resizingInitialRect.width;
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let newHeight = resizingInitialRect.height;
// right
if (resizingSides.right === true) {
newWidth -= deltaLeft;
}
// bottom
if (resizingSides.bottom === true) {
newHeight -= deltaTop;
}
const rect = this.getRect();
// left
if (resizingSides.left === true) {
newLeft -= deltaLeft;
newWidth += deltaLeft;
if (this.props.movable === true) {
// prevent from being shrunk past the minimum width
const right = rect.left + rect.width;
const maxLeft = right - (this.props.minWidth || 0);
let cleanLeft = Math.max(0, newLeft);
cleanLeft = Math.min(cleanLeft, maxLeft);
newWidth -= Math.abs(newLeft - cleanLeft);
newLeft = cleanLeft;
}
}
// top
if (resizingSides.top === true) {
newTop -= deltaTop;
newHeight += deltaTop;
if (this.props.movable === true) {
// prevent from being shrunk past the minimum height
const bottom = rect.top + rect.height;
const maxTop = bottom - (this.props.minHeight || 0);
let cleanTop = Math.max(0, newTop);
cleanTop = Math.min(cleanTop, maxTop);
newHeight += newTop - cleanTop;
newTop = cleanTop;
}
}
if (event.altKey) {
const windows = this.getPossibleTargetWindows(rect);
if (resizingSides.left === true) {
const newLeft2 = (0, snap_1.maybeSnapLeft)(rect, windows, newLeft);
newWidth += newLeft - newLeft2;
newLeft = newLeft2;
}
if (resizingSides.top === true) {
const newTop2 = (0, snap_1.maybeSnapTop)(rect, windows, newTop);
newHeight += newTop - newTop2;
newTop = newTop2;
}
if (resizingSides.bottom === true) {
newHeight = (0, snap_1.maybeSnapTop)(rect, windows, newTop + newHeight) - newTop;
}
if (resizingSides.right === true) {
newWidth = (0, snap_1.maybeSnapLeft)(rect, windows, newLeft + newWidth) - newLeft;
}
}
this.move(newTop, newLeft, event);
this.resize(newWidth, newHeight);
}
getRect() {
const { props, ref } = this;
if (!ref)
throw new Error('expected ref');
return {
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
height: ref.offsetHeight || 0,
left: props.left || 0,
top: props.top || 0,
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
width: ref.offsetWidth || 0,
};
}
getResizable() {
const { resizable } = this.props;
if (resizable === true) {
return ALL_RESIZABLE;
}
else if (resizable == null || resizable === false) {
return;
}
else {
return resizable;
}
}
checkIfResizable(event) {
const canResize = this.getResizable();
if (!canResize) {
return;
}
if (!this.ref) {
return;
}
const { left: offsetLeft, top: offsetTop } = this.ref.getBoundingClientRect();
const { height, width } = this.getRect();
const x = event.clientX - offsetLeft;
const y = event.clientY - offsetTop;
const gutterWidth = this.props.gutterWidth || WINDOW_CURSOR_BOUNDARY;
const atTop = y <= gutterWidth;
const atBottom = y >= height - gutterWidth;
const atLeft = x <= gutterWidth;
const atRight = x >= width - gutterWidth;
return {
bottom: canResize.bottom === true && atBottom,
left: canResize.left === true && atLeft,
right: canResize.right === true && atRight,
top: canResize.top === true && atTop,
};
}
calculateResizable(event) {
const resizing = this.checkIfResizable(event);
if (!resizing) {
return;
}
const canResize = this.getResizable();
if (!canResize) {
return;
}
const { bottom, left, right, top } = resizing;
let newCursor;
const movingHorizontal = left || right;
const movingVertical = top || left;
// left
if (left) {
newCursor = 'ew-resize';
}
// right
if (right) {
newCursor = 'ew-resize';
}
// if resizing vertically and one side can't be resized then use different cursor
if (movingHorizontal &&
(canResize.left !== true || canResize.right !== true)) {
newCursor = 'col-resize';
}
// top
if (top) {
newCursor = 'ns-resize';
// top left
if (left) {
newCursor = 'nwse-resize';
}
// top right
if (right) {
newCursor = 'nesw-resize';
}
}
// bottom
if (bottom) {
newCursor = 'ns-resize';
// bottom left
if (left) {
newCursor = 'nesw-resize';
}
// bottom right
if (right) {
newCursor = 'nwse-resize';
}
}
// if resizing horziontally and one side can't be resized then use different cursor
if (movingVertical &&
!movingHorizontal &&
(canResize.top !== true || canResize.bottom !== true)) {
newCursor = 'row-resize';
}
if (this.state.resizingSides?.bottom === bottom &&
this.state.resizingSides?.left === left &&
this.state.resizingSides?.top === top &&
this.state.resizingSides?.right === right &&
this.state.cursor === newCursor &&
this.state.couldResize === Boolean(newCursor)) {
return;
}
const resizingSides = {
bottom,
left,
right,
top,
};
const { onCanResize } = this.props;
if (onCanResize) {
onCanResize({});
}
this.setState({
couldResize: Boolean(newCursor),
cursor: newCursor,
resizingSides,
});
}
render() {
const { grow, height, left, movable, top, width, zIndex } = this.props;
const style = {
cursor: this.state.cursor,
zIndex: zIndex == null ? 'auto' : zIndex,
};
if (movable === true || top != null || left != null) {
if (grow === true) {
style.left = left || 0;
style.top = top || 0;
}
else {
style.transform = `translate3d(${left || 0}px, ${top || 0}px, 0)`;
}
}
if (grow === true) {
style.right = 0;
style.bottom = 0;
style.width = '100%';
style.height = '100%';
}
else {
style.width = width == null ? 'auto' : width;
style.height = height == null ? 'auto' : height;
}
if (this.props.style) {
Object.assign(style, this.props.style);
}
return (react_1.default.createElement(InteractiveContainer, { className: this.props.className, hidden: this.props.hidden, ref: this.setRef, onMouseDown: this.startAction, onMouseMove: this.onLocalMouseMove, onMouseLeave: this.onMouseLeave, onClick: this.onClick, style: style }, this.props.children));
}
}
exports.Interactive = Interactive;
Interactive.defaultProps = {
minHeight: 0,
minLeft: 0,
minTop: 0,
minWidth: 0,
};
//# sourceMappingURL=Interactive.js.map