diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/MenuButton.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/MenuButton.tsx index 36cac875b9..113dcc839f 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/MenuButton.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/MenuButton.tsx @@ -7,7 +7,6 @@ import Text from "@reearth/beta/components/Text"; import { styled, metricsSizes, mask } from "@reearth/services/theme"; import type { Camera, FlyToDestination, Theme } from "../../types"; -import { Visible } from "../../useVisible"; export type Button = { id: string; @@ -19,7 +18,7 @@ export type Button = { buttonColor?: string; buttonBgcolor?: string; buttonCamera?: Camera; - visible: Visible; + visible?: "always" | "desktop" | "mobile"; }; export type MenuItem = { diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/index.tsx index d7a1105f04..b7b7c483c4 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Button/index.tsx @@ -1,7 +1,6 @@ import { styled } from "@reearth/services/theme"; import type { ComponentProps as WidgetProps } from "../.."; -import { useVisible } from "../../useVisible"; import MenuButton, { Button as ButtonType, MenuItem as MenuItemType } from "./MenuButton"; @@ -13,22 +12,10 @@ export type Property = { menu?: MenuItem[]; }; -const Menu = ({ - widget, - theme, - isMobile, - onVisibilityChange, - context: { onFlyTo } = {}, -}: Props): JSX.Element | null => { +const Menu = ({ widget, theme, context: { onFlyTo } = {} }: Props): JSX.Element | null => { const { default: button, menu: menuItems } = widget.property ?? {}; - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); - return visible ? ( + return ( - ) : null; + ); }; const Wrapper = styled.div` diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/hooks.ts index 0ff0bbf0df..95b3f1603b 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/hooks.ts @@ -1,21 +1,17 @@ import { useState, useEffect, useCallback, useRef } from "react"; import type { Camera, FlyToDestination, Widget } from "../../types"; -import { useVisible } from "../../useVisible"; import { degreeToRadian, radianToDegree } from "./UI"; export default function ({ camera, initialCamera, - widget, - isMobile, onZoomIn, onZoomOut, onCameraOrbit, onCameraRotateRight, onFlyTo, - onVisibilityChange, }: { camera?: Camera; initialCamera?: Camera; @@ -26,18 +22,11 @@ export default function ({ onCameraOrbit?: (orbit: number) => void; onCameraRotateRight?: (radian: number) => void; onFlyTo?: (target: string | FlyToDestination, options?: { duration?: number }) => void; - onVisibilityChange?: (id: string, visible: boolean) => void; }) { const [degree, setDegree] = useState(0); const [isHelpOpened, setIsHelpOpened] = useState(false); const orbitRadianRef = useRef(0); const isMovingOrbit = useRef(false); - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); const handleOnRotate = useCallback( (deg: number) => { @@ -101,7 +90,6 @@ export default function ({ return { degree, isHelpOpened, - visible, events: { onRotate: handleOnRotate, onStartOrbit: handleOnStartOrbit, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/index.tsx index 2bc88bc71e..0c4f8eb830 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Navigator/index.tsx @@ -1,22 +1,14 @@ import type { ComponentProps as WidgetProps } from "../.."; -import { Visible } from "../../useVisible"; import useHooks from "./hooks"; import NavigatorUI from "./UI"; -export type Props = WidgetProps; - -export type Property = { - default: { - visible: Visible; - }; -}; +export type Props = WidgetProps; const Navigator = ({ theme, widget, isMobile, - onVisibilityChange, context: { camera, initialCamera, @@ -27,7 +19,7 @@ const Navigator = ({ onZoomOut, } = {}, }: Props): JSX.Element | null => { - const { degree, visible, events } = useHooks({ + const { degree, events } = useHooks({ camera, initialCamera, widget, @@ -37,10 +29,9 @@ const Navigator = ({ onFlyTo, onZoomIn, onZoomOut, - onVisibilityChange, }); - return visible ? : null; + return ; }; export default Navigator; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts index 35e109d484..54635de4d8 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/hooks.ts @@ -5,7 +5,6 @@ import { TickEvent, TickEventCallback } from "@reearth/beta/lib/core/Map"; import { TimelineManagerRef } from "@reearth/beta/lib/core/Map/useTimelineManager"; import type { Widget } from "../../types"; -import { useVisible } from "../../useVisible"; const MAX_RANGE = 86400000; // a day const getOrNewDate = (d?: Date) => d ?? new Date(); @@ -31,7 +30,6 @@ export const useTimeline = ({ onTick, removeTickEventListener, onExtend, - onVisibilityChange, }: { widget: Widget; timelineManagerRef?: TimelineManagerRef; @@ -43,14 +41,7 @@ export const useTimeline = ({ onTick?: TickEvent; removeTickEventListener?: TickEvent; onExtend?: (id: string, extended: boolean | undefined) => void; - onVisibilityChange?: (id: string, v: boolean) => void; }) => { - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); const widgetId = widget.id; const [range, setRange] = useState(() => makeRange( @@ -225,7 +216,6 @@ export const useTimeline = ({ range, isOpened, currentTime, - visible, events: { onOpen: handleOnOpen, onClose: handleOnClose, diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx index c6c058f1af..5ddaea6ac0 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/builtin/Timeline/index.tsx @@ -2,24 +2,16 @@ import TimelineUI from "@reearth/beta/lib/core/Crust/Widgets/Widget/builtin/Time import { styled } from "@reearth/services/theme"; import type { ComponentProps as WidgetProps } from "../.."; -import { Visible } from "../../useVisible"; import { useTimeline } from "./hooks"; -export type Props = WidgetProps; - -export type Property = { - default: { - visible: Visible; - }; -}; +export type Props = WidgetProps; const Timeline = ({ widget, theme, isMobile, onExtend, - onVisibilityChange, context: { timelineManagerRef, onPlay, @@ -30,7 +22,7 @@ const Timeline = ({ removeTickEventListener, } = {}, }: Props): JSX.Element | null => { - const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ + const { isOpened, currentTime, range, speed, events } = useTimeline({ widget, timelineManagerRef, isMobile, @@ -41,10 +33,9 @@ const Timeline = ({ onTick, removeTickEventListener, onExtend, - onVisibilityChange, }); - return visible ? ( + return ( - ) : null; + ); }; const Widget = styled.div<{ diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx index b35ba1960e..65287776d8 100644 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/Widget/index.tsx @@ -32,7 +32,6 @@ export type Props = { isMobile?: boolean; context?: Context; onExtend?: (id: string, extended: boolean | undefined) => void; - onVisibilityChange?: (id: string, visible: boolean) => void; renderWidget?: (w: Widget) => ReactNode; }; diff --git a/web/src/beta/lib/core/Crust/Widgets/Widget/useVisible.ts b/web/src/beta/lib/core/Crust/Widgets/Widget/useVisible.ts deleted file mode 100644 index e6ae3e7555..0000000000 --- a/web/src/beta/lib/core/Crust/Widgets/Widget/useVisible.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useMemo } from "react"; - -export type Visible = "always" | "desktop" | "mobile"; - -export const useVisible = ({ - widgetId, - visible: defaultVisible, - isMobile, - onVisibilityChange, -}: { - widgetId: string | undefined; - visible: Visible | undefined; - isMobile: boolean | undefined; - onVisibilityChange: ((id: string, v: boolean) => void) | undefined; -}) => { - const visible = useMemo( - () => - !defaultVisible || - defaultVisible === "always" || - (defaultVisible === "desktop" && !isMobile) || - (defaultVisible === "mobile" && !!isMobile), - [defaultVisible, isMobile], - ); - - useEffect(() => { - if (widgetId) { - onVisibilityChange?.(widgetId, visible); - } - }, [widgetId, visible, onVisibilityChange]); - - return visible; -}; diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Area.tsx b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Area.tsx index bf8f8def87..89bcab775d 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Area.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Area.tsx @@ -14,6 +14,7 @@ import type { WidgetProps, InternalWidget, } from "./types"; +import { isInvisibleBuiltin } from "./utils"; export type WidgetAreaType = { zone: "inner" | "outer"; @@ -39,6 +40,7 @@ type Props = { centered?: boolean; built?: boolean; widgets?: InternalWidget[]; + isMobile?: boolean; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; // note that layoutConstraint will be always undefined in published pages layoutConstraint?: { [w in string]: WidgetLayoutConstraint }; @@ -58,6 +60,7 @@ export default function Area({ built, widgets, layoutConstraint, + isMobile, renderWidget, onWidgetAreaSelect, }: Props) { @@ -136,36 +139,39 @@ export default function Area({ alignItems: centered ? "center" : "unset", }} iconColor={area === "middle" ? "#4770FF" : "#E95518"}> - {widgets?.map((widget, i) => { - const constraint = - widget.pluginId && widget.extensionId - ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] - : undefined; - const extended = overriddenExtended?.[widget.id]; - const extendable2 = - (section === "center" && constraint?.extendable?.horizontally) || - (area === "middle" && constraint?.extendable?.vertically); - return ( - - {({ editing }) => - renderWidget?.({ - widget, - layout, - extended, - editing, - onExtend: handleExtend, - }) - } - - ); - })} + {widgets + ?.filter(widget => !isInvisibleBuiltin(widget, isMobile)) + .map((widget, i) => { + const constraint = + widget.pluginId && widget.extensionId + ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] + : undefined; + const extended = overriddenExtended?.[widget.id]; + const extendable2 = + (section === "center" && constraint?.extendable?.horizontally) || + (area === "middle" && constraint?.extendable?.vertically); + + return ( + + {({ editing }) => + renderWidget?.({ + widget, + layout, + extended, + editing, + onExtend: handleExtend, + }) + } + + ); + })} ) : null; } diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx index 40cf3a8b49..bfebb6a5c9 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx @@ -6,6 +6,7 @@ import Slide from "@reearth/beta/components/Slide"; import { styled } from "@reearth/services/theme"; import Area, { WidgetAreaType } from "./Area"; +import { WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint, Theme, WidgetProps } from "./types"; import { filterSections } from "./utils"; @@ -16,14 +17,13 @@ export type Props = { zoneName: "inner" | "outer"; theme?: Theme; built?: boolean; + isMobile?: boolean; layoutConstraint?: { [w: string]: WidgetLayoutConstraint }; invisibleWidgetIDs?: string[]; renderWidget?: (props: WidgetProps) => ReactNode; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; }; -const areas = ["top", "middle", "bottom"] as const; - export default function MobileZone({ selectedWidgetArea, zone, @@ -31,6 +31,7 @@ export default function MobileZone({ layoutConstraint, theme, built, + isMobile, children, invisibleWidgetIDs, renderWidget, @@ -53,7 +54,7 @@ export default function MobileZone({ 1}> {filteredSections.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} @@ -73,6 +74,7 @@ export default function MobileZone({ backgroundColor={zone?.[s]?.[a]?.background ?? "unset"} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx index 25b2a4cc68..413482bae0 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx @@ -2,6 +2,7 @@ import { ReactNode } from "react"; import { GridSection } from "react-align"; import Area, { WidgetAreaType } from "./Area"; +import { WAS_SECTIONS, WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint, WidgetProps } from "./types"; export type { WidgetAreaType }; @@ -13,29 +14,28 @@ export type Props = { zoneName: "inner" | "outer"; invisibleWidgetIDs?: string[]; layoutConstraint?: { [w: string]: WidgetLayoutConstraint }; + isMobile?: boolean; built?: boolean; renderWidget?: (props: WidgetProps) => ReactNode; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; }; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; - export default function Zone({ selectedWidgetArea, zone, zoneName, layoutConstraint, built, + isMobile, children, renderWidget, onWidgetAreaSelect, }: Props) { return ( <> - {sections.map(s => ( + {WAS_SECTIONS.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} @@ -55,6 +55,7 @@ export default function Zone({ centered={zone?.[s]?.[a]?.centered} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/constants.ts b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/constants.ts new file mode 100644 index 0000000000..0eb045605c --- /dev/null +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/constants.ts @@ -0,0 +1,5 @@ +import { type WidgetZone, type WidgetSection, type WidgetAlignSystem } from "."; + +export const WAS_SECTIONS = ["left", "center", "right"] as (keyof WidgetZone)[]; +export const WAS_AREAS = ["top", "middle", "bottom"] as (keyof WidgetSection)[]; +export const WAS_ZONES = ["outer", "inner"] as (keyof WidgetAlignSystem)[]; diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/index.tsx b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/index.tsx index 7793f3afeb..7a96c16da0 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/index.tsx @@ -96,6 +96,7 @@ const WidgetAlignSystem: React.FC = ({ invisibleWidgetIDs={invisibleWidgetIDs} theme={theme} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect}> {(!isMobile || hasInner) && ( @@ -106,6 +107,7 @@ const WidgetAlignSystem: React.FC = ({ zone={alignSystem?.inner} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/types.ts b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/types.ts index 972f32618c..66cc9dfcdf 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/types.ts +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/types.ts @@ -58,4 +58,5 @@ export type WidgetProps = { extended?: boolean; editing: boolean; onExtend?: (id: string, extended: boolean | undefined) => void; + onVisibilityChange?: (widgetId: string, visible: boolean) => void; }; diff --git a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/utils.ts b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/utils.ts index e73aee74ab..5f17917129 100644 --- a/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/utils.ts +++ b/web/src/beta/lib/core/Crust/Widgets/WidgetAlignSystem/utils.ts @@ -1,16 +1,29 @@ -import { WidgetZone } from "./types"; +import { isBuiltinWidget } from "../Widget/builtin"; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; +import { WAS_SECTIONS, WAS_AREAS } from "./constants"; +import { InternalWidget, WidgetZone } from "./types"; export const filterSections = ( zone?: WidgetZone, invisibleWidgetIDs?: string[], - cb?: (s: (typeof sections)[number]) => void, + cb?: (s: (typeof WAS_SECTIONS)[number]) => void, ) => { - return sections.filter( + return WAS_SECTIONS.filter( s => - areas.filter(a => zone?.[s]?.[a]?.widgets?.find(w => !invisibleWidgetIDs?.includes(w.id))) + WAS_AREAS.filter(a => zone?.[s]?.[a]?.widgets?.some(w => !invisibleWidgetIDs?.includes(w.id))) .length || cb?.(s), ); }; + +export const isInvisibleBuiltin = (widget: InternalWidget, isMobile?: boolean) => { + const defaultVisible = widget.property?.default?.visible; + return ( + isBuiltinWidget(`${widget.pluginId}/${widget.extensionId}`) && + !( + !defaultVisible || + defaultVisible === "always" || + (defaultVisible === "desktop" && !isMobile) || + (defaultVisible === "mobile" && !!isMobile) + ) + ); +}; diff --git a/web/src/beta/lib/core/Crust/Widgets/index.tsx b/web/src/beta/lib/core/Crust/Widgets/index.tsx index 7e20e0f4d8..4f2bbee760 100644 --- a/web/src/beta/lib/core/Crust/Widgets/index.tsx +++ b/web/src/beta/lib/core/Crust/Widgets/index.tsx @@ -60,7 +60,7 @@ export type WidgetProps = { layout?: WidgetLayout; onExtend?: (id: string, extended: boolean | undefined) => void; onWidgetMove?: (widgetId: string, options: WidgetLocationOptions) => void; - onVisibilityChange: (widgetId: string, v: boolean) => void; + onVisibilityChange?: (widgetId: string, visible: boolean) => void; }; export default function Widgets({ @@ -80,9 +80,10 @@ export default function Widgets({ onWidgetLayoutUpdate, onWidgetAreaSelect, }: Props): JSX.Element | null { - const { overriddenAlignSystem, moveWidget, onVisibilityChange, invisibleWidgetIDs } = + const { overriddenAlignSystem, moveWidget, invisibleWidgetIDs, onPluginWidgetVisibilityChange } = useWidgetAlignSystem({ alignSystem, + isMobile, }); const renderWidgetInternal = useCallback( @@ -106,10 +107,9 @@ export default function Widgets({ layout, onExtend, onWidgetMove: moveWidget, - onVisibilityChange, + onVisibilityChange: onPluginWidgetVisibilityChange, }) } - onVisibilityChange={onVisibilityChange} onExtend={onExtend} /> ), @@ -120,9 +120,9 @@ export default function Widgets({ isBuilt, isMobile, context, - onVisibilityChange, renderWidget, moveWidget, + onPluginWidgetVisibilityChange, ], ); diff --git a/web/src/beta/lib/core/Crust/Widgets/useWidgetAlignSystem.ts b/web/src/beta/lib/core/Crust/Widgets/useWidgetAlignSystem.ts index 82618a16a1..f8c77e4d6f 100644 --- a/web/src/beta/lib/core/Crust/Widgets/useWidgetAlignSystem.ts +++ b/web/src/beta/lib/core/Crust/Widgets/useWidgetAlignSystem.ts @@ -1,15 +1,24 @@ import { useEffect, useState, useCallback } from "react"; -import type { - WidgetAlignSystem, - InternalWidget, - WidgetLocationOptions, - WidgetArea, - WidgetSection, - WidgetZone, +import { WAS_SECTIONS, WAS_AREAS, WAS_ZONES } from "./WidgetAlignSystem/constants"; +import { isInvisibleBuiltin } from "./WidgetAlignSystem/utils"; + +import { + type WidgetAlignSystem, + type InternalWidget, + type WidgetLocationOptions, + type WidgetArea, + type WidgetSection, + type WidgetZone, } from "."; -export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) => { +export default ({ + alignSystem, + isMobile, +}: { + alignSystem: WidgetAlignSystem | undefined; + isMobile?: boolean; +}) => { const [overriddenAlignSystem, setOverrideAlignSystem] = useState( alignSystem, ); @@ -17,9 +26,25 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) // NOTE: This is invisible list of widget. // The reason why we use invisible list is prevent initializing cost. const [invisibleWidgetIDs, setInvisibleWidgetIDs] = useState([]); + const [invisiblePluginWidgetIDs, setInvisiblePluginWidgetIDs] = useState([]); - const onVisibilityChange = useCallback((widgetId: string, v: boolean) => { - setInvisibleWidgetIDs(a => { + useEffect(() => { + const invisibleBuiltinWidgetIDs: string[] = []; + WAS_ZONES.forEach(zone => { + WAS_SECTIONS.forEach(section => { + WAS_AREAS.forEach(area => { + overriddenAlignSystem?.[zone]?.[section]?.[area]?.widgets?.forEach(w => { + if (isInvisibleBuiltin(w, isMobile)) invisibleBuiltinWidgetIDs.push(w.id); + }); + }); + }); + }); + + setInvisibleWidgetIDs([...invisibleBuiltinWidgetIDs, ...invisiblePluginWidgetIDs]); + }, [isMobile, overriddenAlignSystem, invisiblePluginWidgetIDs]); + + const onPluginWidgetVisibilityChange = useCallback((widgetId: string, v: boolean) => { + setInvisiblePluginWidgetIDs(a => { if (!a.includes(widgetId) && !v) { return [...a, widgetId]; } @@ -32,9 +57,9 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) const moveWidget = useCallback((widgetId: string, options: WidgetLocationOptions) => { if ( - !["outer", "inner"].includes(options.zone) || - !["left", "center", "right"].includes(options.section) || - !["top", "middle", "bottom"].includes(options.area) || + !WAS_ZONES.includes(options.zone) || + !WAS_SECTIONS.includes(options.section) || + !WAS_AREAS.includes(options.area) || (options.section === "center" && options.area === "middle") ) return; @@ -132,8 +157,8 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) return { overriddenAlignSystem, - moveWidget, invisibleWidgetIDs, - onVisibilityChange, + moveWidget, + onPluginWidgetVisibilityChange, }; }; diff --git a/web/src/classic/components/molecules/Visualizer/Widget/SplashScreen/index.tsx b/web/src/classic/components/molecules/Visualizer/Widget/SplashScreen/index.tsx index 2ec3cc0392..0b2143d8b1 100644 --- a/web/src/classic/components/molecules/Visualizer/Widget/SplashScreen/index.tsx +++ b/web/src/classic/components/molecules/Visualizer/Widget/SplashScreen/index.tsx @@ -44,7 +44,6 @@ const SplashScreen = ({ widget, inEditor }: Props): JSX.Element | null => { overlayTitle: title, } = property?.overlay ?? {}; const camera = (property as Property | undefined)?.camera?.filter(c => !!c.cameraPosition); - const [cameraSequence, setCameraSequence] = useState(0); const [delayedCameraSequence, setDelayedCameraSequence] = useState(-1); const currentCamera = camera?.[cameraSequence]; diff --git a/web/src/classic/components/molecules/Visualizer/Widget/index.tsx b/web/src/classic/components/molecules/Visualizer/Widget/index.tsx index 8e01a332e8..4c99a2a265 100644 --- a/web/src/classic/components/molecules/Visualizer/Widget/index.tsx +++ b/web/src/classic/components/molecules/Visualizer/Widget/index.tsx @@ -28,9 +28,10 @@ export type Props = { editing?: boolean; viewport?: Viewport; onExtend?: (id: string, extended: boolean | undefined) => void; + onVisibilityChange?: (widgetId: string, visible: boolean) => void; } & PluginCommonProps; -export type ComponentProps = Omit, "widget" | "viewport"> & { +export type ComponentProps = Omit, "widget"> & { widget: RawWidget; }; @@ -105,7 +106,14 @@ export default function WidgetComponent({ return viewport ? ( Builtin ? (
- +
) : ( - {widgets?.map((widget, i) => { - const constraint = - widget.pluginId && widget.extensionId - ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] - : undefined; - const extended = overriddenExtended?.[widget.id]; - const extendable2 = - (section === "center" && constraint?.extendable?.horizontally) || - (area === "middle" && constraint?.extendable?.vertically); - return ( - - {({ editing }) => ( - - )} - - ); - })} + {widgets + ?.filter(widget => !isInvisibleBuiltin(widget, viewport?.isMobile)) + ?.map((widget, i) => { + const constraint = + widget.pluginId && widget.extensionId + ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] + : undefined; + const extended = overriddenExtended?.[widget.id]; + const extendable2 = + (section === "center" && constraint?.extendable?.horizontally) || + (area === "middle" && constraint?.extendable?.vertically); + return ( + + {({ editing }) => ( + + )} + + ); + })} ) : null; } diff --git a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/MobileZone.tsx b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/MobileZone.tsx index 2766e03033..5b7e9b4d54 100644 --- a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/MobileZone.tsx +++ b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/MobileZone.tsx @@ -10,7 +10,9 @@ import { Viewport } from "../hooks"; import type { CommonProps as PluginCommonProps } from "../Plugin"; import Area from "./Area"; +import { WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint } from "./hooks"; +import { filterSections } from "./utils"; export type Props = { children?: ReactNode; @@ -23,12 +25,10 @@ export type Props = { isBuilt?: boolean; sceneProperty?: any; viewport?: Viewport; + invisibleWidgetIDs?: string[]; onWidgetAlignAreaSelect?: (widgetArea?: WidgetAreaState) => void; } & PluginCommonProps; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; - export default function MobileZone({ isMobileZone, selectedWidgetArea, @@ -40,16 +40,14 @@ export default function MobileZone({ pluginBaseUrl, isEditable, isBuilt, + invisibleWidgetIDs, onWidgetAlignAreaSelect, children, ...props }: Props) { const filteredSections = useMemo(() => { - return sections.filter( - s => - areas.filter(a => zone?.[s]?.[a]?.widgets?.length).length || (s === "center" && children), - ); - }, [zone, children]); + return filterSections(zone, invisibleWidgetIDs, s => s === "center" && children); + }, [zone, children, invisibleWidgetIDs]); const initialPos = useMemo(() => (filteredSections.length === 3 ? 1 : 0), [filteredSections]); @@ -65,7 +63,7 @@ export default function MobileZone({ 1}> {filteredSections.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} diff --git a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/Zone.tsx b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/Zone.tsx index c3fec6eb74..268b1789be 100644 --- a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/Zone.tsx +++ b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/Zone.tsx @@ -7,6 +7,7 @@ import { Viewport } from "../hooks"; import type { CommonProps as PluginCommonProps } from "../Plugin"; import Area from "./Area"; +import { WAS_SECTIONS, WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint } from "./hooks"; export type Props = { @@ -24,9 +25,6 @@ export type Props = { onWidgetAlignAreaSelect?: (widgetAreaState?: WidgetAreaState) => void; } & PluginCommonProps; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; - export default function Zone({ isMobileZone, selectedWidgetArea, @@ -44,9 +42,9 @@ export default function Zone({ }: Props) { return ( <> - {sections.map(s => ( + {WAS_SECTIONS.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} diff --git a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/constants.ts b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/constants.ts new file mode 100644 index 0000000000..0eb045605c --- /dev/null +++ b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/constants.ts @@ -0,0 +1,5 @@ +import { type WidgetZone, type WidgetSection, type WidgetAlignSystem } from "."; + +export const WAS_SECTIONS = ["left", "center", "right"] as (keyof WidgetZone)[]; +export const WAS_AREAS = ["top", "middle", "bottom"] as (keyof WidgetSection)[]; +export const WAS_ZONES = ["outer", "inner"] as (keyof WidgetAlignSystem)[]; diff --git a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/index.tsx b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/index.tsx index 60f9b7e5f9..8fb59f6b41 100644 --- a/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/index.tsx +++ b/web/src/classic/components/molecules/Visualizer/WidgetAlignSystem/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { GridWrapper } from "react-align"; import { WidgetAreaState } from "@reearth/classic/components/organisms/EarthEditor/PropertyPane/hooks"; @@ -15,6 +15,7 @@ import type { WidgetLayoutConstraint, } from "./hooks"; import MobileZone from "./MobileZone"; +import { filterSections } from "./utils"; import ZoneComponent from "./Zone"; export type { @@ -38,6 +39,7 @@ export type Props = { isBuilt?: boolean; viewport?: Viewport; sceneProperty?: any; + invisibleWidgetIDs?: string[]; onWidgetUpdate?: ( id: string, update: { @@ -49,6 +51,7 @@ export type Props = { onWidgetAlignSystemUpdate?: (location: Location, align: Alignment) => void; overrideSceneProperty?: (pluginId: string, property: any) => void; onWidgetAlignAreaSelect?: (widgetArea?: WidgetAreaState) => void; + onVisibilityChange?: (widgetId: string, visible: boolean) => void; } & PluginCommonProps; const WidgetAlignSystem: React.FC = ({ @@ -61,6 +64,7 @@ const WidgetAlignSystem: React.FC = ({ isEditable, isBuilt, layoutConstraint, + invisibleWidgetIDs, onWidgetUpdate, onWidgetAlignSystemUpdate, onWidgetAlignAreaSelect, @@ -71,6 +75,13 @@ const WidgetAlignSystem: React.FC = ({ onWidgetAlignSystemUpdate, }); + const hasInner = useMemo(() => { + if (!alignSystem?.inner) { + return; + } + return !!filterSections(alignSystem?.inner, invisibleWidgetIDs).length; + }, [alignSystem, invisibleWidgetIDs]); + return ( = ({ layoutConstraint={layoutConstraint} onWidgetAlignAreaSelect={onWidgetAlignAreaSelect} {...props}> - {alignSystem?.inner && ( + {hasInner && ( void, +) => { + return WAS_SECTIONS.filter( + s => + WAS_AREAS.filter(a => zone?.[s]?.[a]?.widgets?.some(w => !invisibleWidgetIDs?.includes(w.id))) + .length || cb?.(s), + ); +}; + +export const isInvisibleBuiltin = (widget: Widget, isMobile?: boolean) => { + const defaultVisible = widget.property?.default?.visible; + return ( + isBuiltinWidget(`${widget.pluginId}/${widget.extensionId}`) && + !( + !defaultVisible || + defaultVisible === "always" || + (defaultVisible === "desktop" && !isMobile) || + (defaultVisible === "mobile" && !!isMobile) + ) + ); +}; diff --git a/web/src/classic/components/molecules/Visualizer/hooks.ts b/web/src/classic/components/molecules/Visualizer/hooks.ts index 654a497791..7c7292be05 100644 --- a/web/src/classic/components/molecules/Visualizer/hooks.ts +++ b/web/src/classic/components/molecules/Visualizer/hooks.ts @@ -218,9 +218,11 @@ export default ({ } }, [isPublished, enableGA, trackingId]); - const { overriddenAlignSystem, moveWidget } = useWidgetAlignSystem({ - alignSystem, - }); + const { overriddenAlignSystem, moveWidget, invisibleWidgetIDs, onPluginWidgetVisibilityChange } = + useWidgetAlignSystem({ + alignSystem, + isMobile: viewport.isMobile, + }); const pluginInstances = usePluginInstances({ alignSystem, @@ -301,6 +303,8 @@ export default ({ shownPluginPopupInfo, viewport, overriddenAlignSystem, + invisibleWidgetIDs, + onPluginWidgetVisibilityChange, onPluginModalShow, onPluginPopupShow, isLayerHidden, diff --git a/web/src/classic/components/molecules/Visualizer/index.tsx b/web/src/classic/components/molecules/Visualizer/index.tsx index ccbb11cf2c..107fb01f9d 100644 --- a/web/src/classic/components/molecules/Visualizer/index.tsx +++ b/web/src/classic/components/molecules/Visualizer/index.tsx @@ -137,6 +137,8 @@ export default function Visualizer({ shownPluginPopupInfo, overriddenAlignSystem, viewport, + invisibleWidgetIDs, + onPluginWidgetVisibilityChange, onPluginModalShow, onPluginPopupShow, isLayerHidden, @@ -200,6 +202,8 @@ export default function Visualizer({ pluginBaseUrl={pluginBaseUrl} layoutConstraint={widgets.layoutConstraint} onWidgetAlignAreaSelect={onWidgetAlignAreaSelect} + invisibleWidgetIDs={invisibleWidgetIDs} + onVisibilityChange={onPluginWidgetVisibilityChange} /> )} { +export default ({ + alignSystem, + isMobile, +}: { + alignSystem: WidgetAlignSystem | undefined; + isMobile?: boolean; +}) => { const [overriddenAlignSystem, setOverrideAlignSystem] = useState( alignSystem, ); const moveWidget = useCallback((widgetId: string, options: WidgetLocationOptions) => { if ( - !["outer", "inner"].includes(options.zone) || - !["left", "center", "right"].includes(options.section) || - !["top", "middle", "bottom"].includes(options.area) || + !WAS_ZONES.includes(options.zone) || + !WAS_SECTIONS.includes(options.section) || + !WAS_AREAS.includes(options.area) || (options.section === "center" && options.area === "middle") ) return; @@ -111,8 +119,42 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) setOverrideAlignSystem(alignSystem); }, [alignSystem]); + // NOTE: This is invisible list of widget. + // The reason why we use invisible list is prevent initializing cost. + const [invisibleWidgetIDs, setInvisibleWidgetIDs] = useState([]); + const [invisiblePluginWidgetIDs, setInvisiblePluginWidgetIDs] = useState([]); + + useEffect(() => { + const invisibleBuiltinWidgetIDs: string[] = []; + WAS_ZONES.forEach(zone => { + WAS_SECTIONS.forEach(section => { + WAS_AREAS.forEach(area => { + overriddenAlignSystem?.[zone]?.[section]?.[area]?.widgets?.forEach(w => { + if (isInvisibleBuiltin(w, isMobile)) invisibleBuiltinWidgetIDs.push(w.id); + }); + }); + }); + }); + + setInvisibleWidgetIDs([...invisibleBuiltinWidgetIDs, ...invisiblePluginWidgetIDs]); + }, [isMobile, overriddenAlignSystem, invisiblePluginWidgetIDs]); + + const onPluginWidgetVisibilityChange = useCallback((widgetId: string, v: boolean) => { + setInvisiblePluginWidgetIDs(a => { + if (!a.includes(widgetId) && !v) { + return [...a, widgetId]; + } + if (a.includes(widgetId) && v) { + return a.filter(i => i !== widgetId); + } + return a; + }); + }, []); + return { overriddenAlignSystem, + invisibleWidgetIDs, moveWidget, + onPluginWidgetVisibilityChange, }; }; diff --git a/web/src/classic/core/Crust/Widgets/Widget/Button/MenuButton.tsx b/web/src/classic/core/Crust/Widgets/Widget/Button/MenuButton.tsx index c989cc4cb7..77ba99f89a 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Button/MenuButton.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/Button/MenuButton.tsx @@ -8,7 +8,6 @@ import { metricsSizes } from "@reearth/classic/theme"; import { styled, mask } from "@reearth/services/theme"; import type { Camera, FlyToDestination, Theme } from "../types"; -import { Visible } from "../useVisible"; export type Button = { id: string; @@ -20,7 +19,7 @@ export type Button = { buttonColor?: string; buttonBgcolor?: string; buttonCamera?: Camera; - visible: Visible; + visible?: "always" | "desktop" | "mobile"; }; export type MenuItem = { diff --git a/web/src/classic/core/Crust/Widgets/Widget/Button/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/Button/index.tsx index 55a93d0e24..751a1c0fc6 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Button/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/Button/index.tsx @@ -1,7 +1,6 @@ import { styled } from "@reearth/services/theme"; import type { ComponentProps as WidgetProps } from ".."; -import { useVisible } from "../useVisible"; import MenuButton, { Button as ButtonType, MenuItem as MenuItemType } from "./MenuButton"; @@ -13,22 +12,10 @@ export type Property = { menu?: MenuItem[]; }; -const Menu = ({ - widget, - theme, - isMobile, - onVisibilityChange, - context: { onFlyTo } = {}, -}: Props): JSX.Element | null => { +const Menu = ({ widget, theme, context: { onFlyTo } = {} }: Props): JSX.Element | null => { const { default: button, menu: menuItems } = widget.property ?? {}; - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); - return visible ? ( + return ( - ) : null; + ); }; const Wrapper = styled.div` diff --git a/web/src/classic/core/Crust/Widgets/Widget/Navigator/hooks.ts b/web/src/classic/core/Crust/Widgets/Widget/Navigator/hooks.ts index dfa84f756c..2be3eb78c3 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Navigator/hooks.ts +++ b/web/src/classic/core/Crust/Widgets/Widget/Navigator/hooks.ts @@ -1,21 +1,17 @@ import { useState, useEffect, useCallback, useRef } from "react"; import type { Camera, FlyToDestination, Widget } from "../types"; -import { useVisible } from "../useVisible"; import { degreeToRadian, radianToDegree } from "./NavigatorPresenter"; export default function ({ camera, initialCamera, - widget, - isMobile, onZoomIn, onZoomOut, onCameraOrbit, onCameraRotateRight, onFlyTo, - onVisibilityChange, }: { camera?: Camera; initialCamera?: Camera; @@ -26,18 +22,11 @@ export default function ({ onCameraOrbit?: (orbit: number) => void; onCameraRotateRight?: (radian: number) => void; onFlyTo?: (target: string | FlyToDestination, options?: { duration?: number }) => void; - onVisibilityChange?: (id: string, visible: boolean) => void; }) { const [degree, setDegree] = useState(0); const [isHelpOpened, setIsHelpOpened] = useState(false); const orbitRadianRef = useRef(0); const isMovingOrbit = useRef(false); - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); const handleOnRotate = useCallback( (deg: number) => { @@ -101,7 +90,6 @@ export default function ({ return { degree, isHelpOpened, - visible, events: { onRotate: handleOnRotate, onStartOrbit: handleOnStartOrbit, diff --git a/web/src/classic/core/Crust/Widgets/Widget/Navigator/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/Navigator/index.tsx index 6807f2724d..2a79d4993a 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Navigator/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/Navigator/index.tsx @@ -1,23 +1,15 @@ import type { ComponentProps as WidgetProps } from ".."; -import { Visible } from "../useVisible"; import useHooks from "./hooks"; import NavigatorPresenter from "./NavigatorPresenter"; -export type Props = WidgetProps; - -export type Property = { - default: { - visible: Visible; - }; -}; +export type Props = WidgetProps; const Navigator = ({ theme, editing, widget, isMobile, - onVisibilityChange, context: { camera, initialCamera, @@ -28,7 +20,7 @@ const Navigator = ({ onZoomOut, } = {}, }: Props): JSX.Element | null => { - const { degree, visible, events } = useHooks({ + const { degree, events } = useHooks({ camera, initialCamera, widget, @@ -38,12 +30,9 @@ const Navigator = ({ onFlyTo, onZoomIn, onZoomOut, - onVisibilityChange, }); - return visible ? ( - - ) : null; + return ; }; export default Navigator; diff --git a/web/src/classic/core/Crust/Widgets/Widget/SplashScreen/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/SplashScreen/index.tsx index e5340c9b6f..512c35a1b3 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/SplashScreen/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/SplashScreen/index.tsx @@ -6,7 +6,6 @@ import { styled } from "@reearth/services/theme"; import type { ComponentProps as WidgetProps } from ".."; import type { Camera } from "../types"; -import { useVisible, Visible } from "../useVisible"; export type Props = WidgetProps; @@ -21,7 +20,7 @@ export type Property = { overlayImageW?: number; overlayImageH?: number; overlayTitle?: string; - visible?: Visible; + visible?: "always" | "desktop" | "mobile"; }; camera?: { cameraPosition?: Camera; @@ -33,8 +32,6 @@ export type Property = { const SplashScreen = ({ widget, inEditor, - isMobile, - onVisibilityChange, context: { onFlyTo } = {}, }: Props): JSX.Element | null => { const { property } = widget ?? {}; @@ -50,12 +47,6 @@ const SplashScreen = ({ overlayTitle: title, } = property?.overlay ?? {}; const camera = property?.camera?.filter(c => !!c.cameraPosition); - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.overlay.visible, - isMobile, - onVisibilityChange, - }); const [cameraSequence, setCameraSequence] = useState(0); const [delayedCameraSequence, setDelayedCameraSequence] = useState(-1); @@ -106,7 +97,7 @@ const SplashScreen = ({ return () => clearTimeout(t); }, [delayedCurrentCamera, inEditor]); - return !visible || state === "unmounted" ? null : ( + return state === "unmounted" ? null : ( {title} diff --git a/web/src/classic/core/Crust/Widgets/Widget/Storytelling/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/Storytelling/index.tsx index 8b2eab8b74..df91b94ce3 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Storytelling/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/Storytelling/index.tsx @@ -21,6 +21,7 @@ export type Property = { range?: number; camera?: Camera; autoStart?: boolean; + visible?: "always" | "desktop" | "mobile"; }; stories?: StoryType[]; }; diff --git a/web/src/classic/core/Crust/Widgets/Widget/Timeline/hooks.ts b/web/src/classic/core/Crust/Widgets/Widget/Timeline/hooks.ts index 2723f76c1c..cdc5150d0c 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Timeline/hooks.ts +++ b/web/src/classic/core/Crust/Widgets/Widget/Timeline/hooks.ts @@ -4,7 +4,6 @@ import type { TimeEventHandler } from "@reearth/classic/components/atoms/Timelin import { TickEvent, TickEventCallback } from "@reearth/classic/core/Map"; import type { Clock, Widget } from "../types"; -import { useVisible } from "../useVisible"; const MAX_RANGE = 86400000; // a day const getOrNewDate = (d?: Date) => d ?? new Date(); @@ -31,7 +30,6 @@ export const useTimeline = ({ onTick, removeTickEventListener, onExtend, - onVisibilityChange, }: { widget: Widget; clock?: Clock; @@ -44,14 +42,7 @@ export const useTimeline = ({ onTick?: TickEvent; removeTickEventListener?: TickEvent; onExtend?: (id: string, extended: boolean | undefined) => void; - onVisibilityChange?: (id: string, v: boolean) => void; }) => { - const visible = useVisible({ - widgetId: widget.id, - visible: widget.property?.default?.visible, - isMobile, - onVisibilityChange, - }); const widgetId = widget.id; const [range, setRange] = useState(() => makeRange(clock?.start?.getTime(), clock?.stop?.getTime()), @@ -227,7 +218,6 @@ export const useTimeline = ({ range, isOpened, currentTime, - visible, events: { onOpen: handleOnOpen, onClose: handleOnClose, diff --git a/web/src/classic/core/Crust/Widgets/Widget/Timeline/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/Timeline/index.tsx index 5f72cb7208..4337d04dfe 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/Timeline/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/Timeline/index.tsx @@ -2,24 +2,16 @@ import TimelineUI from "@reearth/classic/components/atoms/Timeline"; import { styled } from "@reearth/services/theme"; import type { ComponentProps as WidgetProps } from ".."; -import { Visible } from "../useVisible"; import { useTimeline } from "./hooks"; -export type Props = WidgetProps; - -export type Property = { - default: { - visible: Visible; - }; -}; +export type Props = WidgetProps; const Timeline = ({ widget, theme, isMobile, onExtend, - onVisibilityChange, context: { clock, overriddenClock, @@ -31,7 +23,7 @@ const Timeline = ({ removeTickEventListener, } = {}, }: Props): JSX.Element | null => { - const { isOpened, currentTime, range, speed, events, visible } = useTimeline({ + const { isOpened, currentTime, range, speed, events } = useTimeline({ widget, clock, overriddenClock, @@ -43,10 +35,9 @@ const Timeline = ({ onTick, removeTickEventListener, onExtend, - onVisibilityChange, }); - return visible ? ( + return ( - ) : null; + ); }; const Widget = styled.div<{ diff --git a/web/src/classic/core/Crust/Widgets/Widget/index.tsx b/web/src/classic/core/Crust/Widgets/Widget/index.tsx index 3867a3f7a9..48fbe3ef57 100644 --- a/web/src/classic/core/Crust/Widgets/Widget/index.tsx +++ b/web/src/classic/core/Crust/Widgets/Widget/index.tsx @@ -30,7 +30,6 @@ export type Props = { isMobile?: boolean; context?: Context; onExtend?: (id: string, extended: boolean | undefined) => void; - onVisibilityChange?: (id: string, visible: boolean) => void; renderWidget?: (w: Widget) => ReactNode; }; diff --git a/web/src/classic/core/Crust/Widgets/Widget/useVisible.ts b/web/src/classic/core/Crust/Widgets/Widget/useVisible.ts deleted file mode 100644 index e6ae3e7555..0000000000 --- a/web/src/classic/core/Crust/Widgets/Widget/useVisible.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useMemo } from "react"; - -export type Visible = "always" | "desktop" | "mobile"; - -export const useVisible = ({ - widgetId, - visible: defaultVisible, - isMobile, - onVisibilityChange, -}: { - widgetId: string | undefined; - visible: Visible | undefined; - isMobile: boolean | undefined; - onVisibilityChange: ((id: string, v: boolean) => void) | undefined; -}) => { - const visible = useMemo( - () => - !defaultVisible || - defaultVisible === "always" || - (defaultVisible === "desktop" && !isMobile) || - (defaultVisible === "mobile" && !!isMobile), - [defaultVisible, isMobile], - ); - - useEffect(() => { - if (widgetId) { - onVisibilityChange?.(widgetId, visible); - } - }, [widgetId, visible, onVisibilityChange]); - - return visible; -}; diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Area.tsx b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Area.tsx index 6e132c4add..5f4e431f24 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Area.tsx +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Area.tsx @@ -14,6 +14,7 @@ import type { WidgetProps, InternalWidget, } from "./types"; +import { isInvisibleBuiltin } from "./utils"; export type WidgetAreaType = { zone: "inner" | "outer"; @@ -39,6 +40,7 @@ type Props = { centered?: boolean; built?: boolean; widgets?: InternalWidget[]; + isMobile?: boolean; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; // note that layoutConstraint will be always undefined in published pages layoutConstraint?: { [w in string]: WidgetLayoutConstraint }; @@ -58,6 +60,7 @@ export default function Area({ built, widgets, layoutConstraint, + isMobile, renderWidget, onWidgetAreaSelect, }: Props) { @@ -136,36 +139,38 @@ export default function Area({ alignItems: centered ? "center" : "unset", }} iconColor={area === "middle" ? "#4770FF" : "#E95518"}> - {widgets?.map((widget, i) => { - const constraint = - widget.pluginId && widget.extensionId - ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] - : undefined; - const extended = overriddenExtended?.[widget.id]; - const extendable2 = - (section === "center" && constraint?.extendable?.horizontally) || - (area === "middle" && constraint?.extendable?.vertically); - return ( - - {({ editing }) => - renderWidget?.({ - widget, - layout, - extended, - editing, - onExtend: handleExtend, - }) - } - - ); - })} + {widgets + ?.filter(widget => !isInvisibleBuiltin(widget, isMobile)) + ?.map((widget, i) => { + const constraint = + widget.pluginId && widget.extensionId + ? layoutConstraint?.[`${widget.pluginId}/${widget.extensionId}`] + : undefined; + const extended = overriddenExtended?.[widget.id]; + const extendable2 = + (section === "center" && constraint?.extendable?.horizontally) || + (area === "middle" && constraint?.extendable?.vertically); + return ( + + {({ editing }) => + renderWidget?.({ + widget, + layout, + extended, + editing, + onExtend: handleExtend, + }) + } + + ); + })} ) : null; } diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx index f8eed3288e..acc7b2d941 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/MobileZone.tsx @@ -6,6 +6,7 @@ import Slide from "@reearth/classic/components/atoms/Slide"; import { styled } from "@reearth/services/theme"; import Area, { WidgetAreaType } from "./Area"; +import { WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint, Theme, WidgetProps } from "./types"; import { filterSections } from "./utils"; @@ -18,12 +19,11 @@ export type Props = { built?: boolean; layoutConstraint?: { [w: string]: WidgetLayoutConstraint }; invisibleWidgetIDs?: string[]; + isMobile?: boolean; renderWidget?: (props: WidgetProps) => ReactNode; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; }; -const areas = ["top", "middle", "bottom"] as const; - export default function MobileZone({ selectedWidgetArea, zone, @@ -33,6 +33,7 @@ export default function MobileZone({ built, children, invisibleWidgetIDs, + isMobile, renderWidget, onWidgetAreaSelect, }: Props) { @@ -53,7 +54,7 @@ export default function MobileZone({ 1}> {filteredSections.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} @@ -73,6 +74,7 @@ export default function MobileZone({ backgroundColor={zone?.[s]?.[a]?.background ?? "unset"} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx index 25b2a4cc68..f66f4e6b02 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/Zone.tsx @@ -2,6 +2,7 @@ import { ReactNode } from "react"; import { GridSection } from "react-align"; import Area, { WidgetAreaType } from "./Area"; +import { WAS_SECTIONS, WAS_AREAS } from "./constants"; import type { WidgetZone, WidgetLayoutConstraint, WidgetProps } from "./types"; export type { WidgetAreaType }; @@ -14,28 +15,27 @@ export type Props = { invisibleWidgetIDs?: string[]; layoutConstraint?: { [w: string]: WidgetLayoutConstraint }; built?: boolean; + isMobile?: boolean; renderWidget?: (props: WidgetProps) => ReactNode; onWidgetAreaSelect?: (widgetArea?: WidgetAreaType) => void; }; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; - export default function Zone({ selectedWidgetArea, zone, zoneName, layoutConstraint, built, + isMobile, children, renderWidget, onWidgetAreaSelect, }: Props) { return ( <> - {sections.map(s => ( + {WAS_SECTIONS.map(s => ( - {areas.map(a => + {WAS_AREAS.map(a => s === "center" && children && a === "middle" ? (
{children} @@ -55,6 +55,7 @@ export default function Zone({ centered={zone?.[s]?.[a]?.centered} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/constants.ts b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/constants.ts new file mode 100644 index 0000000000..0eb045605c --- /dev/null +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/constants.ts @@ -0,0 +1,5 @@ +import { type WidgetZone, type WidgetSection, type WidgetAlignSystem } from "."; + +export const WAS_SECTIONS = ["left", "center", "right"] as (keyof WidgetZone)[]; +export const WAS_AREAS = ["top", "middle", "bottom"] as (keyof WidgetSection)[]; +export const WAS_ZONES = ["outer", "inner"] as (keyof WidgetAlignSystem)[]; diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/index.tsx b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/index.tsx index 1c59ac40f8..648ee861ac 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/index.tsx +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/index.tsx @@ -96,6 +96,7 @@ const WidgetAlignSystem: React.FC = ({ invisibleWidgetIDs={invisibleWidgetIDs} theme={theme} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect}> {(!isMobile || hasInner) && ( @@ -106,6 +107,7 @@ const WidgetAlignSystem: React.FC = ({ zone={alignSystem?.inner} layoutConstraint={layoutConstraint} built={built} + isMobile={isMobile} renderWidget={renderWidget} onWidgetAreaSelect={onWidgetAreaSelect} /> diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/types.ts b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/types.ts index 972f32618c..76710f374a 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/types.ts +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/types.ts @@ -58,4 +58,5 @@ export type WidgetProps = { extended?: boolean; editing: boolean; onExtend?: (id: string, extended: boolean | undefined) => void; + onVisibilityChange?: (id: string, visible: boolean) => void; }; diff --git a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/utils.ts b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/utils.ts index e73aee74ab..2fb2049104 100644 --- a/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/utils.ts +++ b/web/src/classic/core/Crust/Widgets/WidgetAlignSystem/utils.ts @@ -1,16 +1,29 @@ -import { WidgetZone } from "./types"; +import { isBuiltinWidget } from "../Widget/builtin"; -const sections = ["left", "center", "right"] as const; -const areas = ["top", "middle", "bottom"] as const; +import { WAS_SECTIONS, WAS_AREAS } from "./constants"; +import { InternalWidget, WidgetZone } from "./types"; export const filterSections = ( zone?: WidgetZone, invisibleWidgetIDs?: string[], - cb?: (s: (typeof sections)[number]) => void, + cb?: (s: (typeof WAS_SECTIONS)[number]) => void, ) => { - return sections.filter( + return WAS_SECTIONS.filter( s => - areas.filter(a => zone?.[s]?.[a]?.widgets?.find(w => !invisibleWidgetIDs?.includes(w.id))) - .length || cb?.(s), + WAS_AREAS.filter(a => zone?.[s]?.[a]?.widgets?.some(w => !invisibleWidgetIDs?.includes(w.id))) + .length > 0 || cb?.(s), + ); +}; + +export const isInvisibleBuiltin = (widget: InternalWidget, isMobile?: boolean) => { + const defaultVisible = widget.property?.default?.visible; + return ( + isBuiltinWidget(`${widget.pluginId}/${widget.extensionId}`) && + !( + !defaultVisible || + defaultVisible === "always" || + (defaultVisible === "desktop" && !isMobile) || + (defaultVisible === "mobile" && !!isMobile) + ) ); }; diff --git a/web/src/classic/core/Crust/Widgets/index.tsx b/web/src/classic/core/Crust/Widgets/index.tsx index 7e20e0f4d8..4f2bbee760 100644 --- a/web/src/classic/core/Crust/Widgets/index.tsx +++ b/web/src/classic/core/Crust/Widgets/index.tsx @@ -60,7 +60,7 @@ export type WidgetProps = { layout?: WidgetLayout; onExtend?: (id: string, extended: boolean | undefined) => void; onWidgetMove?: (widgetId: string, options: WidgetLocationOptions) => void; - onVisibilityChange: (widgetId: string, v: boolean) => void; + onVisibilityChange?: (widgetId: string, visible: boolean) => void; }; export default function Widgets({ @@ -80,9 +80,10 @@ export default function Widgets({ onWidgetLayoutUpdate, onWidgetAreaSelect, }: Props): JSX.Element | null { - const { overriddenAlignSystem, moveWidget, onVisibilityChange, invisibleWidgetIDs } = + const { overriddenAlignSystem, moveWidget, invisibleWidgetIDs, onPluginWidgetVisibilityChange } = useWidgetAlignSystem({ alignSystem, + isMobile, }); const renderWidgetInternal = useCallback( @@ -106,10 +107,9 @@ export default function Widgets({ layout, onExtend, onWidgetMove: moveWidget, - onVisibilityChange, + onVisibilityChange: onPluginWidgetVisibilityChange, }) } - onVisibilityChange={onVisibilityChange} onExtend={onExtend} /> ), @@ -120,9 +120,9 @@ export default function Widgets({ isBuilt, isMobile, context, - onVisibilityChange, renderWidget, moveWidget, + onPluginWidgetVisibilityChange, ], ); diff --git a/web/src/classic/core/Crust/Widgets/useWidgetAlignSystem.ts b/web/src/classic/core/Crust/Widgets/useWidgetAlignSystem.ts index 82618a16a1..5719d39f43 100644 --- a/web/src/classic/core/Crust/Widgets/useWidgetAlignSystem.ts +++ b/web/src/classic/core/Crust/Widgets/useWidgetAlignSystem.ts @@ -1,15 +1,24 @@ import { useEffect, useState, useCallback } from "react"; -import type { - WidgetAlignSystem, - InternalWidget, - WidgetLocationOptions, - WidgetArea, - WidgetSection, - WidgetZone, +import { WAS_SECTIONS, WAS_AREAS, WAS_ZONES } from "./WidgetAlignSystem/constants"; +import { isInvisibleBuiltin } from "./WidgetAlignSystem/utils"; + +import { + type WidgetAlignSystem, + type InternalWidget, + type WidgetLocationOptions, + type WidgetArea, + type WidgetSection, + type WidgetZone, } from "."; -export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) => { +export default ({ + alignSystem, + isMobile, +}: { + alignSystem: WidgetAlignSystem | undefined; + isMobile?: boolean; +}) => { const [overriddenAlignSystem, setOverrideAlignSystem] = useState( alignSystem, ); @@ -17,9 +26,25 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) // NOTE: This is invisible list of widget. // The reason why we use invisible list is prevent initializing cost. const [invisibleWidgetIDs, setInvisibleWidgetIDs] = useState([]); + const [invisiblePluginWidgetIDs, setInvisiblePluginWidgetIDs] = useState([]); + + useEffect(() => { + const invisibleBuiltinWidgetIDs: string[] = []; + WAS_ZONES.forEach(zone => { + WAS_SECTIONS.forEach(section => { + WAS_AREAS.forEach(area => { + overriddenAlignSystem?.[zone]?.[section]?.[area]?.widgets?.forEach(w => { + if (isInvisibleBuiltin(w, isMobile)) invisibleBuiltinWidgetIDs.push(w.id); + }); + }); + }); + }); + + setInvisibleWidgetIDs([...invisibleBuiltinWidgetIDs, ...invisiblePluginWidgetIDs]); + }, [isMobile, overriddenAlignSystem, invisiblePluginWidgetIDs]); - const onVisibilityChange = useCallback((widgetId: string, v: boolean) => { - setInvisibleWidgetIDs(a => { + const onPluginWidgetVisibilityChange = useCallback((widgetId: string, v: boolean) => { + setInvisiblePluginWidgetIDs(a => { if (!a.includes(widgetId) && !v) { return [...a, widgetId]; } @@ -32,9 +57,9 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) const moveWidget = useCallback((widgetId: string, options: WidgetLocationOptions) => { if ( - !["outer", "inner"].includes(options.zone) || - !["left", "center", "right"].includes(options.section) || - !["top", "middle", "bottom"].includes(options.area) || + !WAS_ZONES.includes(options.zone) || + !WAS_SECTIONS.includes(options.section) || + !WAS_AREAS.includes(options.area) || (options.section === "center" && options.area === "middle") ) return; @@ -134,6 +159,6 @@ export default ({ alignSystem }: { alignSystem: WidgetAlignSystem | undefined }) overriddenAlignSystem, moveWidget, invisibleWidgetIDs, - onVisibilityChange, + onPluginWidgetVisibilityChange, }; };