From 18dd2a3ebe6f1fe52a15a8eb01ed00eab14bf0d2 Mon Sep 17 00:00:00 2001 From: wil Date: Thu, 31 Oct 2024 22:49:13 -0400 Subject: [PATCH] feature/ toolbar node 3 (#62) * fix group selects, better the node transfomer as well * refactor for better code organization --- src/KonvaApp/Engine.ts | 4 +- src/KonvaApp/Nodes/BaseNode.ts | 8 +- src/KonvaApp/Nodes/NetworkedNode.ts | 4 +- src/KonvaApp/Nodes/VideoNode/VideoNode.ts | 6 +- src/KonvaApp/NodesManagers/NodeTransformer.ts | 12 +- .../NodesManagers/SelectionManager.ts | 270 ++++++------------ .../NodesManagers/ToolbarNodeUtilities.ts | 193 +++++++++++++ .../ContextualToolbarNode.tsx | 22 +- 8 files changed, 317 insertions(+), 202 deletions(-) create mode 100644 src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts diff --git a/src/KonvaApp/Engine.ts b/src/KonvaApp/Engine.ts index 6fdb4417fd..ac1f0d1bf3 100644 --- a/src/KonvaApp/Engine.ts +++ b/src/KonvaApp/Engine.ts @@ -395,7 +395,7 @@ export class Engine { await node.startSegmentation(); this.nodeIsolator.enterIsolation(node); node.videoSegmentationMode(true); - this.selectionManager.updateContextComponents(node); + this.selectionManager.updateContextComponents(); uiAccess.loadingBar.update({ progress: 0, message: "Start Adding Extraction Points To the Video", @@ -425,7 +425,7 @@ export class Engine { this.enableSelectorSquare(); this.segmentationButtonCanBePressed = true; node.unlock(); - this.selectionManager.updateContextComponents(node); + this.selectionManager.updateContextComponents(); this.selectionManager.enable(); // to close off the session. } else { diff --git a/src/KonvaApp/Nodes/BaseNode.ts b/src/KonvaApp/Nodes/BaseNode.ts index b38e75bc9b..ed1ea2f983 100644 --- a/src/KonvaApp/Nodes/BaseNode.ts +++ b/src/KonvaApp/Nodes/BaseNode.ts @@ -109,7 +109,7 @@ export abstract class BaseNode { this.kNode.setDraggable(false); this.selectionManagerRef.updateNodeTransformer(); if (this.isKEventRef()) { - this.selectionManagerRef.updateContextComponents(this); + this.selectionManagerRef.updateContextComponents(); } } public unlock() { @@ -117,7 +117,7 @@ export abstract class BaseNode { this.kNode.setDraggable(true); this.selectionManagerRef.updateNodeTransformer(); if (this.isKEventRef()) { - this.selectionManagerRef.updateContextComponents(this); + this.selectionManagerRef.updateContextComponents(); } } @@ -181,7 +181,7 @@ export abstract class BaseNode { this.kNode.on("dragend", () => { // console.log("Drag End", this.kNode._id); - this.selectionManagerRef.dragEnd(this); + this.selectionManagerRef.dragEnd(); }); } @@ -204,7 +204,7 @@ export abstract class BaseNode { }); this.kNode.on("transformend", (event) => { console.log("transformend", event.target._id); - this.selectionManagerRef.transformEnd(this); + this.selectionManagerRef.transformEnd(); }); } public removeListenToBaseKNodeTransformations() { diff --git a/src/KonvaApp/Nodes/NetworkedNode.ts b/src/KonvaApp/Nodes/NetworkedNode.ts index df72d03470..8c008b097b 100644 --- a/src/KonvaApp/Nodes/NetworkedNode.ts +++ b/src/KonvaApp/Nodes/NetworkedNode.ts @@ -79,7 +79,7 @@ export abstract class NetworkedNode extends BaseNode { protected setStatus(newStatus: UploadStatus, message?: string) { this.mediaFileStatus = newStatus; this.errorMessage = message; - this.selectionManagerRef.updateContextComponents(this); + this.selectionManagerRef.updateContextComponents(); } protected setProgress( newProgress: number, @@ -99,6 +99,6 @@ export abstract class NetworkedNode extends BaseNode { if (this._progress === 100) { this.didFinishLoading = true; } - this.selectionManagerRef.updateContextComponents(this); + this.selectionManagerRef.updateContextComponents(); } } diff --git a/src/KonvaApp/Nodes/VideoNode/VideoNode.ts b/src/KonvaApp/Nodes/VideoNode/VideoNode.ts index f77363ad35..f1759cfb8d 100644 --- a/src/KonvaApp/Nodes/VideoNode/VideoNode.ts +++ b/src/KonvaApp/Nodes/VideoNode/VideoNode.ts @@ -477,14 +477,14 @@ export class VideoNode extends NetworkedNode { this.videoComponent.pause(); this.videoComponent.currentTime = 0; this.setProgress(25, { message: "Loading Video Extractor..." }); - this.selectionManagerRef.updateContextComponents(this); - this.selectionManagerRef.showContextComponents(this); + this.selectionManagerRef.updateContextComponents(); + this.selectionManagerRef.showContextComponents(); console.log("loadingbar should show"); const blob = await NodeUtilities.urlToBlob(this.mediaFileUrl); this.setProgress(75, { message: "Loading Video Extractor..." }); - this.selectionManagerRef.updateContextComponents(this); + this.selectionManagerRef.updateContextComponents(); this.segmentationSession = await this.videoSegmentationAPI.createSession(blob); diff --git a/src/KonvaApp/NodesManagers/NodeTransformer.ts b/src/KonvaApp/NodesManagers/NodeTransformer.ts index c5a1e37438..0d105105b1 100644 --- a/src/KonvaApp/NodesManagers/NodeTransformer.ts +++ b/src/KonvaApp/NodesManagers/NodeTransformer.ts @@ -34,7 +34,7 @@ export class NodeTransformer { public getKonvaNode() { return this.kTransformer; } - public enable({ selectedNodes }: { selectedNodes: Set }) { + public addNodes({ selectedNodes }: { selectedNodes: Set }) { const kNodesArray = Array.from(selectedNodes).reduce((acc, node) => { acc.push(node.kNode); return acc; @@ -48,4 +48,14 @@ export class NodeTransformer { public clear() { this.kTransformer.nodes([]); } + public enable() { + this.kTransformer.rotateEnabled(true); + this.kTransformer.resizeEnabled(true); + this.kTransformer.draggable(true); + } + public disable() { + this.kTransformer.rotateEnabled(false); + this.kTransformer.resizeEnabled(false); + this.kTransformer.draggable(false); + } } diff --git a/src/KonvaApp/NodesManagers/SelectionManager.ts b/src/KonvaApp/NodesManagers/SelectionManager.ts index 9ebdcaf9e4..dd7f00aaef 100644 --- a/src/KonvaApp/NodesManagers/SelectionManager.ts +++ b/src/KonvaApp/NodesManagers/SelectionManager.ts @@ -5,8 +5,13 @@ import { uiAccess, uiEvents } from "~/signals"; import { NetworkedNode } from "../Nodes/NetworkedNode"; import { LoadingBarStatus } from "~/components/ui"; import { ImageNode, TextNode, VideoNode } from "../Nodes"; -import { ToolbarNodeButtonNames } from "~/components/features/ToolbarNode"; -import { ContextualToolbarProps } from "~/signals/uiAccess/toolbarNode"; +import { + calculateContextualsPosition, + getImageNodeButtonStates, + getMultiSelectButtonStates, + getTextNodeButtonStates, + getVideoNodeButtonStates, +} from "./ToolbarNodeUtilities"; export enum SelectionManagerEvents { NODES_TRANSLATIONS = "nodestranslation", @@ -44,6 +49,7 @@ export class SelectionManager { }) { this.firstSelectedNode = undefined; this.selectedNodes = new Set(); + this.initialPositions = new Map(); this.initialTransformations = new Map(); this.eventTarget = new EventTarget(); @@ -76,7 +82,11 @@ export class SelectionManager { if ( this.disabled || // no-op when disabled this.selectedNodes.has(node) || // if the node is already selected - (this.selectedNodes.size > 0 && node.isLocked()) // if in multiselect but picked a locked item + (this.selectedNodes.size > 0 && node.isLocked()) || + // if in multiselect but picked a locked item + (this.selectedNodes.size > 0 && + this.selectedNodes.values().next().value?.isLocked()) + // if the first node picked is a locked item ) { return false; } @@ -84,11 +94,12 @@ export class SelectionManager { node.highlight(); if (this.selectedNodes.size === 0) { node.setIsKEventRef(true); - this.updateContextComponents(node); - this.showContextComponents(node); } this.selectedNodes.add(node); this.updateNodeTransformer(); + this.updateContextComponents(); + this.showContextComponents(); + if (!doNotDraw) { this.mediaLayerRef.batchDraw(); } @@ -106,6 +117,8 @@ export class SelectionManager { } this.selectedNodes.delete(node); this.updateNodeTransformer(); + this.updateContextComponents(); + this.showContextComponents(); if (!doNotDraw) { this.mediaLayerRef.batchDraw(); } @@ -129,8 +142,8 @@ export class SelectionManager { this.mediaLayerRef.batchDraw(); } - public onToggleLock(node: MediaNode) { - this.updateContextComponents(node); + public onToggleLock() { + this.updateContextComponents(); } public isDragging() { return this._isDragging; @@ -150,7 +163,7 @@ export class SelectionManager { }); } - public dragEnd(refNode: MediaNode) { + public dragEnd() { // release the dragging state for mouseevents in nodes this._isDragging = false; // track and map the final positions @@ -173,8 +186,8 @@ export class SelectionManager { ); // done, clear data and show menu this.initialPositions.clear(); - this.updateContextComponents(refNode); - this.showContextComponents(refNode); + this.updateContextComponents(); + this.showContextComponents(); } private getKNodeTransformation(kNode: Konva.Node) { return { @@ -202,7 +215,7 @@ export class SelectionManager { }); this.hideContextComponents(); } - public transformEnd(refNode: MediaNode) { + public transformEnd() { // track and map the final positions const finalTransformations = new Map(); this.selectedNodes.forEach((selectedNode) => { @@ -228,43 +241,18 @@ export class SelectionManager { }, ), ); - this.updateContextComponents(refNode); - this.showContextComponents(refNode); + this.updateContextComponents(); + this.showContextComponents(); } public updateNodeTransformer() { - const transformableNodes = new Set( - Array.from(this.selectedNodes).filter((node) => { - return !node.isLocked(); - }), - ); - this.nodeTransformerRef.enable({ selectedNodes: transformableNodes }); - } - - protected calculateContextualsPosition(kNode: Konva.Node) { - const w = kNode.getSize().width * kNode.scaleX(); - const h = kNode.getSize().height * kNode.scaleY(); - const x0 = kNode.getPosition().x; - const y0 = kNode.getPosition().y; - - const d = kNode.getAbsoluteRotation(); - const r = d >= 0 ? (d * Math.PI) / 180 : ((360 + d) * Math.PI) / 180; - - let px: number, py: number; - if (r < Math.PI / 2) { - px = x0 + (h * Math.sin(r) + w * Math.cos(r)) / 2 - h * Math.sin(r); - py = y0 + h * Math.cos(r) + w * Math.cos(Math.PI / 2 - r); - } else if (r < Math.PI) { - px = x0 - (h * Math.sin(r) - w * Math.cos(r)) / 2; - py = y0 + w * Math.cos(Math.PI / 2 - r); - } else if (r < (Math.PI * 3) / 2) { - px = x0 + h * Math.cos(r) - (h * Math.cos(r) + w * Math.sin(r)) / 2; - py = y0; + const lockFlag = this.selectedNodes.values().next().value?.isLocked(); + if (lockFlag) { + this.nodeTransformerRef.disable(); } else { - px = x0 + (-h * Math.sin(r) + w * Math.cos(r)) / 2; - py = y0 + h * Math.cos(r); + this.nodeTransformerRef.enable(); } - return { x: px, y: py }; + this.nodeTransformerRef.addNodes({ selectedNodes: this.selectedNodes }); } public hideContextComponents() { @@ -272,42 +260,65 @@ export class SelectionManager { uiAccess.loadingBar.hide(); } - public showContextComponents(node: MediaNode) { + public showContextComponents() { const showOrUpdate = uiAccess.toolbarNode.isShowing() ? uiAccess.toolbarNode.update : uiAccess.toolbarNode.show; + const node = this.selectedNodes.values().next().value; + if (node === undefined) { + this.hideContextComponents(); + return; // no node + } + if (this.selectedNodes.size === 1) { + if (node instanceof ImageNode) { + showOrUpdate({ + locked: node.isLocked(), + buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }), + }); + } + if (node instanceof TextNode) { + showOrUpdate({ + locked: node.isLocked(), + buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }), + }); + } + if (node instanceof VideoNode) { + showOrUpdate({ + locked: node.isLocked(), + buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }), + }); + } - if (node instanceof ImageNode) { - showOrUpdate({ - locked: node.isLocked(), - buttonStates: getImageNodeButtonStates({ locked: node.isLocked() }), - }); - } - if (node instanceof TextNode) { - showOrUpdate({ - locked: node.isLocked(), - buttonStates: getTextNodeButtonStates({ locked: node.isLocked() }), - }); - } - if (node instanceof VideoNode) { - showOrUpdate({ - locked: node.isLocked(), - buttonStates: getVideoNodeButtonStates({ locked: node.isLocked() }), - }); - } - - // show loading bar is the node is noding - if (node instanceof NetworkedNode) { - if (!node.didFinishLoading && !uiAccess.loadingBar.isShowing()) { - // console.log("show loading bar"); - uiAccess.loadingBar.show(); + // show loading bar is the node is noding + if (node instanceof NetworkedNode) { + if (!node.didFinishLoading && !uiAccess.loadingBar.isShowing()) { + // console.log("show loading bar"); + uiAccess.loadingBar.show(); + } + } + } else { + if (node) { + showOrUpdate({ + locked: node.isLocked(), + buttonStates: getMultiSelectButtonStates({ locked: node.isLocked() }), + }); } } } - public updateContextComponents(node: MediaNode) { - const coord = this.calculateContextualsPosition(node.kNode); + public updateContextComponents() { + // use first node as reference + const node = this.selectedNodes.values().next().value; + if (node === undefined) { + this.hideContextComponents(); + return; // no nodes + } + // console.log("SelectionManager > updateContextComponents for node:", node); + const coord = calculateContextualsPosition( + this.nodeTransformerRef.getKonvaNode(), + ); + if (node instanceof VideoNode) { if (node.isSegmentationMode && !uiAccess.toolbarNode.isLockDisabled()) { uiAccess.toolbarNode.disableLock(); @@ -316,6 +327,7 @@ export class SelectionManager { } } if (node.isLocked() !== uiAccess.toolbarNode.isLocked()) { + // console.log("setting lock"); uiAccess.toolbarNode.setLocked(node.isLocked()); } uiAccess.toolbarNode.setPosition({ @@ -344,121 +356,3 @@ export class SelectionManager { } } } - -function getImageNodeButtonStates( - props: { locked?: boolean | "unknown" } = {}, -) { - const ButtonNames = ToolbarNodeButtonNames; - return Object.values(ButtonNames).reduce( - (buttonStates, buttonName) => { - // transform button depends on locked state - if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { - buttonStates.TRANSFORM = { - disabled: props.locked === "unknown" || props.locked === true, - hidden: false, - active: true, - }; - return buttonStates; - } - - // hidden buttons - if ( - buttonName === ButtonNames.AI_STYLIZE || - buttonName === ButtonNames.SEGMENTATION || - buttonName === ButtonNames.DOWNLOAD || - buttonName === ButtonNames.CHROMA - ) { - buttonStates[buttonName] = { - disabled: true, - hidden: true, - active: false, - }; - return buttonStates; - } - - // all other buttons - buttonStates[buttonName] = { - disabled: false, - hidden: false, - active: false, - }; - return buttonStates; - }, - {} as ContextualToolbarProps["buttonStates"], - ); -} -function getTextNodeButtonStates(props: { locked?: boolean | "unknown" } = {}) { - const ButtonNames = ToolbarNodeButtonNames; - return Object.values(ButtonNames).reduce( - (buttonStates, buttonName) => { - // transform button depends on locked state - if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { - buttonStates[buttonName] = { - disabled: props.locked === "unknown" || props.locked === true, - hidden: false, - active: true, - }; - return buttonStates; - } - - // hidden buttons - if ( - buttonName === ButtonNames.AI_STYLIZE || - buttonName === ButtonNames.SEGMENTATION || - buttonName === ButtonNames.DOWNLOAD || - buttonName === ButtonNames.CHROMA - ) { - buttonStates[buttonName] = { - disabled: true, - hidden: true, - active: false, - }; - return buttonStates; - } - - // all other buttons - buttonStates[buttonName] = { - disabled: false, - hidden: false, - active: false, - }; - return buttonStates; - }, - {} as ContextualToolbarProps["buttonStates"], - ); -} -function getVideoNodeButtonStates( - props: { locked?: boolean | "unknown" } = {}, -) { - const ButtonNames = ToolbarNodeButtonNames; - return Object.values(ButtonNames).reduce( - (buttonStates, buttonName) => { - // transform button depends on locked state - if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { - buttonStates.TRANSFORM = { - disabled: props.locked === "unknown" || props.locked === true, - hidden: false, - active: true, - }; - return buttonStates; - } - // soon to come feature is disabled - if (buttonName === ButtonNames.AI_STYLIZE) { - buttonStates[buttonName] = { - disabled: true, - hidden: true, - active: false, - }; - return buttonStates; - } - // all other buttons - buttonStates[buttonName] = { - disabled: false, - hidden: false, - active: false, - }; - return buttonStates; - }, - {} as ContextualToolbarProps["buttonStates"], - ); -} diff --git a/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts b/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts new file mode 100644 index 0000000000..8a33cb2611 --- /dev/null +++ b/src/KonvaApp/NodesManagers/ToolbarNodeUtilities.ts @@ -0,0 +1,193 @@ +import Konva from "konva"; +import { ToolbarNodeButtonNames } from "~/components/features/ToolbarNode"; +import { ContextualToolbarProps } from "~/signals/uiAccess/toolbarNode"; +export function calculateContextualsPosition(kNode: Konva.Transformer) { + const w = kNode.getSize().width * kNode.scaleX(); + const h = kNode.getSize().height * kNode.scaleY(); + const x0 = kNode.getPosition().x; + const y0 = kNode.getPosition().y; + + const d = kNode.getAbsoluteRotation(); + const r = d >= 0 ? (d * Math.PI) / 180 : ((360 + d) * Math.PI) / 180; + + let px: number, py: number; + if (r < Math.PI / 2) { + //top right quadrants + px = x0 + (h * Math.sin(r) + w * Math.cos(r)) / 2 - h * Math.sin(r); + py = y0 + h * Math.cos(r) + w * Math.cos(Math.PI / 2 - r); + } else if (r < Math.PI) { + //lower right quadrants + px = x0 - (h * Math.sin(r) - w * Math.cos(r)) / 2; + py = y0 + w * Math.cos(Math.PI / 2 - r); + } else if (r < (Math.PI * 3) / 2) { + //lower left quadrants + px = x0 + h * Math.cos(r) - (h * Math.cos(r) + w * Math.sin(r)) / 2; + py = y0; + } else { + //top left quadrants + px = x0 + (-h * Math.sin(r) + w * Math.cos(r)) / 2; + py = y0 + h * Math.cos(r); + } + return { x: px, y: py }; +} +export function getImageNodeButtonStates( + props: { locked?: boolean | "unknown" } = {}, +) { + const ButtonNames = ToolbarNodeButtonNames; + return Object.values(ButtonNames).reduce( + (buttonStates, buttonName) => { + // transform button depends on locked state + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates.TRANSFORM = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + return buttonStates; + } + + // hidden buttons + if ( + buttonName === ButtonNames.AI_STYLIZE || + buttonName === ButtonNames.SEGMENTATION || + buttonName === ButtonNames.DOWNLOAD || + buttonName === ButtonNames.CHROMA + ) { + buttonStates[buttonName] = { + disabled: true, + hidden: true, + active: false, + }; + return buttonStates; + } + + // all other buttons + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, + {} as ContextualToolbarProps["buttonStates"], + ); +} +export function getTextNodeButtonStates( + props: { locked?: boolean | "unknown" } = {}, +) { + const ButtonNames = ToolbarNodeButtonNames; + return Object.values(ButtonNames).reduce( + (buttonStates, buttonName) => { + // transform button depends on locked state + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates[buttonName] = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + return buttonStates; + } + + // hidden buttons + if ( + buttonName === ButtonNames.AI_STYLIZE || + buttonName === ButtonNames.SEGMENTATION || + buttonName === ButtonNames.DOWNLOAD || + buttonName === ButtonNames.CHROMA + ) { + buttonStates[buttonName] = { + disabled: true, + hidden: true, + active: false, + }; + return buttonStates; + } + + // all other buttons + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, + {} as ContextualToolbarProps["buttonStates"], + ); +} +export function getVideoNodeButtonStates( + props: { locked?: boolean | "unknown" } = {}, +) { + const ButtonNames = ToolbarNodeButtonNames; + return Object.values(ButtonNames).reduce( + (buttonStates, buttonName) => { + // transform button depends on locked state + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates.TRANSFORM = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + return buttonStates; + } + // soon to come feature is disabled + if (buttonName === ButtonNames.AI_STYLIZE) { + buttonStates[buttonName] = { + disabled: true, + hidden: true, + active: false, + }; + return buttonStates; + } + // all other buttons + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, + {} as ContextualToolbarProps["buttonStates"], + ); +} +export function getMultiSelectButtonStates( + props: { locked?: boolean | "unknown" } = {}, +) { + const ButtonNames = ToolbarNodeButtonNames; + return Object.values(ButtonNames).reduce( + (buttonStates, buttonName) => { + // transform button depends on locked state + if (props.locked !== undefined && buttonName === ButtonNames.TRANSFORM) { + buttonStates[buttonName] = { + disabled: props.locked === "unknown" || props.locked === true, + hidden: false, + active: true, + }; + return buttonStates; + } + + // hidden buttons + if ( + buttonName === ButtonNames.AI_STYLIZE || + buttonName === ButtonNames.SEGMENTATION || + buttonName === ButtonNames.DOWNLOAD || + buttonName === ButtonNames.CHROMA + ) { + buttonStates[buttonName] = { + disabled: true, + hidden: true, + active: false, + }; + return buttonStates; + } + + // all other buttons + buttonStates[buttonName] = { + disabled: false, + hidden: false, + active: false, + }; + return buttonStates; + }, + {} as ContextualToolbarProps["buttonStates"], + ); +} diff --git a/src/KonvaRootComponent/ContextualToolbarNode.tsx b/src/KonvaRootComponent/ContextualToolbarNode.tsx index 44b8ec93cc..5a362ade64 100644 --- a/src/KonvaRootComponent/ContextualToolbarNode.tsx +++ b/src/KonvaRootComponent/ContextualToolbarNode.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler } from "react"; +import { MouseEventHandler, useState, useEffect } from "react"; import { Transition } from "@headlessui/react"; import { twMerge } from "tailwind-merge"; @@ -10,6 +10,10 @@ import { ToolbarNodeButtonNames } from "~/components/features/ToolbarNode/enums" import { transitionTimingStyles } from "~/components/styles"; export const ContextualToolbarNode = () => { + const [winSize, setWinSize] = useState<{ width: number; height: number }>({ + width: window.innerWidth, + height: window.innerHeight, + }); const { isShowing, position, ...rest } = uiAccess.toolbarNode.signal.value; const buttonsProps = Object.values(ToolbarNodeButtonNames).reduce( (acc, buttonName) => { @@ -33,6 +37,20 @@ export const ContextualToolbarNode = () => { }; }, ); + useEffect(() => { + function resizeHandler() { + setWinSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + window.addEventListener("resize", resizeHandler); + return () => { + window.removeEventListener("resize", resizeHandler); + }; + }, []); + + const boundedTopPosition = Math.min(position.y, winSize.height - 164); return ( { "fixed -translate-x-1/2 translate-y-5 data-[closed]:opacity-0", )} style={{ - top: position.y, + top: boundedTopPosition, left: position.x, }} >