Skip to content

Commit

Permalink
chore(web): story page change on user scroll (#671)
Browse files Browse the repository at this point in the history
  • Loading branch information
KaWaite authored Sep 8, 2023
1 parent b3c7e8c commit e03a0a9
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 166 deletions.
130 changes: 76 additions & 54 deletions web/src/beta/features/Editor/StoryPanel/Page/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import { Fragment } from "react";
import { Fragment, useMemo } from "react";

import { Item } from "@reearth/services/api/propertyApi/utils";
import { convert } from "@reearth/services/api/propertyApi/utils";
import { InstallableStoryBlock } from "@reearth/services/api/storytellingApi/blocks";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";

import StoryBlock from "../Block";
import { StoryPageFragmentFragment } from "../hooks";
import SelectableArea from "../SelectableArea";

import BlockAddBar from "./BlockAddBar";
import useHooks from "./hooks";

type Props = {
sceneId?: string;
storyId?: string;
pageId?: string;
propertyId?: string;
propertyItems?: Item[];
page?: StoryPageFragmentFragment;
selectedPageId?: string;
installableStoryBlocks?: InstallableStoryBlock[];
selectedStoryBlockId?: string;
showPageSettings?: boolean;
onPageSettingsToggle?: () => void;
onPageSelect?: (pageId?: string | undefined) => void;
onBlockSelect: (blockId?: string) => void;
};

const StoryPage: React.FC<Props> = ({
sceneId,
storyId,
pageId,
propertyId,
propertyItems,
page,
selectedPageId,
installableStoryBlocks,
selectedStoryBlockId,
showPageSettings,
onPageSettingsToggle,
onPageSelect,
onBlockSelect,
}) => {
const t = useT();
const propertyItems = useMemo(() => convert(page?.property), [page?.property]);

const {
openBlocksIndex,
installedStoryBlocks,
Expand All @@ -42,57 +52,69 @@ const StoryPage: React.FC<Props> = ({
} = useHooks({
sceneId,
storyId,
pageId,
pageId: page?.id,
propertyItems,
});

return (
<Wrapper id={pageId}>
{titleProperty && (
<StoryBlock
block={{
id: titleId,
pluginId: "reearth",
extensionId: "titleStoryBlock",
title: titleProperty.title,
property: {
id: propertyId ?? "",
items: [titleProperty],
},
}}
isSelected={selectedStoryBlockId === titleId}
onClick={() => onBlockSelect(titleId)}
onClickAway={onBlockSelect}
onChange={handlePropertyValueUpdate}
<SelectableArea
title={page?.title ?? t("Page")}
position="left-bottom"
icon="storyPage"
noBorder
isSelected={selectedPageId === page?.id}
propertyId={page?.property?.id}
propertyItems={propertyItems}
onClick={() => onPageSelect?.(page?.id)}
showSettings={showPageSettings}
onSettingsToggle={onPageSettingsToggle}>
<Wrapper id={page?.id}>
{titleProperty && (
<StoryBlock
block={{
id: titleId,
pluginId: "reearth",
extensionId: "titleStoryBlock",
title: titleProperty.title,
property: {
id: page?.property?.id ?? "",
items: [titleProperty],
},
}}
isSelected={selectedStoryBlockId === titleId}
onClick={() => onBlockSelect(titleId)}
onClickAway={onBlockSelect}
onChange={handlePropertyValueUpdate}
/>
)}
<BlockAddBar
openBlocks={openBlocksIndex === -1}
installableStoryBlocks={installableStoryBlocks}
onBlockOpen={() => handleBlockOpen(-1)}
onBlockAdd={handleStoryBlockCreate(0)}
/>
)}
<BlockAddBar
openBlocks={openBlocksIndex === -1}
installableStoryBlocks={installableStoryBlocks}
onBlockOpen={() => handleBlockOpen(-1)}
onBlockAdd={handleStoryBlockCreate(0)}
/>
{installedStoryBlocks &&
installedStoryBlocks.length > 0 &&
installedStoryBlocks.map((b, idx) => (
<Fragment key={idx}>
<StoryBlock
block={b}
isSelected={selectedStoryBlockId === b.id}
onClick={() => onBlockSelect(b.id)}
onClickAway={onBlockSelect}
onChange={handlePropertyValueUpdate}
onRemove={handleStoryBlockDelete}
/>
<BlockAddBar
openBlocks={openBlocksIndex === idx}
installableStoryBlocks={installableStoryBlocks}
onBlockOpen={() => handleBlockOpen(idx)}
onBlockAdd={handleStoryBlockCreate(idx + 1)}
/>
</Fragment>
))}
</Wrapper>
{installedStoryBlocks &&
installedStoryBlocks.length > 0 &&
installedStoryBlocks.map((b, idx) => (
<Fragment key={idx}>
<StoryBlock
block={b}
isSelected={selectedStoryBlockId === b.id}
onClick={() => onBlockSelect(b.id)}
onClickAway={onBlockSelect}
onChange={handlePropertyValueUpdate}
onRemove={handleStoryBlockDelete}
/>
<BlockAddBar
openBlocks={openBlocksIndex === idx}
installableStoryBlocks={installableStoryBlocks}
onBlockOpen={() => handleBlockOpen(idx)}
onBlockAdd={handleStoryBlockCreate(idx + 1)}
/>
</Fragment>
))}
</Wrapper>
</SelectableArea>
);
};

Expand Down
145 changes: 145 additions & 0 deletions web/src/beta/features/Editor/StoryPanel/PanelContent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Fragment, useEffect, useMemo, useRef } from "react";

import { InstallableStoryBlock } from "@reearth/services/api/storytellingApi/blocks";
import { styled } from "@reearth/services/theme";

import { StoryPageFragmentFragment } from "../hooks";
import StoryPage from "../Page";

export const pagesElementId = "story-page-content";

export type Props = {
sceneId?: string;
storyId?: string;
pages?: StoryPageFragmentFragment[];
selectedPageId?: string;
installableStoryBlocks?: InstallableStoryBlock[];
selectedStoryBlockId?: string;
showPageSettings?: boolean;
showingIndicator?: boolean;
isAutoScrolling?: boolean;
onAutoScrollingChange: (isScrolling: boolean) => void;
onPageSettingsToggle?: () => void;
onPageSelect?: (pageId?: string | undefined) => void;
onBlockSelect: (blockId?: string) => void;
onCurrentPageChange?: (pageId: string) => void;
};

const StoryContent: React.FC<Props> = ({
sceneId,
storyId,
pages,
selectedPageId,
installableStoryBlocks,
selectedStoryBlockId,
showPageSettings,
showingIndicator,
isAutoScrolling,
onAutoScrollingChange,
onPageSettingsToggle,
onPageSelect,
onBlockSelect,
onCurrentPageChange,
}) => {
const scrollRef = useRef<number | undefined>(undefined);
const scrollTimeoutRef = useRef<NodeJS.Timeout>();

const pageHeight = useMemo(() => {
const element = document.getElementById(pagesElementId);
return element?.clientHeight;
}, []);

useEffect(() => {
const ids = pages?.map(p => p.id) as string[];
const panelContentElement = document.getElementById(pagesElementId);

const observer = new IntersectionObserver(
entries => {
if (isAutoScrolling) return; // to avoid conflicts with page selection in editor UI
entries.forEach(entry => {
const id = entry.target.getAttribute("id") ?? "";
if (selectedPageId === id) return;

const diff = (scrollRef.current as number) - (panelContentElement?.scrollTop as number);
const isScrollingUp = diff > 0;

if (entry.isIntersecting) {
onCurrentPageChange?.(id);
scrollRef.current = panelContentElement?.scrollTop;
return;
}
const currentIndex = ids?.indexOf(id) as number;
const prevEntry = ids[currentIndex - 1];
if (isScrollingUp) {
const id = prevEntry;
onCurrentPageChange?.(id);
}
});
},
{
root: panelContentElement,
threshold: 0.2,
},
);
ids?.forEach(id => {
const e = document.getElementById(id);
if (e) {
observer.observe(e);
}
});
return () => {
ids?.forEach(id => {
const e = document.getElementById(id);
if (e) {
observer.unobserve(e);
}
});
};
}, [pages, selectedPageId, isAutoScrolling, onCurrentPageChange]);

useEffect(() => {
const wrapperElement = document.getElementById(pagesElementId);
if (isAutoScrolling) {
wrapperElement?.addEventListener("scroll", () => {
clearTimeout(scrollTimeoutRef.current);
scrollTimeoutRef.current = setTimeout(function () {
onAutoScrollingChange(false);
}, 100);
});
}
}, [isAutoScrolling, onAutoScrollingChange]);

return (
<PagesWrapper id={pagesElementId} showingIndicator={showingIndicator}>
{pages?.map(p => (
<Fragment key={p.id}>
<StoryPage
sceneId={sceneId}
storyId={storyId}
page={p}
selectedPageId={selectedPageId}
installableStoryBlocks={installableStoryBlocks}
selectedStoryBlockId={selectedStoryBlockId}
showPageSettings={showPageSettings}
onPageSettingsToggle={onPageSettingsToggle}
onPageSelect={onPageSelect}
onBlockSelect={onBlockSelect}
/>
<PageGap height={pageHeight} />
</Fragment>
))}
</PagesWrapper>
);
};

export default StoryContent;

const PagesWrapper = styled.div<{ showingIndicator?: boolean }>`
height: ${({ showingIndicator }) => (showingIndicator ? "calc(100% - 8px)" : "100%")};
overflow-y: auto;
cursor: pointer;
`;

const PageGap = styled.div<{ height?: number }>`
height: ${({ height }) => (height ? height + "px" : "70vh")};
`;
30 changes: 12 additions & 18 deletions web/src/beta/features/Editor/StoryPanel/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";

import useStorytellingAPI from "@reearth/services/api/storytellingApi";
import type { StoryFragmentFragment, StoryPageFragmentFragment } from "@reearth/services/gql";

export type { StoryFragmentFragment, StoryPageFragmentFragment } from "@reearth/services/gql";

export const pageElementId = "story-page";

export default ({
sceneId,
selectedStory,
currentPage,
onPageSelect,
onCurrentPageChange,
}: {
sceneId?: string;
selectedStory?: StoryFragmentFragment;
currentPage?: StoryPageFragmentFragment;
onPageSelect: (id: string) => void;
onCurrentPageChange: (id: string, disableScrollIntoView?: boolean) => void;
}) => {
const [showPageSettings, setShowPageSettings] = useState(false);
const [selectedPageId, setSelectedPageId] = useState<string>();
Expand Down Expand Up @@ -51,11 +49,12 @@ export default ({

const { installableStoryBlocks } = useInstallableStoryBlocksQuery({ sceneId });

useEffect(() => {
if (currentPage) {
document.getElementById(currentPage.id)?.scrollIntoView({ behavior: "smooth" });
}
}, [currentPage]);
const handleCurrentPageChange = useCallback(
(pageId: string) => {
onCurrentPageChange(pageId, true); // true disables scrollIntoView
},
[onCurrentPageChange],
);

const pageInfo = useMemo(() => {
const pages = selectedStory?.pages ?? [];
Expand All @@ -65,24 +64,19 @@ export default ({
return {
currentPage: currentIndex + 1,
maxPage: pages.length,
onPageChange: (page: number) => onPageSelect(pages[page - 1]?.id),
onPageChange: (pageIndex: number) => onCurrentPageChange(pages[pageIndex - 1]?.id),
};
}, [onPageSelect, currentPage, selectedStory]);

const pageHeight = useMemo(() => {
const element = document.getElementById(pageElementId);
return element?.clientHeight;
}, []);
}, [selectedStory, currentPage, onCurrentPageChange]);

return {
pageInfo,
pageHeight,
installableBlocks: installableStoryBlocks,
selectedPageId,
selectedBlockId,
showPageSettings,
handlePageSettingsToggle,
handlePageSelect,
handleBlockSelect,
handleCurrentPageChange,
};
};
Loading

0 comments on commit e03a0a9

Please sign in to comment.