UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

537 lines 18.9 kB
"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