From de9c029ba8e73ed18a0493a1d0d606ab0d491857 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Sat, 23 Dec 2023 11:14:48 +0800 Subject: [PATCH] ScreenTabs refactor (#182) * inital implementation * cleanup * ui jump fix * fix wrong first tab top right border radius * add comment * remove react.fragment. only do initial screens populate on componentDidMount, rename this.scrollIntoViewTimeout (add Id to distinguish from state.scrollIntoViewTimeout). * clean up some imports * use single tabRef --- src/app/workspace/screen/tab.tsx | 131 +++++++++++++++ src/app/workspace/screen/tabs.less | 2 +- src/app/workspace/screen/tabs.tsx | 252 +++++++++++------------------ 3 files changed, 228 insertions(+), 157 deletions(-) create mode 100644 src/app/workspace/screen/tab.tsx diff --git a/src/app/workspace/screen/tab.tsx b/src/app/workspace/screen/tab.tsx new file mode 100644 index 000000000..09f0c92d3 --- /dev/null +++ b/src/app/workspace/screen/tab.tsx @@ -0,0 +1,131 @@ +// Copyright 2023, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import * as mobxReact from "mobx-react"; +import * as mobx from "mobx"; +import { sprintf } from "sprintf-js"; +import { boundMethod } from "autobind-decorator"; +import { For } from "tsx-control-statements/components"; +import cn from "classnames"; +import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model"; +import { renderCmdText } from "../../common/common"; +import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; +import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg"; +import * as constants from "../../appconst"; +import { Reorder } from "framer-motion"; +import { MagicLayout } from "../../magiclayout"; + +@mobxReact.observer +class ScreenTab extends React.Component< + { screen: Screen; activeScreenId: string; index: number; onSwitchScreen: (screenId: string) => void }, + {} +> { + tabRef = React.createRef(); + dragEndTimeout = null; + scrollIntoViewTimeout = null; + + componentWillUnmount() { + if (this.scrollIntoViewTimeout) { + clearTimeout(this.dragEndTimeout); + } + } + + @boundMethod + handleDragEnd() { + if (this.dragEndTimeout) { + clearTimeout(this.dragEndTimeout); + } + + // Wait for the animation to complete + this.dragEndTimeout = setTimeout(() => { + const tabElement = this.tabRef.current; + if (tabElement) { + const finalTabPosition = tabElement.offsetLeft; + + // Calculate the new index based on the final position + const newIndex = Math.floor(finalTabPosition / MagicLayout.TabWidth); + + GlobalCommandRunner.screenReorder(this.props.screen.screenId, `${newIndex + 1}`); + } + }, 100); + } + + @boundMethod + openScreenSettings(e: any, screen: Screen): void { + e.preventDefault(); + e.stopPropagation(); + mobx.action(() => { + GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId }); + })(); + GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS); + } + + renderTabIcon = (screen: Screen): React.ReactNode => { + const tabIcon = screen.getTabIcon(); + if (tabIcon === "default" || tabIcon === "square") { + return ( +
+ +
+ ); + } + return ( +
+ +
+ ); + }; + + render() { + let { screen, activeScreenId, index, onSwitchScreen } = this.props; + + let tabIndex = null; + if (index + 1 <= 9) { + tabIndex =
{renderCmdText(String(index + 1))}
; + } + let settings = ( +
this.openScreenSettings(e, screen)} title="Actions" className="tab-gear"> + +
+ ); + let archived = screen.archived.get() ? ( + + ) : null; + + let webShared = screen.isWebShared() ? ( + + ) : null; + + return ( + onSwitchScreen(screen.screenId)} + onContextMenu={(event) => this.openScreenSettings(event, screen)} + onDragEnd={this.handleDragEnd} + > + {this.renderTabIcon(screen)} +
+ {archived} + {webShared} + {screen.name.get()} +
+ {tabIndex} + {settings} +
+ ); + } +} + +export { ScreenTab }; diff --git a/src/app/workspace/screen/tabs.less b/src/app/workspace/screen/tabs.less index 1cf09a557..20dfb0630 100644 --- a/src/app/workspace/screen/tabs.less +++ b/src/app/workspace/screen/tabs.less @@ -5,7 +5,7 @@ font-size: 12.5px; &:first-child { - border-radius: 12px 0px 0px 0px; + border-radius: 8px 0px 0px 0px; } &.color-green, diff --git a/src/app/workspace/screen/tabs.tsx b/src/app/workspace/screen/tabs.tsx index ce37091c8..201226a7a 100644 --- a/src/app/workspace/screen/tabs.tsx +++ b/src/app/workspace/screen/tabs.tsx @@ -7,56 +7,122 @@ import * as mobx from "mobx"; import { sprintf } from "sprintf-js"; import { boundMethod } from "autobind-decorator"; import { For } from "tsx-control-statements/components"; -import cn from "classnames"; -import dayjs from "dayjs"; -import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel, GlobalCommandRunner, Session, Screen } from "../../../model/model"; -import { renderCmdText } from "../../common/common"; -import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; -import { ReactComponent as ActionsIcon } from "../../assets/icons/tab/actions.svg"; import { ReactComponent as AddIcon } from "../../assets/icons/add.svg"; import * as constants from "../../appconst"; import { Reorder } from "framer-motion"; -import { MagicLayout } from "../../magiclayout"; +import { ScreenTab } from "./tab"; -import "../workspace.less"; import "./tabs.less"; -dayjs.extend(localizedFormat); - @mobxReact.observer -class ScreenTabs extends React.Component<{ session: Session }, { showingScreens: Screen[] }> { +class ScreenTabs extends React.Component< + { session: Session }, + { showingScreens: Screen[]; scrollIntoViewTimeout: number } +> { tabsRef: React.RefObject = React.createRef(); tabRefs: { [screenId: string]: React.RefObject } = {}; lastActiveScreenId: string = null; dragEndTimeout = null; - scrollIntoViewTimeout = null; + scrollIntoViewTimeoutId = null; deltaYHistory = []; + disposeScreensReaction = null; constructor(props: any) { super(props); this.state = { showingScreens: [], + scrollIntoViewTimeout: 0, }; } - @mobx.computed - get activeScreenId(): string { + componentDidMount(): void { + // handle initial scrollIntoView + this.componentDidUpdate(); + + // populate showingScreens state + this.setState({ showingScreens: this.getScreens() }); + // Update showingScreens state when the screens change + this.disposeScreensReaction = mobx.reaction( + () => this.getScreens(), + (screens) => { + // Different timeout for when screens are added vs removed + let timeout = 100; + if (screens.length < this.state.showingScreens.length) { + timeout = 400; + } + this.setState({ showingScreens: screens, scrollIntoViewTimeout: timeout }); + } + ); + + // Add the wheel event listener to the tabsRef + if (this.tabsRef.current) { + this.tabsRef.current.addEventListener("wheel", this.handleWheel, { passive: false }); + } + } + + componentWillUnmount() { + if (this.dragEndTimeout) { + clearTimeout(this.dragEndTimeout); + } + + if (this.disposeScreensReaction) { + this.disposeScreensReaction(); // Clean up the reaction + } + } + + componentDidUpdate(): void { + // Scroll the active screen into view + let activeScreenId = this.getActiveScreenId(); + if (activeScreenId !== this.lastActiveScreenId) { + if (this.scrollIntoViewTimeoutId) { + clearTimeout(this.scrollIntoViewTimeoutId); + } + this.lastActiveScreenId = activeScreenId; + this.scrollIntoViewTimeoutId = setTimeout(() => { + if (!this.tabsRef.current) { + return; + } + let tabElem = this.tabsRef.current.querySelector( + sprintf('.screen-tab[data-screenid="%s"]', activeScreenId) + ); + if (!tabElem) { + return; + } + tabElem.scrollIntoView(); + }, this.state.scrollIntoViewTimeout); + } + } + + @boundMethod + getActiveScreenId(): string { let { session } = this.props; if (session) { return session.activeScreenId.get(); } + return null; } @mobx.computed - get screens(): Screen[] { - if (this.activeScreenId) { - let screens = GlobalModel.getSessionScreens(this.props.session.sessionId); - let showingScreens = screens - .filter((screen) => !screen.archived.get() || this.activeScreenId === screen.screenId) - .sort((a, b) => a.screenIdx.get() - b.screenIdx.get()); - return showingScreens; + @boundMethod + getScreens(): Screen[] { + let activeScreenId = this.getActiveScreenId(); + if (!activeScreenId) { + return []; } + + let screens = GlobalModel.getSessionScreens(this.props.session.sessionId); + let showingScreens = []; + + for (const screen of screens) { + if (!screen.archived.get() || activeScreenId === screen.screenId) { + showingScreens.push(screen); + } + } + + showingScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get()); + + return showingScreens; } @boundMethod @@ -103,68 +169,6 @@ class ScreenTabs extends React.Component<{ session: Session }, { showingScreens: // For touchpad events, do nothing and let the browser handle it } - componentDidMount(): void { - this.componentDidUpdate(); - - // Add the wheel event listener to the tabsRef - if (this.tabsRef.current) { - this.tabsRef.current.addEventListener("wheel", this.handleWheel, { passive: false }); - } - } - - componentWillUnmount() { - if (this.dragEndTimeout) { - clearTimeout(this.dragEndTimeout); - clearTimeout(this.scrollIntoViewTimeout); - } - } - - componentDidUpdate(): void { - // Scroll the active screen into view - let { session } = this.props; - let activeScreenId = session.activeScreenId.get(); - if (activeScreenId !== this.lastActiveScreenId) { - if (this.scrollIntoViewTimeout) { - clearTimeout(this.scrollIntoViewTimeout); - } - this.scrollIntoViewTimeout = setTimeout(() => { - if (this.tabsRef.current) { - let tabElem = this.tabsRef.current.querySelector( - sprintf('.screen-tab[data-screenid="%s"]', activeScreenId) - ); - if (tabElem) { - tabElem.scrollIntoView(); - } - } - this.lastActiveScreenId = activeScreenId; - }, 100); - } - - // Set the showingScreens state if it's not set or if the number of screens has changed. - // Individual screen update are handled automatically by mobx. - if (this.screens && this.state.showingScreens.length !== this.screens.length) { - this.setState({ showingScreens: this.screens }); - } - } - - @boundMethod - handleDragEnd(screenId) { - if (this.dragEndTimeout) { - clearTimeout(this.dragEndTimeout); - } - - // Wait for the animation to complete - this.dragEndTimeout = setTimeout(() => { - const tabElement = this.tabRefs[screenId].current; - const finalTabPosition = tabElement.offsetLeft; - - // Calculate the new index based on the final position - const newIndex = Math.floor(finalTabPosition / MagicLayout.TabWidth); - - GlobalCommandRunner.screenReorder(screenId, `${newIndex + 1}`); - }, 100); - } - @boundMethod openScreenSettings(e: any, screen: Screen): void { e.preventDefault(); @@ -175,75 +179,6 @@ class ScreenTabs extends React.Component<{ session: Session }, { showingScreens: GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS); } - renderTabIcon = (screen: Screen): React.ReactNode => { - const tabIcon = screen.getTabIcon(); - if (tabIcon === "default" || tabIcon === "square") { - return ( -
- -
- ); - } - return ( -
- -
- ); - }; - - renderTab(screen: Screen, activeScreenId: string, index: number): JSX.Element { - let tabIndex = null; - if (index + 1 <= 9) { - tabIndex =
{renderCmdText(String(index + 1))}
; - } - let settings = ( -
this.openScreenSettings(e, screen)} title="Actions" className="tab-gear"> - -
- ); - let archived = screen.archived.get() ? ( - - ) : null; - - let webShared = screen.isWebShared() ? ( - - ) : null; - - // Create a ref for the tab if it doesn't exist - if (!this.tabRefs[screen.screenId]) { - this.tabRefs[screen.screenId] = React.createRef(); - } - - return ( - this.handleSwitchScreen(screen.screenId)} - onContextMenu={(event) => this.openScreenSettings(event, screen)} - onDragEnd={() => this.handleDragEnd(screen.screenId)} - > - {this.renderTabIcon(screen)} -
- {archived} - {webShared} - {screen.name.get()} -
- {tabIndex} - {settings} -
- ); - } - render() { let { showingScreens } = this.state; let { session } = this.props; @@ -252,7 +187,8 @@ class ScreenTabs extends React.Component<{ session: Session }, { showingScreens: } let screen: Screen | null = null; let index = 0; - let activeScreenId = session.activeScreenId.get(); + let activeScreenId = this.getActiveScreenId(); + return (
- - {this.renderTab(screen, activeScreenId, index)} - +