diff --git a/package.json b/package.json index d87b18439..412364486 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "rc-slider": "9.7.1", "react": "^17.0.1", "react-accessible-accordion": "^3.3.4", - "react-colorful": "^4.4.4", + "react-colorful": "^5.3.0", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "^17.0.1", diff --git a/src/components/atoms/PropertyPane/index.stories.tsx b/src/components/atoms/PropertyPane/index.stories.tsx deleted file mode 100644 index 8c35b98eb..000000000 --- a/src/components/atoms/PropertyPane/index.stories.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { Meta } from "@storybook/react"; -import PropertyPane from "."; - -export default { - title: "atoms/PropertyPane", - component: PropertyPane, -} as Meta; - -export const Default = () => ; diff --git a/src/components/atoms/PropertyPane/index.tsx b/src/components/atoms/PropertyPane/index.tsx deleted file mode 100644 index 1359f3592..000000000 --- a/src/components/atoms/PropertyPane/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -import { styled } from "@reearth/theme"; - -export interface Props { - className?: string; -} - -const PropertyPane: React.FC = ({ className, children }) => { - return {children}; -}; - -const Wrapper = styled.div` - background: ${props => props.theme.properties}; - margin: 14px 0; -`; - -export default PropertyPane; diff --git a/src/components/molecules/EarthEditor/ExportPane/index.tsx b/src/components/molecules/EarthEditor/ExportPane/index.tsx index 4f1875ca7..28ed281fc 100644 --- a/src/components/molecules/EarthEditor/ExportPane/index.tsx +++ b/src/components/molecules/EarthEditor/ExportPane/index.tsx @@ -1,8 +1,6 @@ import React, { useState, useCallback } from "react"; import { useIntl } from "react-intl"; import { styled } from "@reearth/theme"; -// TODO: 後で汎用化して PropertyPane をやめる -import Wrapper from "@reearth/components/atoms/PropertyPane"; import Button from "@reearth/components/atoms/Button"; import SelectBox, { Props as SelectBoxProps } from "@reearth/components/atoms/SelectBox"; import Text from "@reearth/components/atoms/Text"; @@ -47,6 +45,11 @@ const ExportPane: React.FC = ({ className, show = true, onExport }) => { ) : null; }; +const Wrapper = styled.div` + background: ${props => props.theme.properties}; + margin: 14px 0; +`; + const SelectWrapper = styled.div` display: flex; align-items: center; diff --git a/src/components/molecules/EarthEditor/LayerMultipleSelectionModal/hooks.ts b/src/components/molecules/EarthEditor/LayerMultipleSelectionModal/hooks.ts index 9a649f1a9..417769f56 100644 --- a/src/components/molecules/EarthEditor/LayerMultipleSelectionModal/hooks.ts +++ b/src/components/molecules/EarthEditor/LayerMultipleSelectionModal/hooks.ts @@ -67,10 +67,10 @@ export default function ({ selectRightLayerIds(layers.map(l => l.id)); }, []); - const ok = useCallback(() => onSelect?.(rightLayers.children?.map(l => l.content) ?? []), [ - onSelect, - rightLayers.children, - ]); + const ok = useCallback( + () => onSelect?.(rightLayers.children?.map(l => l.content) ?? []), + [onSelect, rightLayers.children], + ); const addLayers = useCallback(() => { const layers = uniqBy(flattenLayers(selectedLeftLayers.current), l => l.id); diff --git a/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/hooks.ts b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/hooks.ts new file mode 100644 index 000000000..5ca1100a3 --- /dev/null +++ b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/hooks.ts @@ -0,0 +1,56 @@ +import React, { useState, useEffect, useCallback } from "react"; + +export default function ({ + group, + visible, + visibilityChangeable, + onExpand, + onVisibilityChange, +}: { + group?: boolean; + visible?: boolean; + visibilityChangeable?: boolean; + onExpand?: () => void; + onVisibilityChange?: (visible: boolean) => void; +}) { + const [isHover, toggleHover] = useState(false); + const [showHelp, setShowHelp] = useState(false); + const mouseEnterSec = 1100; + + const handleVisibilityChange = useCallback( + (event: React.MouseEvent) => { + if (!visibilityChangeable) return; + event.stopPropagation(); + onVisibilityChange?.(!visible); + }, + [visible, onVisibilityChange, visibilityChangeable], + ); + + const handleExpand = useCallback( + (e: React.MouseEvent) => { + if (!group) return; + e.stopPropagation(); + onExpand?.(); + }, + [onExpand, group], + ); + + useEffect(() => { + if (isHover) { + const timer = setTimeout(() => { + setShowHelp(true); + }, mouseEnterSec); + return () => clearTimeout(timer); + } + setShowHelp(false); + return; + }, [isHover]); + + return { + isHover, + showHelp, + toggleHover, + handleVisibilityChange, + handleExpand, + }; +} diff --git a/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer.tsx b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/index.tsx similarity index 74% rename from src/components/molecules/EarthEditor/LayerTreeViewItem/Layer.tsx rename to src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/index.tsx index 23b750116..e482656bf 100644 --- a/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer.tsx +++ b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/index.tsx @@ -1,14 +1,16 @@ -import React, { forwardRef, useState, useEffect, useRef, useCallback } from "react"; -import { useClickAway } from "react-use"; +import React, { forwardRef } from "react"; import { styled, useTheme } from "@reearth/theme"; -import useDoubleClick from "@reearth/util/use-double-click"; import Icon from "@reearth/components/atoms/Icon"; import HelpButton from "@reearth/components/atoms/HelpButton"; import Text from "@reearth/components/atoms/Text"; import fonts from "@reearth/theme/fonts"; -import LayerActions from "./LayerActions"; +import useDoubleClick from "@reearth/util/use-double-click"; + +import LayerActions, { Format } from "../LayerActions"; +import useHooks from "./hooks"; +import useEditable from "./use-editable"; -export type Format = "kml" | "czml" | "geojson" | "shape" | "reearth"; +export type { Format } from "../LayerActions"; export type DropType = "top" | "bottom" | "bottomOfChildren"; @@ -40,6 +42,7 @@ export type Props = { childSelected?: boolean; dropType?: DropType; allSiblingsDoesNotHaveChildren?: boolean; + visibilityShown?: boolean; onClick: () => void; onExpand?: () => void; onVisibilityChange?: (isVisible: boolean) => void; @@ -75,6 +78,7 @@ const Layer: React.ForwardRefRenderFunction = ( childSelected, dropType, allSiblingsDoesNotHaveChildren, + visibilityShown, onVisibilityChange, onClick, onExpand, @@ -85,90 +89,20 @@ const Layer: React.ForwardRefRenderFunction = ( }, ref, ) => { - const [editing, setEditing] = useState(false); - const [editingName, setEditingName] = useState(title || ""); - const [isHover, toggleHover] = useState(false); - const editingNameRef = useRef(editingName); - const [showHelp, setShowHelp] = useState(false); - const mouseEnterSec = 1100; - - const handleVisibilityChange = useCallback( - (event: React.MouseEvent) => { - if (!visibilityChangeable) return; - event.stopPropagation(); - onVisibilityChange?.(!visible); - }, - [visible, onVisibilityChange, visibilityChangeable], - ); - - const startEditing = useCallback(() => { - if (!renamable) return; - setEditing(true); - }, [renamable]); - const finishEditing = useCallback(() => { - setEditing(false); - }, []); - const resetEditing = useCallback(() => { - editingNameRef.current = title || ""; - setEditingName(title || ""); - }, [title]); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - finishEditing(); - } else if (e.key === "Escape") { - resetEditing(); - finishEditing(); - } - }, - [resetEditing, finishEditing], - ); - const handleChange = useCallback((e: React.ChangeEvent) => { - editingNameRef.current = e.currentTarget.value; - setEditingName(e.currentTarget.value); - }, []); - - useEffect(() => { - if (isHover) { - const timer = setTimeout(() => { - setShowHelp(true); - }, mouseEnterSec); - return () => clearTimeout(timer); - } else { - setShowHelp(false); - return; - } - }, [isHover]); - - useEffect(() => { - resetEditing(); - }, [resetEditing]); - - useEffect( - () => - editing - ? () => { - if (title !== editingNameRef.current) { - onRename?.(editingNameRef.current); - } - resetEditing(); - } - : undefined, - [editing, title, onRename, resetEditing], - ); - - const inputRef = useRef(null); - useClickAway(inputRef, finishEditing); + const { isHover, showHelp, handleExpand, handleVisibilityChange, toggleHover } = useHooks({ + group, + visibilityChangeable, + visible, + onExpand, + onVisibilityChange, + }); + const { editing, editingName, startEditing, inputProps } = useEditable({ + name: title, + renamable, + onRename, + }); const [handleClick, handleDoubleClick] = useDoubleClick(onClick, startEditing); - const handleExpand = useCallback( - (e: React.MouseEvent) => { - if (!group) return; - e.stopPropagation(); - onExpand?.(); - }, - [onExpand, group], - ); const theme = useTheme(); @@ -209,15 +143,7 @@ const Layer: React.ForwardRefRenderFunction = ( /> {editing ? ( - + ) : ( <> = ( ? theme.layers.disableTextColor : theme.layers.textColor }> - {title} + {editingName} {group && typeof childrenCount === "number" && showChildrenCount && ( = ( {childrenCount} )} - {typeof visible === "boolean" && ( + {visibilityShown && ( diff --git a/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/use-editable.ts b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/use-editable.ts new file mode 100644 index 000000000..23d19c110 --- /dev/null +++ b/src/components/molecules/EarthEditor/LayerTreeViewItem/Layer/use-editable.ts @@ -0,0 +1,76 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useClickAway } from "react-use"; + +export default function ({ + name, + renamable, + onRename, +}: { + name?: string; + renamable?: boolean; + onClick?: () => void; + onRename?: (name: string) => void; +}) { + const [editing, setEditing] = useState(false); + const [editingName, setEditingName] = useState(name || ""); + const editingNameRef = useRef(editingName); + + const startEditing = useCallback(() => { + if (!renamable) return; + setEditing(true); + }, [renamable]); + + const finishEditing = useCallback(() => { + setEditing(false); + if (name !== editingNameRef.current) { + onRename?.(editingNameRef.current); + } + }, [name, onRename]); + + const resetEditing = useCallback(() => { + editingNameRef.current = name || ""; + setEditingName(name || ""); + }, [name]); + + const cancelEditing = useCallback(() => { + resetEditing(); + finishEditing(); + }, [finishEditing, resetEditing]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + finishEditing(); + } else if (e.key === "Escape") { + cancelEditing(); + } + }, + [cancelEditing, finishEditing], + ); + + const handleChange = useCallback((e: React.ChangeEvent) => { + editingNameRef.current = e.currentTarget.value; + setEditingName(e.currentTarget.value); + }, []); + + useEffect(() => { + resetEditing(); + }, [resetEditing]); + + const inputRef = useRef(null); + useClickAway(inputRef, cancelEditing); + + return { + editing, + editingName, + startEditing, + finishEditing, + inputProps: { + value: editingName, + ref: inputRef, + onChange: handleChange, + onKeyDown: handleKeyDown, + onBlur: finishEditing, + }, + }; +} diff --git a/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/hooks.tsx b/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/hooks.tsx deleted file mode 100644 index 9c2ef3875..000000000 --- a/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/hooks.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import useFileInput from "use-file-input"; - -export type Format = "kml" | "czml" | "geojson" | "shape" | "reearth"; - -export default ({ onLayerImport }: { onLayerImport?: (file: File, format: Format) => void }) => { - const importLayer = useFileInput( - (files: FileList) => { - const file = files[0]; - if (!file) return; - - const extension = file.name.slice(file.name.lastIndexOf(".") + 1); - const format: Format | undefined = ["kml", "czml", "geojson"].includes(extension) - ? (extension as Format) - : extension == "json" - ? "reearth" - : ["zip", "shp"].includes(extension) - ? "shape" - : undefined; - if (!format) return; - - onLayerImport?.(file, format); - }, - { accept: ".kml,.czml,.geojson,.shp,.zip,.json" }, - ); - - return { - importLayer, - }; -}; diff --git a/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/index.tsx b/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/index.tsx index 85a21dd13..09ff358ad 100644 --- a/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/index.tsx +++ b/src/components/molecules/EarthEditor/LayerTreeViewItem/LayerActions/index.tsx @@ -1,11 +1,12 @@ import React from "react"; +import useFileInput from "use-file-input"; +import { useIntl } from "react-intl"; import { styled } from "@reearth/theme"; -import useHooks from "./hooks"; import HelpButton from "@reearth/components/atoms/HelpButton"; import Icon from "@reearth/components/atoms/Icon"; -import { useIntl } from "react-intl"; -import { Format } from "../Layer"; + +export type Format = "kml" | "czml" | "geojson" | "shape" | "reearth"; export type Props = { rootLayerId?: string; @@ -23,9 +24,25 @@ const LayerActions: React.FC = ({ onLayerGroupCreate, }) => { const intl = useIntl(); - const { importLayer } = useHooks({ - onLayerImport, - }); + const importLayer = useFileInput( + (files: FileList) => { + const file = files[0]; + if (!file) return; + + const extension = file.name.slice(file.name.lastIndexOf(".") + 1); + const format: Format | undefined = ["kml", "czml", "geojson"].includes(extension) + ? (extension as Format) + : extension == "json" + ? "reearth" + : ["zip", "shp"].includes(extension) + ? "shape" + : undefined; + if (!format) return; + + onLayerImport?.(file, format); + }, + { accept: ".kml,.czml,.geojson,.shp,.zip,.json" }, + ); return ( = LayerType; @@ -11,6 +10,7 @@ export type Props = ItemProps> & { className?: string; rootLayerId?: string; selectedLayerId?: string; + visibilityShown?: boolean; onVisibilityChange?: (isVisible: boolean) => void; onRename?: (name: string) => void; onRemove?: (selectedLayerId: string) => void; @@ -33,6 +33,7 @@ function LayerTreeViewItem( canDrop, selectable, siblings, + visibilityShown, onSelect, onExpand, onVisibilityChange, @@ -73,6 +74,7 @@ function LayerTreeViewItem( : undefined } allSiblingsDoesNotHaveChildren={allSiblingsDoesNotHaveChildren} + visibilityShown={visibilityShown} onClick={handleClick} onExpand={onExpand} onVisibilityChange={onVisibilityChange} @@ -115,6 +117,7 @@ export function useLayerTreeViewItem({ rootLayerId, selectedLayerId, className, + visibilityShown, onVisibilityChange, onRename, onRemove, @@ -124,38 +127,51 @@ export function useLayerTreeViewItem({ rootLayerId?: string; selectedLayerId?: string; className?: string; - onVisibilityChange?: (props: Props, isVisible: boolean) => void; - onRename?: (props: Props, name: string) => void; + visibilityShown?: boolean; + onVisibilityChange?: (element: Item>, isVisible: boolean) => void; + onRename?: (element: Item>, name: string) => void; onRemove?: (selectedLayerId: string) => void; onGroupCreate?: () => void; onImport?: (file: File, format: Format) => void; } = {}) { return useMemo(() => { function InnerLayerTreeViewItem(props: Props, ref: Ref) { + const item = props?.item; + const events = useMemo( + () => ({ + onVisibilityChange: item + ? (isVisible: boolean) => onVisibilityChange?.(item, isVisible) + : undefined, + onRename: item ? (name: string) => onRename?.(item, name) : undefined, + }), + [item], + ); + return LayerTreeViewItem( { ...props, rootLayerId, selectedLayerId, className, - onVisibilityChange: (isVisible: boolean) => onVisibilityChange?.(props, isVisible), - onRename: (name: string) => onRename?.(props, name), - onRemove: (selectedLayerId: string) => onRemove?.(selectedLayerId), - onImport: (file: File, format: Format) => onImport?.(file, format), - onGroupCreate: () => onGroupCreate?.(), + visibilityShown, + onRemove, + onGroupCreate, + onImport, + ...events, }, ref, ); } return forwardRef(InnerLayerTreeViewItem); }, [ + rootLayerId, + selectedLayerId, className, - onVisibilityChange, - onRename, + visibilityShown, onRemove, onGroupCreate, onImport, - rootLayerId, - selectedLayerId, + onVisibilityChange, + onRename, ]); } diff --git a/src/components/molecules/EarthEditor/OutlinePane/hooks.tsx b/src/components/molecules/EarthEditor/OutlinePane/hooks.tsx index 3992f4675..20a077508 100644 --- a/src/components/molecules/EarthEditor/OutlinePane/hooks.tsx +++ b/src/components/molecules/EarthEditor/OutlinePane/hooks.tsx @@ -234,20 +234,25 @@ export default ({ ); const layerTreeViewItemOnRename = useCallback( - (props, name) => onLayerRename?.(props.item.id, name), + (item: TreeViewItemType>, name: string) => + onLayerRename?.(item.id, name), [onLayerRename], ); const layerTreeViewItemOnLayerVisibilityChange = useCallback( - (props, visibility) => onLayerVisibilityChange?.(props.item.id, visibility), + (item: TreeViewItemType>, visibility: boolean) => + onLayerVisibilityChange?.(item.id, visibility), [onLayerVisibilityChange], ); - const TreeViewItem = useLayerTreeViewItem({ + const SceneTreeViewItem = useLayerTreeViewItem(); + + const LayerTreeViewItem = useLayerTreeViewItem({ onRename: layerTreeViewItemOnRename, onVisibilityChange: layerTreeViewItemOnLayerVisibilityChange, onRemove: onLayerRemove, onImport: onLayerImport, onGroupCreate: onLayerGroupCreate, + visibilityShown: true, selectedLayerId, rootLayerId, }); @@ -271,7 +276,8 @@ export default ({ drop, dropExternals, removeLayer, - TreeViewItem, + SceneTreeViewItem, + LayerTreeViewItem, selected, }; }; diff --git a/src/components/molecules/EarthEditor/OutlinePane/index.tsx b/src/components/molecules/EarthEditor/OutlinePane/index.tsx index 79ce73ab4..51384a508 100644 --- a/src/components/molecules/EarthEditor/OutlinePane/index.tsx +++ b/src/components/molecules/EarthEditor/OutlinePane/index.tsx @@ -64,26 +64,34 @@ const OutlinePane: React.FC = ({ onDrop, loading, }) => { - const { sceneWidgetsItem, layersItem, select, drop, dropExternals, TreeViewItem, selected } = - useHooks({ - rootLayerId, - layers, - widgets, - sceneDescription, - selectedLayerId, - selectedWidgetId, - selectedType, - onLayerSelect, - onLayerImport, - onLayerRemove, - onSceneSelect, - onWidgetSelect, - onLayerMove, - onLayerRename, - onLayerVisibilityChange, - onDrop, - onLayerGroupCreate, - }); + const { + sceneWidgetsItem, + layersItem, + select, + drop, + dropExternals, + SceneTreeViewItem, + LayerTreeViewItem, + selected, + } = useHooks({ + rootLayerId, + layers, + widgets, + sceneDescription, + selectedLayerId, + selectedWidgetId, + selectedType, + onLayerSelect, + onLayerImport, + onLayerRemove, + onSceneSelect, + onWidgetSelect, + onLayerMove, + onLayerRename, + onLayerVisibilityChange, + onDrop, + onLayerGroupCreate, + }); return ( @@ -92,7 +100,7 @@ const OutlinePane: React.FC = ({ item={sceneWidgetsItem} selected={selected} - renderItem={TreeViewItem} + renderItem={SceneTreeViewItem} draggable droppable selectable @@ -109,7 +117,7 @@ const OutlinePane: React.FC = ({ item={layersItem} selected={selected} - renderItem={TreeViewItem} + renderItem={LayerTreeViewItem} draggable droppable selectable diff --git a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/ColorField/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/ColorField/index.tsx index 681ecce86..8c09a83e8 100644 --- a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/ColorField/index.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/ColorField/index.tsx @@ -1,17 +1,14 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; -import Button from "@reearth/components/atoms/Button"; -import { styled, css, useTheme } from "@reearth/theme"; -import "./styles.css"; - import tinycolor, { ColorInput } from "tinycolor2"; import { useIntl } from "react-intl"; import { usePopper } from "react-popper"; import { RgbaColorPicker } from "react-colorful"; -import "react-colorful/dist/index.css"; -import Text from "@reearth/components/atoms/Text"; +import Button from "@reearth/components/atoms/Button"; +import { styled, css, useTheme, metricsSizes } from "@reearth/theme"; +import Text from "@reearth/components/atoms/Text"; import { FieldProps } from "../types"; -import { metricsSizes } from "@reearth/theme/metrics"; +import "./styles.css"; export type Props = FieldProps; @@ -28,10 +25,12 @@ const getHexString = (value?: ColorInput) => { return color.getAlpha() === 1 ? color.toHexString() : color.toHex8String(); }; +const initColor = tinycolor().toRgb(); + const ColorField: React.FC = ({ value, onChange, overridden, linked }) => { const intl = useIntl(); const [colorState, setColor] = useState(null); - const [rgba, setRgba] = useState(tinycolor().toRgb()); + const [rgba, setRgba] = useState(initColor); const [open, setOpen] = useState(false); const wrapperRef = useRef(null); const pickerRef = useRef(null); @@ -51,7 +50,7 @@ const ColorField: React.FC = ({ value, onChange, overridden, linked }) => if (color != colorState) { setColor(color); } - }, [rgba]); // eslint-disable-line react-hooks/exhaustive-deps + }, [colorState, rgba]); const { styles, attributes } = usePopper(wrapperRef.current, pickerRef.current, { placement: "bottom-start", diff --git a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/URLField/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/URLField/index.tsx index ea443bbb4..a0379e62e 100644 --- a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/URLField/index.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/URLField/index.tsx @@ -30,6 +30,7 @@ const URLField: React.FC = ({ const [isAssetModalOpen, setAssetModalOpen] = useState(false); const openAssetModal = useCallback(() => setAssetModalOpen(true), []); const closeAssetModal = useCallback(() => setAssetModalOpen(false), []); + const deleteValue = useCallback(() => onChange?.(null), [onChange]); return ( @@ -45,7 +46,7 @@ const URLField: React.FC = ({ onClick={openAssetModal} /> {value ? ( - onChange?.(null)} /> + ) : fileType === "image" ? ( ) : fileType === "video" ? ( diff --git a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/index.tsx index 3f7e65494..4b133ed55 100644 --- a/src/components/molecules/EarthEditor/PropertyPane/PropertyField/index.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/PropertyField/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useMemo } from "react"; import { styled } from "@reearth/theme"; import PropertyTitle, { @@ -129,15 +129,16 @@ const PropertyField: React.FC = ({ layers, assets, }) => { - const events = useBind( - { + const rawEvents = useMemo( + () => ({ onClear, onUploadFile, onRemoveFile, onLink, - }, - schema?.id, + }), + [onClear, onUploadFile, onRemoveFile, onLink], ); + const events = useBind(rawEvents, schema?.id); const commonProps: FieldProps = { linked: diff --git a/src/components/atoms/PropertyGroup/index.stories.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyGroup/index.stories.tsx similarity index 77% rename from src/components/atoms/PropertyGroup/index.stories.tsx rename to src/components/molecules/EarthEditor/PropertyPane/PropertyGroup/index.stories.tsx index a02eab526..050b8a30a 100644 --- a/src/components/atoms/PropertyGroup/index.stories.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/PropertyGroup/index.stories.tsx @@ -3,7 +3,7 @@ import { Meta } from "@storybook/react"; import PropertyGroup from "."; export default { - title: "atoms/PropertyGroup", + title: "molecules/EarthEditor/PropertyPane/PropertyGroup", component: PropertyGroup, } as Meta; diff --git a/src/components/atoms/PropertyGroup/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyGroup/index.tsx similarity index 100% rename from src/components/atoms/PropertyGroup/index.tsx rename to src/components/molecules/EarthEditor/PropertyPane/PropertyGroup/index.tsx diff --git a/src/components/molecules/EarthEditor/PropertyPane/PropertyItem/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/PropertyItem/index.tsx index fba0f95d9..ba492f451 100644 --- a/src/components/molecules/EarthEditor/PropertyPane/PropertyItem/index.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/PropertyItem/index.tsx @@ -5,10 +5,9 @@ import { mapValues } from "lodash-es"; import { styled, useTheme } from "@reearth/theme"; import { ExtendedFuncProps2 } from "@reearth/types"; import { useBind } from "@reearth/util/use-bind"; -import { partitionObject } from "@reearth/util/util"; import { zeroValues } from "@reearth/util/value"; -import GroupWrapper from "@reearth/components/atoms/PropertyGroup"; +import GroupWrapper from "../PropertyGroup"; import PropertyList, { Item as PropertyListItem } from "../PropertyList"; import PropertyField, { Props as FieldProps, @@ -128,15 +127,13 @@ const PropertyItem: React.FC = ({ onItemRemove, onItemsUpdate, onRemovePane, + onChange, + onRemove, + onLink, + onUploadFile, + onRemoveFile, ...props }) => { - const [eventProps, otherProps] = partitionObject(props, [ - "onChange", - "onRemove", - "onLink", - "onUploadFile", - "onRemoveFile", - ]); const intl = useIntl(); const theme = useTheme(); @@ -157,6 +154,7 @@ const PropertyItem: React.FC = ({ ); const selectedItem = isList ? groups[selected] : groups[0]; + const selectedItemId = isList ? selectedItem?.id : undefined; const propertyListItems = useMemo( () => groups @@ -190,10 +188,10 @@ const PropertyItem: React.FC = ({ selectedItem ? item?.schemaFields.map(f => { const events = mapValues( - eventProps, + { onChange, onRemove, onLink, onUploadFile, onRemoveFile }, f => (...args: any[]) => - f?.(item.schemaGroup, isList ? selectedItem.id : undefined, ...args), + f?.(item.schemaGroup, selectedItemId, ...args), ); const field = selectedItem?.fields.find(f2 => f2.id === f.id); const condf = f.only && selectedItem?.fields.find(f2 => f2.id === f.only?.field); @@ -211,7 +209,17 @@ const PropertyItem: React.FC = ({ }; }) : [], - [eventProps, item, selectedItem, isList], + [ + item?.schemaFields, + item?.schemaGroup, + onChange, + onLink, + onRemove, + onRemoveFile, + onUploadFile, + selectedItem, + selectedItemId, + ], ); const handleItemMove = useCallback( @@ -232,20 +240,22 @@ const PropertyItem: React.FC = ({ }, [item?.schemaGroup, onItemRemove, propertyListItems], ); - const { onItemsUpdate: handleItemUpdate } = useBind({ onItemsUpdate }, item?.schemaGroup); + const events = useMemo(() => ({ onItemsUpdate }), [onItemsUpdate]); + const { onItemsUpdate: handleItemUpdate } = useBind(events, item?.schemaGroup); const handleItemAdd = useCallback(() => { - if (item) [onItemAdd?.(item.schemaGroup)]; + if (item) onItemAdd?.(item.schemaGroup); }, [onItemAdd, item]); + const itemTitle = intl.formatMessage({ defaultMessage: "Basic" }); const handleDelete = useCallback(() => { if (!onRemovePane || !item?.title) return; - if (item?.title === intl.formatMessage({ defaultMessage: "Basic" })) { + if (item?.title === itemTitle) { setModal(true); } else { onRemovePane(); } - }, [item, onRemovePane, intl]); + }, [item, onRemovePane, itemTitle]); return ( = ({ hidden={f.hidden} isTemplate={isTemplate} {...f.events} - {...otherProps} + {...props} /> ); })} diff --git a/src/components/molecules/EarthEditor/PropertyPane/index.tsx b/src/components/molecules/EarthEditor/PropertyPane/index.tsx index 5bd46e669..8f9ccbf3f 100644 --- a/src/components/molecules/EarthEditor/PropertyPane/index.tsx +++ b/src/components/molecules/EarthEditor/PropertyPane/index.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { useMemo } from "react"; +import { useIntl } from "react-intl"; -import Wrapper from "@reearth/components/atoms/PropertyPane"; -import GroupWrapper from "@reearth/components/atoms/PropertyGroup"; -import Text from "@reearth/components/atoms/Text"; -import Button from "@reearth/components/atoms/Button"; -import { partitionObject } from "@reearth/util/util"; +import { styled, useTheme } from "@reearth/theme"; import { ExtendedFuncProps } from "@reearth/types"; import { useBind } from "@reearth/util/use-bind"; +import Text from "@reearth/components/atoms/Text"; +import Button from "@reearth/components/atoms/Button"; +import WidgetToggleButton from "./WidgetToggleSwitch"; +import GroupWrapper from "./PropertyGroup"; import PropertyItem, { Props as PropertyItemProps, Item as ItemItem, @@ -25,9 +26,6 @@ import PropertyItem, { Asset as AssetType, Mode as ModeType, } from "./PropertyItem"; -import WidgetToggleButton from "./WidgetToggleSwitch"; -import { styled, useTheme } from "@reearth/theme"; -import { useIntl } from "react-intl"; export type Item = ItemItem; export type SchemaField = ItemSchemaField; @@ -103,6 +101,15 @@ const PropertyPane: React.FC = ({ onRemovePane, selectedWidget, onWidgetActivate, + onChange, + onRemove, + onLink, + onUploadFile, + onRemoveFile, + onItemAdd, + onItemMove, + onItemRemove, + onItemsUpdate, ...props }) => { const theme = useTheme(); @@ -115,18 +122,32 @@ const PropertyPane: React.FC = ({ const infoboxCreatable = !propertyId && mode === "infobox" && isInfoboxCreatable; - const [eventProps, otherProps] = partitionObject(props, [ - "onChange", - "onRemove", - "onLink", - "onUploadFile", - "onRemoveFile", - "onItemAdd", - "onItemMove", - "onItemRemove", - "onItemsUpdate", - ]); + const eventProps = useMemo( + () => ({ + onChange, + onRemove, + onLink, + onUploadFile, + onRemoveFile, + onItemAdd, + onItemMove, + onItemRemove, + onItemsUpdate, + }), + [ + onChange, + onRemove, + onLink, + onUploadFile, + onRemoveFile, + onItemAdd, + onItemMove, + onItemRemove, + onItemsUpdate, + ], + ); const events = useBind(eventProps, propertyId); + return ( <> {mode === "widget" && ( @@ -160,8 +181,8 @@ const PropertyPane: React.FC = ({ item={item} onRemovePane={onRemovePane} mode={mode} + {...props} {...events} - {...otherProps} /> ))} @@ -185,6 +206,11 @@ const searchField = ( return; }; +const Wrapper = styled.div` + background: ${props => props.theme.properties}; + margin: 14px 0; +`; + const StyledButton = styled(Button)` float: right; `; diff --git a/src/components/molecules/Visualizer/Widget/Storytelling/hooks.ts b/src/components/molecules/Visualizer/Widget/Storytelling/hooks.ts index 72bd18fc4..7f37353ee 100644 --- a/src/components/molecules/Visualizer/Widget/Storytelling/hooks.ts +++ b/src/components/molecules/Visualizer/Widget/Storytelling/hooks.ts @@ -40,6 +40,8 @@ export default function ({ stories?: Story[]; }) { const [menuOpen, openMenu] = useState(false); + const toggleMenu = useCallback(() => openMenu(o => !o), []); + const [selected, select] = useState<{ index: number; @@ -139,10 +141,12 @@ export default function ({ return; } + const p = selected.primitive?.property?.default; + const position = { - lat: selected.primitive?.property?.default?.location?.lat as number | undefined, - lng: selected.primitive?.property?.default?.location?.lng as number | undefined, - height: (selected.primitive?.property?.default?.height as number | undefined) ?? 0, + lat: (p?.location?.lat ?? p?.position?.lat) as number | undefined, + lng: (p?.location?.lng ?? p?.position?.lng) as number | undefined, + height: (p?.height as number | undefined) ?? 0, }; if (typeof position.lat !== "number" && typeof position.lng !== "number") return; @@ -174,6 +178,7 @@ export default function ({ handlePrev, selectAt, openMenu, + toggleMenu, }; } diff --git a/src/components/molecules/Visualizer/Widget/Storytelling/index.tsx b/src/components/molecules/Visualizer/Widget/Storytelling/index.tsx index c7fcad632..00f612df5 100644 --- a/src/components/molecules/Visualizer/Widget/Storytelling/index.tsx +++ b/src/components/molecules/Visualizer/Widget/Storytelling/index.tsx @@ -32,13 +32,14 @@ const Storytelling = ({ widget }: Props): JSX.Element | null => { const { camera, duration, autoStart, range } = (widget?.property as Property | undefined)?.default ?? {}; - const { stories, menuOpen, selected, handleNext, handlePrev, selectAt, openMenu } = useHooks({ - camera, - autoStart, - range, - duration, - stories: storiesData, - }); + const { stories, menuOpen, selected, handleNext, handlePrev, selectAt, openMenu, toggleMenu } = + useHooks({ + camera, + autoStart, + range, + duration, + stories: storiesData, + }); const wrapperRef = useRef(null); useClickAway(wrapperRef, () => { @@ -81,7 +82,7 @@ const Storytelling = ({ widget }: Props): JSX.Element | null => { - openMenu(o => !o)} menuOpen={menuOpen} /> + {selected?.story.title} diff --git a/src/components/molecules/Visualizer/index.tsx b/src/components/molecules/Visualizer/index.tsx index 25bb89a46..46099ff7a 100644 --- a/src/components/molecules/Visualizer/index.tsx +++ b/src/components/molecules/Visualizer/index.tsx @@ -21,6 +21,7 @@ export type Infobox = { export type Primitive = PrimitiveType & { infoboxEditable?: boolean; pluginProperty?: any; + hidden?: boolean; }; export type Widget = WidgetType & { @@ -110,19 +111,21 @@ export default function Visualizer({ {...props} camera={innerCamera} onCameraChange={updateCamera}> - {primitives?.map(primitive => ( -

