added ability to set open info panel state by default
This commit is contained in:
parent
e14db16290
commit
e5ad49d07b
@ -32,6 +32,7 @@ const InfoPanelSettingsSection: React.FC<InfoPanelSettingsSectionProps> = ({
|
|||||||
infoPanelTriggerLabel,
|
infoPanelTriggerLabel,
|
||||||
infoPanelTriggerFontFamily,
|
infoPanelTriggerFontFamily,
|
||||||
infoPanelDisabled,
|
infoPanelDisabled,
|
||||||
|
infoPanelOpenByDefault,
|
||||||
// Header section
|
// Header section
|
||||||
infoPanelHeaderImageUrl,
|
infoPanelHeaderImageUrl,
|
||||||
infoPanelHeaderText,
|
infoPanelHeaderText,
|
||||||
@ -186,6 +187,19 @@ const InfoPanelSettingsSection: React.FC<InfoPanelSettingsSectionProps> = ({
|
|||||||
Disable this info panel
|
Disable this info panel
|
||||||
</label>
|
</label>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Default State'>
|
||||||
|
<label className='inline-flex items-center gap-2 text-sm'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={infoPanelOpenByDefault}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange('infoPanelOpenByDefault', e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Open by default
|
||||||
|
</label>
|
||||||
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -400,6 +400,22 @@ const InfoPanelSettingsSectionCompact: React.FC<
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className='flex items-center gap-2 text-[11px] font-medium text-white/70'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={element.infoPanelOpenByDefault || false}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange('infoPanelOpenByDefault', e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Open by default
|
||||||
|
</label>
|
||||||
|
<p className='mt-1 text-[10px] text-white/50'>
|
||||||
|
Opens the panel when this page renders
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className='mb-1 block text-[11px] font-medium text-white/70'>
|
<label className='mb-1 block text-[11px] font-medium text-white/70'>
|
||||||
Icon
|
Icon
|
||||||
|
|||||||
@ -269,6 +269,7 @@ export interface InfoPanelSettingsSectionProps {
|
|||||||
infoPanelTriggerLabel: string;
|
infoPanelTriggerLabel: string;
|
||||||
infoPanelTriggerFontFamily: string;
|
infoPanelTriggerFontFamily: string;
|
||||||
infoPanelDisabled: boolean;
|
infoPanelDisabled: boolean;
|
||||||
|
infoPanelOpenByDefault: boolean;
|
||||||
// Header section
|
// Header section
|
||||||
infoPanelHeaderImageUrl?: string;
|
infoPanelHeaderImageUrl?: string;
|
||||||
infoPanelHeaderText?: string;
|
infoPanelHeaderText?: string;
|
||||||
|
|||||||
@ -150,6 +150,7 @@ interface FormState {
|
|||||||
infoPanelTriggerLabel: string;
|
infoPanelTriggerLabel: string;
|
||||||
infoPanelTriggerFontFamily: string;
|
infoPanelTriggerFontFamily: string;
|
||||||
infoPanelDisabled: boolean;
|
infoPanelDisabled: boolean;
|
||||||
|
infoPanelOpenByDefault: boolean;
|
||||||
panelTitle: string;
|
panelTitle: string;
|
||||||
panelText: string;
|
panelText: string;
|
||||||
panelXPercent: string;
|
panelXPercent: string;
|
||||||
@ -309,6 +310,7 @@ const initialState: FormState = {
|
|||||||
infoPanelTriggerLabel: '',
|
infoPanelTriggerLabel: '',
|
||||||
infoPanelTriggerFontFamily: '',
|
infoPanelTriggerFontFamily: '',
|
||||||
infoPanelDisabled: false,
|
infoPanelDisabled: false,
|
||||||
|
infoPanelOpenByDefault: false,
|
||||||
panelTitle: '',
|
panelTitle: '',
|
||||||
panelText: '',
|
panelText: '',
|
||||||
panelXPercent: '0',
|
panelXPercent: '0',
|
||||||
@ -521,6 +523,7 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
|||||||
settings.infoPanelTriggerFontFamily || '',
|
settings.infoPanelTriggerFontFamily || '',
|
||||||
),
|
),
|
||||||
infoPanelDisabled: Boolean(settings.infoPanelDisabled),
|
infoPanelDisabled: Boolean(settings.infoPanelDisabled),
|
||||||
|
infoPanelOpenByDefault: Boolean(settings.infoPanelOpenByDefault),
|
||||||
panelTitle: String(settings.panelTitle || ''),
|
panelTitle: String(settings.panelTitle || ''),
|
||||||
panelText: String(settings.panelText || ''),
|
panelText: String(settings.panelText || ''),
|
||||||
panelXPercent: String(settings.panelXPercent ?? 0),
|
panelXPercent: String(settings.panelXPercent ?? 0),
|
||||||
@ -1067,6 +1070,7 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
|
|||||||
settings.infoPanelTriggerFontFamily =
|
settings.infoPanelTriggerFontFamily =
|
||||||
state.infoPanelTriggerFontFamily.trim();
|
state.infoPanelTriggerFontFamily.trim();
|
||||||
settings.infoPanelDisabled = state.infoPanelDisabled;
|
settings.infoPanelDisabled = state.infoPanelDisabled;
|
||||||
|
settings.infoPanelOpenByDefault = state.infoPanelOpenByDefault;
|
||||||
|
|
||||||
// Panel content
|
// Panel content
|
||||||
settings.panelTitle = state.panelTitle.trim();
|
settings.panelTitle = state.panelTitle.trim();
|
||||||
|
|||||||
@ -72,6 +72,7 @@ import type {
|
|||||||
InfoPanelImage,
|
InfoPanelImage,
|
||||||
} from '../types/constructor';
|
} from '../types/constructor';
|
||||||
import {
|
import {
|
||||||
|
findDefaultOpenInfoPanelElements,
|
||||||
isElementFlagEnabled,
|
isElementFlagEnabled,
|
||||||
isInfoPanelElementType,
|
isInfoPanelElementType,
|
||||||
} from '../lib/elementDefaults';
|
} from '../lib/elementDefaults';
|
||||||
@ -219,19 +220,20 @@ export default function RuntimePresentation({
|
|||||||
element: CanvasElement;
|
element: CanvasElement;
|
||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [activeInfoPanel, setActiveInfoPanel] = useState<CanvasElement | null>(
|
const [activeInfoPanelIds, setActiveInfoPanelIds] = useState<string[]>([]);
|
||||||
null,
|
const [activeDetailImages, setActiveDetailImages] = useState<
|
||||||
);
|
Record<string, InfoPanelImage | undefined>
|
||||||
const [activeDetailImage, setActiveDetailImage] =
|
>({});
|
||||||
useState<InfoPanelImage | null>(null);
|
|
||||||
const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
|
const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
|
||||||
|
panelId: string;
|
||||||
items: GalleryCarouselMediaItem[];
|
items: GalleryCarouselMediaItem[];
|
||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
// Track selected image in media section (runtime-only local state)
|
// Track selected image in media section (runtime-only local state)
|
||||||
const [runtimeSelectedImageId, setRuntimeSelectedImageId] = useState<
|
const [runtimeSelectedImageIds, setRuntimeSelectedImageIds] = useState<
|
||||||
string | null
|
Record<string, string | null>
|
||||||
>(null);
|
>({});
|
||||||
|
const defaultInfoPanelPageIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const transitionVideoRef = useRef<HTMLVideoElement>(null);
|
const transitionVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
const lastInitializedPageIdRef = useRef<string | null>(null);
|
const lastInitializedPageIdRef = useRef<string | null>(null);
|
||||||
@ -472,6 +474,37 @@ export default function RuntimePresentation({
|
|||||||
}
|
}
|
||||||
}, [selectedPage]);
|
}, [selectedPage]);
|
||||||
|
|
||||||
|
const activeInfoPanelElements = useMemo(
|
||||||
|
() =>
|
||||||
|
activeInfoPanelIds
|
||||||
|
.map((panelId) =>
|
||||||
|
pageElements.find((element) => element.id === panelId),
|
||||||
|
)
|
||||||
|
.filter((element): element is CanvasElement => Boolean(element)),
|
||||||
|
[activeInfoPanelIds, pageElements],
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeInfoPanelGalleryElement = activeInfoPanelGallery
|
||||||
|
? pageElements.find(
|
||||||
|
(element) => element.id === activeInfoPanelGallery.panelId,
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPage) return;
|
||||||
|
if (!selectedPageId || defaultInfoPanelPageIdRef.current === selectedPageId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
defaultInfoPanelPageIdRef.current = selectedPageId;
|
||||||
|
const defaultOpenInfoPanels =
|
||||||
|
findDefaultOpenInfoPanelElements(pageElements);
|
||||||
|
|
||||||
|
setActiveInfoPanelIds(defaultOpenInfoPanels.map((element) => element.id));
|
||||||
|
setActiveDetailImages({});
|
||||||
|
setActiveInfoPanelGallery(null);
|
||||||
|
setRuntimeSelectedImageIds({});
|
||||||
|
}, [pageElements, selectedPage, selectedPageId]);
|
||||||
|
|
||||||
// Set initial backgrounds when page first loads (before preload cache is populated)
|
// Set initial backgrounds when page first loads (before preload cache is populated)
|
||||||
// The condition ensures this only runs once on initial load when backgrounds are empty.
|
// The condition ensures this only runs once on initial load when backgrounds are empty.
|
||||||
// After that, navigateToPage handles all subsequent navigation explicitly.
|
// After that, navigateToPage handles all subsequent navigation explicitly.
|
||||||
@ -626,10 +659,10 @@ export default function RuntimePresentation({
|
|||||||
const targetPage = pages.find((page) => page.slug === targetPageSlug);
|
const targetPage = pages.find((page) => page.slug === targetPageSlug);
|
||||||
if (!targetPage) return;
|
if (!targetPage) return;
|
||||||
|
|
||||||
setActiveInfoPanel(null);
|
setActiveInfoPanelIds([]);
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages({});
|
||||||
setActiveInfoPanelGallery(null);
|
setActiveInfoPanelGallery(null);
|
||||||
setRuntimeSelectedImageId(null);
|
setRuntimeSelectedImageIds({});
|
||||||
navigateToPage(targetPage.id);
|
navigateToPage(targetPage.id);
|
||||||
},
|
},
|
||||||
[navigateToPage, pages, transitionPhase, isBuffering],
|
[navigateToPage, pages, transitionPhase, isBuffering],
|
||||||
@ -643,7 +676,7 @@ export default function RuntimePresentation({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInfoPanelUseAsBackground = useCallback(
|
const handleInfoPanelUseAsBackground = useCallback(
|
||||||
(item: InfoPanelImage) => {
|
(panelId: string, item: InfoPanelImage) => {
|
||||||
const mediaType =
|
const mediaType =
|
||||||
item.itemType === 'video'
|
item.itemType === 'video'
|
||||||
? 'video'
|
? 'video'
|
||||||
@ -670,14 +703,20 @@ export default function RuntimePresentation({
|
|||||||
: '',
|
: '',
|
||||||
navCurrentBgAudioUrl,
|
navCurrentBgAudioUrl,
|
||||||
);
|
);
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages((current) => {
|
||||||
setActiveInfoPanelGallery(null);
|
const next = { ...current };
|
||||||
|
delete next[panelId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setActiveInfoPanelGallery((current) =>
|
||||||
|
current?.panelId === panelId ? null : current,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[navCurrentBgAudioUrl, navSetBackgroundDirectly],
|
[navCurrentBgAudioUrl, navSetBackgroundDirectly],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInfoPanelOpenGallery = useCallback(
|
const handleInfoPanelOpenGallery = useCallback(
|
||||||
(items: InfoPanelImage[], initialIndex: number) => {
|
(panelId: string, items: InfoPanelImage[], initialIndex: number) => {
|
||||||
const activeItemId = items[initialIndex]?.id;
|
const activeItemId = items[initialIndex]?.id;
|
||||||
const galleryItems = items
|
const galleryItems = items
|
||||||
.map<GalleryCarouselMediaItem | null>((item) => {
|
.map<GalleryCarouselMediaItem | null>((item) => {
|
||||||
@ -704,8 +743,13 @@ export default function RuntimePresentation({
|
|||||||
.filter((item): item is GalleryCarouselMediaItem => Boolean(item));
|
.filter((item): item is GalleryCarouselMediaItem => Boolean(item));
|
||||||
|
|
||||||
if (galleryItems.length === 0) return;
|
if (galleryItems.length === 0) return;
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[panelId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
setActiveInfoPanelGallery({
|
setActiveInfoPanelGallery({
|
||||||
|
panelId,
|
||||||
items: galleryItems,
|
items: galleryItems,
|
||||||
initialIndex: Math.max(
|
initialIndex: Math.max(
|
||||||
0,
|
0,
|
||||||
@ -738,8 +782,14 @@ export default function RuntimePresentation({
|
|||||||
|
|
||||||
// Handle info panel click
|
// Handle info panel click
|
||||||
if (isInfoPanelElementType(element.type)) {
|
if (isInfoPanelElementType(element.type)) {
|
||||||
setActiveInfoPanel(element);
|
setActiveInfoPanelIds((current) =>
|
||||||
setActiveDetailImage(null);
|
current.includes(element.id) ? current : [...current, element.id],
|
||||||
|
);
|
||||||
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[element.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1090,7 +1140,7 @@ export default function RuntimePresentation({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
isInfoPanelOpen={activeInfoPanel?.id === element.id}
|
isInfoPanelOpen={activeInfoPanelIds.includes(element.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1184,82 +1234,154 @@ export default function RuntimePresentation({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeInfoPanelGallery && (
|
{activeInfoPanelGallery && activeInfoPanelGalleryElement && (
|
||||||
<GalleryCarouselOverlay
|
<GalleryCarouselOverlay
|
||||||
cards={activeInfoPanelGallery.items}
|
cards={activeInfoPanelGallery.items}
|
||||||
initialIndex={activeInfoPanelGallery.initialIndex}
|
initialIndex={activeInfoPanelGallery.initialIndex}
|
||||||
onClose={() => setActiveInfoPanelGallery(null)}
|
onClose={() => setActiveInfoPanelGallery(null)}
|
||||||
resolveUrl={resolveUrlWithBlob}
|
resolveUrl={resolveUrlWithBlob}
|
||||||
prevIconUrl={activeInfoPanel?.galleryCarouselPrevIconUrl}
|
prevIconUrl={
|
||||||
nextIconUrl={activeInfoPanel?.galleryCarouselNextIconUrl}
|
activeInfoPanelGalleryElement.galleryCarouselPrevIconUrl
|
||||||
backIconUrl={activeInfoPanel?.galleryCarouselBackIconUrl}
|
}
|
||||||
backLabel={activeInfoPanel?.galleryCarouselBackLabel || 'BACK'}
|
nextIconUrl={
|
||||||
prevX={activeInfoPanel?.galleryCarouselPrevX}
|
activeInfoPanelGalleryElement.galleryCarouselNextIconUrl
|
||||||
prevY={activeInfoPanel?.galleryCarouselPrevY}
|
}
|
||||||
nextX={activeInfoPanel?.galleryCarouselNextX}
|
backIconUrl={
|
||||||
nextY={activeInfoPanel?.galleryCarouselNextY}
|
activeInfoPanelGalleryElement.galleryCarouselBackIconUrl
|
||||||
backX={activeInfoPanel?.galleryCarouselBackX}
|
}
|
||||||
backY={activeInfoPanel?.galleryCarouselBackY}
|
backLabel={
|
||||||
prevWidth={activeInfoPanel?.galleryCarouselPrevWidth}
|
activeInfoPanelGalleryElement.galleryCarouselBackLabel ||
|
||||||
prevHeight={activeInfoPanel?.galleryCarouselPrevHeight}
|
'BACK'
|
||||||
nextWidth={activeInfoPanel?.galleryCarouselNextWidth}
|
}
|
||||||
nextHeight={activeInfoPanel?.galleryCarouselNextHeight}
|
prevX={activeInfoPanelGalleryElement.galleryCarouselPrevX}
|
||||||
backWidth={activeInfoPanel?.galleryCarouselBackWidth}
|
prevY={activeInfoPanelGalleryElement.galleryCarouselPrevY}
|
||||||
backHeight={activeInfoPanel?.galleryCarouselBackHeight}
|
nextX={activeInfoPanelGalleryElement.galleryCarouselNextX}
|
||||||
|
nextY={activeInfoPanelGalleryElement.galleryCarouselNextY}
|
||||||
|
backX={activeInfoPanelGalleryElement.galleryCarouselBackX}
|
||||||
|
backY={activeInfoPanelGalleryElement.galleryCarouselBackY}
|
||||||
|
prevWidth={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselPrevWidth
|
||||||
|
}
|
||||||
|
prevHeight={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselPrevHeight
|
||||||
|
}
|
||||||
|
nextWidth={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselNextWidth
|
||||||
|
}
|
||||||
|
nextHeight={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselNextHeight
|
||||||
|
}
|
||||||
|
backWidth={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselBackWidth
|
||||||
|
}
|
||||||
|
backHeight={
|
||||||
|
activeInfoPanelGalleryElement.galleryCarouselBackHeight
|
||||||
|
}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
isEditMode={false}
|
isEditMode={false}
|
||||||
pageTransitionSettings={transitionSettings}
|
pageTransitionSettings={transitionSettings}
|
||||||
galleryElement={activeInfoPanel || undefined}
|
galleryElement={activeInfoPanelGalleryElement}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Info Panel Overlay */}
|
{/* Info Panel Overlay */}
|
||||||
{activeInfoPanel && (
|
{activeInfoPanelElements.map((activeInfoPanel, panelIndex) => {
|
||||||
<>
|
const selectedImageId =
|
||||||
<InfoPanelOverlay
|
runtimeSelectedImageIds[activeInfoPanel.id];
|
||||||
element={
|
const panelDetailImage = activeDetailImages[activeInfoPanel.id];
|
||||||
runtimeSelectedImageId
|
|
||||||
? {
|
return (
|
||||||
...activeInfoPanel,
|
<React.Fragment key={activeInfoPanel.id}>
|
||||||
infoPanelSelectedImageId: runtimeSelectedImageId,
|
<InfoPanelOverlay
|
||||||
}
|
element={
|
||||||
: activeInfoPanel
|
selectedImageId
|
||||||
}
|
? {
|
||||||
onClose={() => {
|
...activeInfoPanel,
|
||||||
setActiveInfoPanel(null);
|
infoPanelSelectedImageId: selectedImageId,
|
||||||
setActiveDetailImage(null);
|
}
|
||||||
setActiveInfoPanelGallery(null);
|
: activeInfoPanel
|
||||||
setRuntimeSelectedImageId(null);
|
}
|
||||||
}}
|
onClose={() => {
|
||||||
resolveUrl={resolveUrlWithBlob}
|
setActiveInfoPanelIds((current) =>
|
||||||
letterboxStyles={letterboxStyles}
|
current.filter(
|
||||||
cssVars={cssVars}
|
(panelId) => panelId !== activeInfoPanel.id,
|
||||||
onImageClick={(image) => setActiveDetailImage(image)}
|
),
|
||||||
onOpenGallery={handleInfoPanelOpenGallery}
|
);
|
||||||
onUseAsBackground={handleInfoPanelUseAsBackground}
|
setActiveDetailImages((current) => {
|
||||||
onSelectImage={(imageId) =>
|
const next = { ...current };
|
||||||
setRuntimeSelectedImageId(imageId)
|
delete next[activeInfoPanel.id];
|
||||||
}
|
return next;
|
||||||
onNavigateToPage={handleInfoPanelNavigateToPage}
|
});
|
||||||
onOpenExternalUrl={handleInfoPanelOpenExternalUrl}
|
setActiveInfoPanelGallery((current) =>
|
||||||
active360ItemId={
|
current?.panelId === activeInfoPanel.id
|
||||||
activeDetailImage?.itemType === '360'
|
? null
|
||||||
? activeDetailImage.id
|
: current,
|
||||||
: null
|
);
|
||||||
}
|
setRuntimeSelectedImageIds((current) => {
|
||||||
/>
|
const next = { ...current };
|
||||||
{activeDetailImage && (
|
delete next[activeInfoPanel.id];
|
||||||
<ImageDetailPanel
|
return next;
|
||||||
element={activeInfoPanel}
|
});
|
||||||
image={activeDetailImage}
|
}}
|
||||||
onClose={() => setActiveDetailImage(null)}
|
|
||||||
resolveUrl={resolveUrlWithBlob}
|
resolveUrl={resolveUrlWithBlob}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
cssVars={cssVars}
|
cssVars={cssVars}
|
||||||
|
renderBackdrop={panelIndex === 0}
|
||||||
|
onBackdropClose={() => {
|
||||||
|
setActiveInfoPanelIds([]);
|
||||||
|
setActiveDetailImages({});
|
||||||
|
setActiveInfoPanelGallery(null);
|
||||||
|
setRuntimeSelectedImageIds({});
|
||||||
|
}}
|
||||||
|
onImageClick={(image) =>
|
||||||
|
setActiveDetailImages((current) => ({
|
||||||
|
...current,
|
||||||
|
[activeInfoPanel.id]: image,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onOpenGallery={(items, initialIndex) =>
|
||||||
|
handleInfoPanelOpenGallery(
|
||||||
|
activeInfoPanel.id,
|
||||||
|
items,
|
||||||
|
initialIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onUseAsBackground={(item) =>
|
||||||
|
handleInfoPanelUseAsBackground(activeInfoPanel.id, item)
|
||||||
|
}
|
||||||
|
onSelectImage={(imageId) =>
|
||||||
|
setRuntimeSelectedImageIds((current) => ({
|
||||||
|
...current,
|
||||||
|
[activeInfoPanel.id]: imageId,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onNavigateToPage={handleInfoPanelNavigateToPage}
|
||||||
|
onOpenExternalUrl={handleInfoPanelOpenExternalUrl}
|
||||||
|
active360ItemId={
|
||||||
|
panelDetailImage?.itemType === '360'
|
||||||
|
? panelDetailImage.id
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
{panelDetailImage && (
|
||||||
</>
|
<ImageDetailPanel
|
||||||
)}
|
element={activeInfoPanel}
|
||||||
|
image={panelDetailImage}
|
||||||
|
onClose={() =>
|
||||||
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[activeInfoPanel.id];
|
||||||
|
return next;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resolveUrl={resolveUrlWithBlob}
|
||||||
|
letterboxStyles={letterboxStyles}
|
||||||
|
cssVars={cssVars}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</BackdropPortalProvider>
|
</BackdropPortalProvider>
|
||||||
</div>
|
</div>
|
||||||
{/* End inner canvas container */}
|
{/* End inner canvas container */}
|
||||||
|
|||||||
@ -53,6 +53,10 @@ interface InfoPanelOverlayProps {
|
|||||||
onOpenExternalUrl?: (url: string) => void;
|
onOpenExternalUrl?: (url: string) => void;
|
||||||
/** Callback when a media item should replace the current screen background */
|
/** Callback when a media item should replace the current screen background */
|
||||||
onUseAsBackground?: (image: InfoPanelImage) => void;
|
onUseAsBackground?: (image: InfoPanelImage) => void;
|
||||||
|
/** Whether this overlay instance should render the fullscreen backdrop layer */
|
||||||
|
renderBackdrop?: boolean;
|
||||||
|
/** Optional close handler for backdrop clicks; defaults to onClose */
|
||||||
|
onBackdropClose?: () => void;
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean;
|
||||||
/** Callback when panel position changes (edit mode only) */
|
/** Callback when panel position changes (edit mode only) */
|
||||||
onPanelPositionChange?: (xPercent: number, yPercent: number) => void;
|
onPanelPositionChange?: (xPercent: number, yPercent: number) => void;
|
||||||
@ -140,6 +144,8 @@ const InfoPanelOverlay: React.FC<InfoPanelOverlayProps> = ({
|
|||||||
onNavigateToPage,
|
onNavigateToPage,
|
||||||
onOpenExternalUrl,
|
onOpenExternalUrl,
|
||||||
onUseAsBackground,
|
onUseAsBackground,
|
||||||
|
renderBackdrop = true,
|
||||||
|
onBackdropClose,
|
||||||
isEditMode = false,
|
isEditMode = false,
|
||||||
onPanelPositionChange,
|
onPanelPositionChange,
|
||||||
active360ItemId,
|
active360ItemId,
|
||||||
@ -371,20 +377,20 @@ const InfoPanelOverlay: React.FC<InfoPanelOverlayProps> = ({
|
|||||||
const handleBackdropClick = useCallback(
|
const handleBackdropClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (e.target === overlayRef.current && !isEditMode) {
|
if (e.target === overlayRef.current && !isEditMode) {
|
||||||
onClose();
|
(onBackdropClose || onClose)();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onClose, isEditMode],
|
[onBackdropClose, onClose, isEditMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle touch on backdrop
|
// Handle touch on backdrop
|
||||||
const handleBackdropTouch = useCallback(
|
const handleBackdropTouch = useCallback(
|
||||||
(e: React.TouchEvent) => {
|
(e: React.TouchEvent) => {
|
||||||
if (e.target === overlayRef.current && !isEditMode) {
|
if (e.target === overlayRef.current && !isEditMode) {
|
||||||
onClose();
|
(onBackdropClose || onClose)();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onClose, isEditMode],
|
[onBackdropClose, onClose, isEditMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extract panel styling from element
|
// Extract panel styling from element
|
||||||
@ -462,16 +468,20 @@ const InfoPanelOverlay: React.FC<InfoPanelOverlayProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop overlay - separate from panel for correct z-index stacking */}
|
{/* Backdrop overlay - separate from panel for correct z-index stacking */}
|
||||||
<div
|
{renderBackdrop && (
|
||||||
ref={overlayRef}
|
<div
|
||||||
className='fixed inset-0 z-[51] overflow-hidden'
|
ref={overlayRef}
|
||||||
style={{
|
className='fixed inset-0 z-[51] overflow-hidden'
|
||||||
backgroundColor: isOverlayVisible ? panelOverlayColor : 'transparent',
|
style={{
|
||||||
pointerEvents: isEditMode ? 'none' : 'auto',
|
backgroundColor: isOverlayVisible
|
||||||
}}
|
? panelOverlayColor
|
||||||
onClick={handleBackdropClick}
|
: 'transparent',
|
||||||
onTouchEnd={handleBackdropTouch}
|
pointerEvents: isEditMode ? 'none' : 'auto',
|
||||||
/>
|
}}
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
onTouchEnd={handleBackdropTouch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Inner container constrained to canvas bounds */}
|
{/* Inner container constrained to canvas bounds */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -142,6 +142,19 @@ export const isElementFlagEnabled = (value: unknown): boolean =>
|
|||||||
value === true ||
|
value === true ||
|
||||||
(typeof value === 'string' && value.trim().toLowerCase() === 'true');
|
(typeof value === 'string' && value.trim().toLowerCase() === 'true');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all enabled Info Panel elements configured to open with the page.
|
||||||
|
*/
|
||||||
|
export const findDefaultOpenInfoPanelElements = (
|
||||||
|
elements: CanvasElement[],
|
||||||
|
): CanvasElement[] =>
|
||||||
|
elements.filter(
|
||||||
|
(element) =>
|
||||||
|
isInfoPanelElementType(element.type) &&
|
||||||
|
isElementFlagEnabled(element.infoPanelOpenByDefault) &&
|
||||||
|
!isElementFlagEnabled(element.infoPanelDisabled),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize appearDelaySec value
|
* Normalize appearDelaySec value
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -64,6 +64,7 @@ import {
|
|||||||
normalizeAppearDurationSec,
|
normalizeAppearDurationSec,
|
||||||
ELEMENT_TYPE_LABELS,
|
ELEMENT_TYPE_LABELS,
|
||||||
getNavigationButtonKind,
|
getNavigationButtonKind,
|
||||||
|
findDefaultOpenInfoPanelElements,
|
||||||
isElementFlagEnabled,
|
isElementFlagEnabled,
|
||||||
isNavigationElementType,
|
isNavigationElementType,
|
||||||
isDescriptionElementType,
|
isDescriptionElementType,
|
||||||
@ -320,12 +321,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
// Info panel overlay state
|
// Info panel overlay state
|
||||||
const [activeInfoPanel, setActiveInfoPanel] = useState<{
|
const [activeInfoPanelIds, setActiveInfoPanelIds] = useState<string[]>([]);
|
||||||
elementId: string;
|
const defaultInfoPanelPageIdRef = useRef<string | null>(null);
|
||||||
} | null>(null);
|
const elementsPageIdRef = useRef<string | null>(null);
|
||||||
const [activeDetailImage, setActiveDetailImage] =
|
const [activeDetailImages, setActiveDetailImages] = useState<
|
||||||
useState<InfoPanelImage | null>(null);
|
Record<string, InfoPanelImage | undefined>
|
||||||
|
>({});
|
||||||
const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
|
const [activeInfoPanelGallery, setActiveInfoPanelGallery] = useState<{
|
||||||
|
panelId: string;
|
||||||
items: GalleryCarouselMediaItem[];
|
items: GalleryCarouselMediaItem[];
|
||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
@ -430,11 +433,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
);
|
);
|
||||||
}, [activeGalleryCarousel, elements]);
|
}, [activeGalleryCarousel, elements]);
|
||||||
|
|
||||||
// Look up current element for info panel (so it receives updates from element editor)
|
// Look up current elements for info panels (so they receive updates).
|
||||||
const activeInfoPanelElement = useMemo(() => {
|
const activeInfoPanelElements = useMemo(
|
||||||
if (!activeInfoPanel) return null;
|
() =>
|
||||||
return elements.find((el) => el.id === activeInfoPanel.elementId) || null;
|
activeInfoPanelIds
|
||||||
}, [activeInfoPanel, elements]);
|
.map((panelId) => elements.find((element) => element.id === panelId))
|
||||||
|
.filter((element): element is CanvasElement => Boolean(element)),
|
||||||
|
[activeInfoPanelIds, elements],
|
||||||
|
);
|
||||||
|
|
||||||
// In edit mode, show overlay when info_panel element is selected (even without click)
|
// In edit mode, show overlay when info_panel element is selected (even without click)
|
||||||
const editModeInfoPanelElement = useMemo(() => {
|
const editModeInfoPanelElement = useMemo(() => {
|
||||||
@ -445,18 +451,42 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
|
|
||||||
// In edit mode the overlay is only a selected-element preview. Do not keep
|
// In edit mode the overlay is only a selected-element preview. Do not keep
|
||||||
// runtime-open Info Panel state above the canvas after selection is cleared.
|
// runtime-open Info Panel state above the canvas after selection is cleared.
|
||||||
const infoPanelElementToRender = isConstructorEditMode
|
const infoPanelElementsToRender = isConstructorEditMode
|
||||||
? editModeInfoPanelElement
|
? editModeInfoPanelElement
|
||||||
: activeInfoPanelElement;
|
? [editModeInfoPanelElement]
|
||||||
const shouldShowInfoPanelOverlays = !!infoPanelElementToRender;
|
: []
|
||||||
|
: activeInfoPanelElements;
|
||||||
|
const shouldShowInfoPanelOverlays = infoPanelElementsToRender.length > 0;
|
||||||
|
const activeInfoPanelGalleryElement = activeInfoPanelGallery
|
||||||
|
? elements.find(
|
||||||
|
(element) => element.id === activeInfoPanelGallery.panelId,
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
// Reset info panel state when switching between edit and interact modes
|
// Reset info panel state when switching between edit and interact modes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveInfoPanel(null);
|
setActiveInfoPanelIds([]);
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages({});
|
||||||
setActiveInfoPanelGallery(null);
|
setActiveInfoPanelGallery(null);
|
||||||
}, [isConstructorEditMode]);
|
}, [isConstructorEditMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConstructorEditMode) {
|
||||||
|
defaultInfoPanelPageIdRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (elementsPageIdRef.current !== activePageId) return;
|
||||||
|
if (!activePageId || defaultInfoPanelPageIdRef.current === activePageId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
defaultInfoPanelPageIdRef.current = activePageId;
|
||||||
|
const defaultOpenInfoPanels = findDefaultOpenInfoPanelElements(elements);
|
||||||
|
|
||||||
|
setActiveInfoPanelIds(defaultOpenInfoPanels.map((element) => element.id));
|
||||||
|
setActiveDetailImages({});
|
||||||
|
setActiveInfoPanelGallery(null);
|
||||||
|
}, [activePageId, elements, isConstructorEditMode]);
|
||||||
|
|
||||||
// Draggable panels using useDraggable hook
|
// Draggable panels using useDraggable hook
|
||||||
const { position: toolbarPosition, onDragStart: onToolbarDragStart } =
|
const { position: toolbarPosition, onDragStart: onToolbarDragStart } =
|
||||||
useDraggable({
|
useDraggable({
|
||||||
@ -668,8 +698,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
pages.find((page) => page.slug === targetPageSlug) || null;
|
pages.find((page) => page.slug === targetPageSlug) || null;
|
||||||
if (!targetPage) return;
|
if (!targetPage) return;
|
||||||
|
|
||||||
setActiveInfoPanel(null);
|
setActiveInfoPanelIds([]);
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages({});
|
||||||
setActiveInfoPanelGallery(null);
|
setActiveInfoPanelGallery(null);
|
||||||
switchToPage(targetPage).then(() => {
|
switchToPage(targetPage).then(() => {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
@ -688,7 +718,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleInfoPanelUseAsBackground = useCallback(
|
const handleInfoPanelUseAsBackground = useCallback(
|
||||||
(item: InfoPanelImage) => {
|
(panelId: string, item: InfoPanelImage) => {
|
||||||
const mediaType =
|
const mediaType =
|
||||||
item.itemType === 'video'
|
item.itemType === 'video'
|
||||||
? 'video'
|
? 'video'
|
||||||
@ -715,14 +745,20 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
: '',
|
: '',
|
||||||
navCurrentBgAudioUrl,
|
navCurrentBgAudioUrl,
|
||||||
);
|
);
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages((current) => {
|
||||||
setActiveInfoPanelGallery(null);
|
const next = { ...current };
|
||||||
|
delete next[panelId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setActiveInfoPanelGallery((current) =>
|
||||||
|
current?.panelId === panelId ? null : current,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[navCurrentBgAudioUrl, navSetBackgroundDirectly],
|
[navCurrentBgAudioUrl, navSetBackgroundDirectly],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInfoPanelOpenGallery = useCallback(
|
const handleInfoPanelOpenGallery = useCallback(
|
||||||
(items: InfoPanelImage[], initialIndex: number) => {
|
(panelId: string, items: InfoPanelImage[], initialIndex: number) => {
|
||||||
const activeItemId = items[initialIndex]?.id;
|
const activeItemId = items[initialIndex]?.id;
|
||||||
const galleryItems = items
|
const galleryItems = items
|
||||||
.map<GalleryCarouselMediaItem | null>((item) => {
|
.map<GalleryCarouselMediaItem | null>((item) => {
|
||||||
@ -749,8 +785,13 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
.filter((item): item is GalleryCarouselMediaItem => Boolean(item));
|
.filter((item): item is GalleryCarouselMediaItem => Boolean(item));
|
||||||
|
|
||||||
if (galleryItems.length === 0) return;
|
if (galleryItems.length === 0) return;
|
||||||
setActiveDetailImage(null);
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[panelId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
setActiveInfoPanelGallery({
|
setActiveInfoPanelGallery({
|
||||||
|
panelId,
|
||||||
items: galleryItems,
|
items: galleryItems,
|
||||||
initialIndex: Math.max(
|
initialIndex: Math.max(
|
||||||
0,
|
0,
|
||||||
@ -1102,13 +1143,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
if (createdPage?.id) {
|
if (createdPage?.id) {
|
||||||
setSuccessMessage('Page duplicated.');
|
setSuccessMessage('Page duplicated.');
|
||||||
}
|
}
|
||||||
}, [
|
}, [activePage, activePageId, duplicatePage, existingSlugs, saveConstructor]);
|
||||||
activePage,
|
|
||||||
activePageId,
|
|
||||||
duplicatePage,
|
|
||||||
existingSlugs,
|
|
||||||
saveConstructor,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleShowDeletePageModal = useCallback(() => {
|
const handleShowDeletePageModal = useCallback(() => {
|
||||||
if (!activePageId || !activePage) {
|
if (!activePageId || !activePage) {
|
||||||
@ -1135,7 +1170,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
const currentIndex = sortedPages.findIndex(
|
const currentIndex = sortedPages.findIndex(
|
||||||
(page) => page.id === activePageId,
|
(page) => page.id === activePageId,
|
||||||
);
|
);
|
||||||
const remainingPages = sortedPages.filter((page) => page.id !== activePageId);
|
const remainingPages = sortedPages.filter(
|
||||||
|
(page) => page.id !== activePageId,
|
||||||
|
);
|
||||||
const fallbackPage =
|
const fallbackPage =
|
||||||
currentIndex >= 0
|
currentIndex >= 0
|
||||||
? remainingPages[Math.min(currentIndex, remainingPages.length - 1)]
|
? remainingPages[Math.min(currentIndex, remainingPages.length - 1)]
|
||||||
@ -1226,6 +1263,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activePage) {
|
if (!activePage) {
|
||||||
|
elementsPageIdRef.current = null;
|
||||||
setElements([]);
|
setElements([]);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
updateBackgroundFromPage(null);
|
updateBackgroundFromPage(null);
|
||||||
@ -1480,6 +1518,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
elementsPageIdRef.current = activePage.id;
|
||||||
setElements(normalizedElements);
|
setElements(normalizedElements);
|
||||||
setSelectedMenuItem('none');
|
setSelectedMenuItem('none');
|
||||||
|
|
||||||
@ -1831,8 +1870,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
(element: CanvasElement) => {
|
(element: CanvasElement) => {
|
||||||
if (isConstructorEditMode) return;
|
if (isConstructorEditMode) return;
|
||||||
if (isInfoPanelElementType(element.type)) {
|
if (isInfoPanelElementType(element.type)) {
|
||||||
setActiveInfoPanel({ elementId: element.id });
|
setActiveInfoPanelIds((current) =>
|
||||||
setActiveDetailImage(null);
|
current.includes(element.id) ? current : [...current, element.id],
|
||||||
|
);
|
||||||
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[element.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isConstructorEditMode],
|
[isConstructorEditMode],
|
||||||
@ -2385,9 +2430,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onInfoPanelClick={() => handleInfoPanelClick(element)}
|
onInfoPanelClick={() => handleInfoPanelClick(element)}
|
||||||
isInfoPanelOpen={
|
isInfoPanelOpen={activeInfoPanelIds.includes(
|
||||||
activeInfoPanel?.elementId === element.id
|
element.id,
|
||||||
}
|
)}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
pageTransitionSettings={transitionSettings}
|
pageTransitionSettings={transitionSettings}
|
||||||
preloadCache={{
|
preloadCache={{
|
||||||
@ -2493,105 +2538,153 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeInfoPanelGallery && (
|
{activeInfoPanelGallery && activeInfoPanelGalleryElement && (
|
||||||
<GalleryCarouselOverlay
|
<GalleryCarouselOverlay
|
||||||
cards={activeInfoPanelGallery.items}
|
cards={activeInfoPanelGallery.items}
|
||||||
initialIndex={activeInfoPanelGallery.initialIndex}
|
initialIndex={activeInfoPanelGallery.initialIndex}
|
||||||
onClose={() => setActiveInfoPanelGallery(null)}
|
onClose={() => setActiveInfoPanelGallery(null)}
|
||||||
resolveUrl={resolveUrlWithBlob}
|
resolveUrl={resolveUrlWithBlob}
|
||||||
prevIconUrl={infoPanelElementToRender?.galleryCarouselPrevIconUrl}
|
prevIconUrl={activeInfoPanelGalleryElement.galleryCarouselPrevIconUrl}
|
||||||
nextIconUrl={infoPanelElementToRender?.galleryCarouselNextIconUrl}
|
nextIconUrl={activeInfoPanelGalleryElement.galleryCarouselNextIconUrl}
|
||||||
backIconUrl={infoPanelElementToRender?.galleryCarouselBackIconUrl}
|
backIconUrl={activeInfoPanelGalleryElement.galleryCarouselBackIconUrl}
|
||||||
backLabel={
|
backLabel={
|
||||||
infoPanelElementToRender?.galleryCarouselBackLabel || 'BACK'
|
activeInfoPanelGalleryElement.galleryCarouselBackLabel || 'BACK'
|
||||||
}
|
}
|
||||||
prevX={infoPanelElementToRender?.galleryCarouselPrevX}
|
prevX={activeInfoPanelGalleryElement.galleryCarouselPrevX}
|
||||||
prevY={infoPanelElementToRender?.galleryCarouselPrevY}
|
prevY={activeInfoPanelGalleryElement.galleryCarouselPrevY}
|
||||||
nextX={infoPanelElementToRender?.galleryCarouselNextX}
|
nextX={activeInfoPanelGalleryElement.galleryCarouselNextX}
|
||||||
nextY={infoPanelElementToRender?.galleryCarouselNextY}
|
nextY={activeInfoPanelGalleryElement.galleryCarouselNextY}
|
||||||
backX={infoPanelElementToRender?.galleryCarouselBackX}
|
backX={activeInfoPanelGalleryElement.galleryCarouselBackX}
|
||||||
backY={infoPanelElementToRender?.galleryCarouselBackY}
|
backY={activeInfoPanelGalleryElement.galleryCarouselBackY}
|
||||||
prevWidth={infoPanelElementToRender?.galleryCarouselPrevWidth}
|
prevWidth={activeInfoPanelGalleryElement.galleryCarouselPrevWidth}
|
||||||
prevHeight={infoPanelElementToRender?.galleryCarouselPrevHeight}
|
prevHeight={activeInfoPanelGalleryElement.galleryCarouselPrevHeight}
|
||||||
nextWidth={infoPanelElementToRender?.galleryCarouselNextWidth}
|
nextWidth={activeInfoPanelGalleryElement.galleryCarouselNextWidth}
|
||||||
nextHeight={infoPanelElementToRender?.galleryCarouselNextHeight}
|
nextHeight={activeInfoPanelGalleryElement.galleryCarouselNextHeight}
|
||||||
backWidth={infoPanelElementToRender?.galleryCarouselBackWidth}
|
backWidth={activeInfoPanelGalleryElement.galleryCarouselBackWidth}
|
||||||
backHeight={infoPanelElementToRender?.galleryCarouselBackHeight}
|
backHeight={activeInfoPanelGalleryElement.galleryCarouselBackHeight}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
isEditMode={false}
|
isEditMode={false}
|
||||||
pageTransitionSettings={transitionSettings}
|
pageTransitionSettings={transitionSettings}
|
||||||
galleryElement={infoPanelElementToRender || undefined}
|
galleryElement={activeInfoPanelGalleryElement}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Info Panel Overlay */}
|
{/* Info Panel Overlay */}
|
||||||
{shouldShowInfoPanelOverlays && infoPanelElementToRender && (
|
{shouldShowInfoPanelOverlays &&
|
||||||
<>
|
infoPanelElementsToRender.map(
|
||||||
<InfoPanelOverlay
|
(infoPanelElementToRender, panelIndex) => {
|
||||||
element={infoPanelElementToRender}
|
const panelDetailImage =
|
||||||
onClose={() => {
|
activeDetailImages[infoPanelElementToRender.id];
|
||||||
setActiveInfoPanel(null);
|
|
||||||
setActiveDetailImage(null);
|
return (
|
||||||
setActiveInfoPanelGallery(null);
|
<React.Fragment key={infoPanelElementToRender.id}>
|
||||||
}}
|
<InfoPanelOverlay
|
||||||
resolveUrl={resolveUrlWithBlob}
|
element={infoPanelElementToRender}
|
||||||
letterboxStyles={letterboxStyles}
|
onClose={() => {
|
||||||
cssVars={canvasCssVars}
|
setActiveInfoPanelIds((current) =>
|
||||||
onImageClick={(image) => setActiveDetailImage(image)}
|
current.filter(
|
||||||
onOpenGallery={handleInfoPanelOpenGallery}
|
(panelId) => panelId !== infoPanelElementToRender.id,
|
||||||
onUseAsBackground={handleInfoPanelUseAsBackground}
|
),
|
||||||
onNavigateToPage={handleInfoPanelNavigateToPage}
|
);
|
||||||
onOpenExternalUrl={handleInfoPanelOpenExternalUrl}
|
setActiveDetailImages((current) => {
|
||||||
onSelectImage={
|
const next = { ...current };
|
||||||
isConstructorEditMode
|
delete next[infoPanelElementToRender.id];
|
||||||
? (imageId) => {
|
return next;
|
||||||
updateSelectedElement({
|
|
||||||
infoPanelSelectedImageId: imageId,
|
|
||||||
});
|
});
|
||||||
|
setActiveInfoPanelGallery((current) =>
|
||||||
|
current?.panelId === infoPanelElementToRender.id
|
||||||
|
? null
|
||||||
|
: current,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
resolveUrl={resolveUrlWithBlob}
|
||||||
|
letterboxStyles={letterboxStyles}
|
||||||
|
cssVars={canvasCssVars}
|
||||||
|
renderBackdrop={panelIndex === 0}
|
||||||
|
onBackdropClose={() => {
|
||||||
|
setActiveInfoPanelIds([]);
|
||||||
|
setActiveDetailImages({});
|
||||||
|
setActiveInfoPanelGallery(null);
|
||||||
|
}}
|
||||||
|
onImageClick={(image) =>
|
||||||
|
setActiveDetailImages((current) => ({
|
||||||
|
...current,
|
||||||
|
[infoPanelElementToRender.id]: image,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
: undefined
|
onOpenGallery={(items, initialIndex) =>
|
||||||
}
|
handleInfoPanelOpenGallery(
|
||||||
isEditMode={isConstructorEditMode}
|
infoPanelElementToRender.id,
|
||||||
onPanelPositionChange={
|
items,
|
||||||
isConstructorEditMode
|
initialIndex,
|
||||||
? (xPercent, yPercent) => {
|
)
|
||||||
updateSelectedElement({
|
|
||||||
panelXPercent: xPercent,
|
|
||||||
panelYPercent: yPercent,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
: undefined
|
onUseAsBackground={(item) =>
|
||||||
}
|
handleInfoPanelUseAsBackground(
|
||||||
active360ItemId={
|
infoPanelElementToRender.id,
|
||||||
activeDetailImage?.itemType === '360'
|
item,
|
||||||
? activeDetailImage.id
|
)
|
||||||
: null
|
}
|
||||||
}
|
onNavigateToPage={handleInfoPanelNavigateToPage}
|
||||||
/>
|
onOpenExternalUrl={handleInfoPanelOpenExternalUrl}
|
||||||
{/* In edit mode, always show detail panel (with placeholder if no image selected) */}
|
onSelectImage={
|
||||||
{(activeDetailImage || isConstructorEditMode) && (
|
isConstructorEditMode
|
||||||
<ImageDetailPanel
|
? (imageId) => {
|
||||||
element={infoPanelElementToRender}
|
updateSelectedElement({
|
||||||
image={activeDetailImage}
|
infoPanelSelectedImageId: imageId,
|
||||||
onClose={() => setActiveDetailImage(null)}
|
});
|
||||||
resolveUrl={resolveUrlWithBlob}
|
}
|
||||||
letterboxStyles={letterboxStyles}
|
: undefined
|
||||||
cssVars={canvasCssVars}
|
}
|
||||||
isEditMode={isConstructorEditMode}
|
isEditMode={isConstructorEditMode}
|
||||||
onDetailPositionChange={
|
onPanelPositionChange={
|
||||||
isConstructorEditMode
|
isConstructorEditMode
|
||||||
? (xPercent, yPercent) => {
|
? (xPercent, yPercent) => {
|
||||||
updateSelectedElement({
|
updateSelectedElement({
|
||||||
detailXPercent: xPercent,
|
panelXPercent: xPercent,
|
||||||
detailYPercent: yPercent,
|
panelYPercent: yPercent,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
active360ItemId={
|
||||||
|
panelDetailImage?.itemType === '360'
|
||||||
|
? panelDetailImage.id
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* In edit mode, always show detail panel (with placeholder if no image selected) */}
|
||||||
|
{(panelDetailImage || isConstructorEditMode) && (
|
||||||
|
<ImageDetailPanel
|
||||||
|
element={infoPanelElementToRender}
|
||||||
|
image={panelDetailImage}
|
||||||
|
onClose={() =>
|
||||||
|
setActiveDetailImages((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[infoPanelElementToRender.id];
|
||||||
|
return next;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
: undefined
|
resolveUrl={resolveUrlWithBlob}
|
||||||
}
|
letterboxStyles={letterboxStyles}
|
||||||
/>
|
cssVars={canvasCssVars}
|
||||||
)}
|
isEditMode={isConstructorEditMode}
|
||||||
</>
|
onDetailPositionChange={
|
||||||
)}
|
isConstructorEditMode
|
||||||
|
? (xPercent, yPercent) => {
|
||||||
|
updateSelectedElement({
|
||||||
|
detailXPercent: xPercent,
|
||||||
|
detailYPercent: yPercent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Create Page Modal */}
|
{/* Create Page Modal */}
|
||||||
<CreatePageModal
|
<CreatePageModal
|
||||||
|
|||||||
@ -394,6 +394,7 @@ const ElementTypeDefaultDetailsPage = () => {
|
|||||||
form.state.infoPanelTriggerFontFamily
|
form.state.infoPanelTriggerFontFamily
|
||||||
}
|
}
|
||||||
infoPanelDisabled={form.state.infoPanelDisabled}
|
infoPanelDisabled={form.state.infoPanelDisabled}
|
||||||
|
infoPanelOpenByDefault={form.state.infoPanelOpenByDefault}
|
||||||
// Header section
|
// Header section
|
||||||
infoPanelHeaderImageUrl={
|
infoPanelHeaderImageUrl={
|
||||||
form.state.infoPanelHeaderImageUrl
|
form.state.infoPanelHeaderImageUrl
|
||||||
|
|||||||
@ -593,6 +593,7 @@ const ProjectElementDefaultDetailsPage = () => {
|
|||||||
form.state.infoPanelTriggerFontFamily
|
form.state.infoPanelTriggerFontFamily
|
||||||
}
|
}
|
||||||
infoPanelDisabled={form.state.infoPanelDisabled}
|
infoPanelDisabled={form.state.infoPanelDisabled}
|
||||||
|
infoPanelOpenByDefault={form.state.infoPanelOpenByDefault}
|
||||||
// Header section
|
// Header section
|
||||||
infoPanelHeaderImageUrl={form.state.infoPanelHeaderImageUrl}
|
infoPanelHeaderImageUrl={form.state.infoPanelHeaderImageUrl}
|
||||||
infoPanelHeaderText={form.state.infoPanelHeaderText}
|
infoPanelHeaderText={form.state.infoPanelHeaderText}
|
||||||
|
|||||||
@ -472,6 +472,7 @@ export interface CanvasElement extends BaseCanvasElement {
|
|||||||
infoPanelTriggerLabel?: string;
|
infoPanelTriggerLabel?: string;
|
||||||
infoPanelTriggerFontFamily?: string;
|
infoPanelTriggerFontFamily?: string;
|
||||||
infoPanelDisabled?: boolean;
|
infoPanelDisabled?: boolean;
|
||||||
|
infoPanelOpenByDefault?: boolean;
|
||||||
|
|
||||||
// Component 2: Info Panel (section-based like Gallery)
|
// Component 2: Info Panel (section-based like Gallery)
|
||||||
// Header section
|
// Header section
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user