- ))} + {primitives?.map(primitive => + primitive.hidden ? null : ( +

+ ), + )} {widgets?.map(widget => ( { return ( - l?.reduce((a, b) => { + l?.reduce((a, { layers, ...b }) => { if (!b || !b.isVisible) { return a; } - if (b.layers?.length) { - return [...a, ...flattenLayers(b.layers)]; + if (layers?.length) { + return [...a, { ...b, hiddden: true }, ...flattenLayers(layers)]; } if (!b.pluginId || !b.extensionId) return a; return [...a, b]; diff --git a/src/config.ts b/src/config.ts index f74db3567..1548e120d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,7 +21,7 @@ declare global { export const defaultConfig: Config = { api: "/api", plugins: "/plugins", - published: "/published.html?alias={}", + published: window.origin + "/p/{}", }; export default async function loadConfig() { diff --git a/src/theme/index.ts b/src/theme/index.ts index 5116c9f34..55807fa2e 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -3,7 +3,7 @@ export * from "./styled"; export { default as colors } from "./colors"; export { default as styles } from "./styles"; export { default as fonts } from "./fonts"; -export { default as metrics } from "./metrics"; +export { default as metrics, metricsSizes } from "./metrics"; export { default } from "./darkTheme"; export { default as Provider } from "./provider"; export { default as PublishedAppProvider } from "./publishedAppProvider"; diff --git a/src/util/use-bind.ts b/src/util/use-bind.ts index a495f8198..99a33e97d 100644 --- a/src/util/use-bind.ts +++ b/src/util/use-bind.ts @@ -7,10 +7,13 @@ export const useBind =

an p: P, a?: A, ): { [K in keyof P]?: OmitFunc> } => { - return mapValues(p, f => { - // eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps - return useMemo(() => (f && isPresent(a) ? (...args) => f(a, ...args) : undefined), [f, a]); - }); + return useMemo( + () => + mapValues(p, f => { + return f && isPresent(a) ? (...args) => f(a, ...args) : undefined; + }), + [a, p], + ); }; export const useBind2 =

any }, A, B>( @@ -18,36 +21,33 @@ export const useBind2 =

> } => { - return mapValues(p, f => { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useMemo( - () => (f && isPresent(a) && isPresent(b) ? (...args) => f(a, b, ...args) : undefined), - // eslint-disable-next-line react-hooks/exhaustive-deps - [f, a, b], - ); - }); + return useMemo( + () => + mapValues(p, f => { + return f && isPresent(a) && isPresent(b) ? (...args) => f(a, b, ...args) : undefined; + }), + [a, b, p], + ); }; export const useBind3 = < P extends { [key in string]?: (a: A, b: B, ...args: any) => any }, A, B, - C + C, >( p: P, a?: A, b?: B, c?: C, ): { [K in keyof P]?: OmitFunc3> } => { - return mapValues(p, f => { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useMemo( - () => - f && isPresent(a) && isPresent(b) && isPresent(c) + return useMemo( + () => + mapValues(p, f => { + return f && isPresent(a) && isPresent(b) && isPresent(c) ? (...args) => f(a, b, c, ...args) - : undefined, - // eslint-disable-next-line react-hooks/exhaustive-deps - [f, a, b, c], - ); - }); + : undefined; + }), + [p, a, b, c], + ); }; diff --git a/src/util/use-double-click.ts b/src/util/use-double-click.ts index 01efecbe7..62f2a970e 100644 --- a/src/util/use-double-click.ts +++ b/src/util/use-double-click.ts @@ -1,18 +1,20 @@ import { useCallback, useRef } from "react"; const useDoubleClick = ( - onClick: () => void, + onClick: (() => void) | undefined, onDoubleClick: (() => void) | undefined, ): [() => void, () => void] => { const t = useRef(); const handleClick = useCallback(() => { - t.current && clearTimeout(t.current); - t.current = setTimeout(onClick, 200); + if (t.current) clearTimeout(t.current); + if (onClick) { + t.current = setTimeout(onClick, 200); + } }, [onClick]); const handleDoubleClick = useCallback(() => { - t.current && clearTimeout(t.current); + if (t.current) clearTimeout(t.current); onDoubleClick?.(); }, [onDoubleClick]); diff --git a/webpack.config.js b/webpack.config.js index cf102f1c6..f8fbee893 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,6 +51,7 @@ module.exports = (env, args = {}) => { app.get("/reearth_config.json", (_req, res) => { res.json({ api: "http://localhost:8080/api", + published: "/published.html?alias={}", ...Object.fromEntries(Object.entries(config).filter(([, v]) => Boolean(v))), }); }); diff --git a/yarn.lock b/yarn.lock index 561b95c82..2ad84d25c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15365,17 +15365,12 @@ react-clientside-effect@^1.2.2: dependencies: "@babel/runtime" "^7.12.13" -react-colorful@^4.4.4: - version "4.4.4" - resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-4.4.4.tgz#38e7c5b7075bbf63d3cce22d8c61a439a58b7561" - integrity sha512-01V2/6rr6sa1vaZntWZJXZxnU7ew02NG2rqq0eoVp4d3gFU5Ug9lDzNMbr+8ns0byXsJbBR8LbwQTlAjz6x7Kg== - react-colorful@^5.0.1: version "5.1.4" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.1.4.tgz#7391568db7c0a4163436bfb076e5da8ef394e87c" integrity sha512-WOEpRNz8Oo2SEU4eYQ279jEKFSjpFPa9Vi2U/K0DGwP9wOQ8wYkJcNSd5Qbv1L8OFvyKDCbWekjftXaU5mbmtg== -react-colorful@^5.1.2: +react-colorful@^5.1.2, react-colorful@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.3.0.tgz#bcbae49c1affa9ab9a3c8063398c5948419296bd" integrity sha512-zWE5E88zmjPXFhv6mGnRZqKin9s5vip1O3IIGynY9EhZxN8MATUxZkT3e/9OwTEm4DjQBXc6PFWP6AetY+Px+A==