UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

361 lines (359 loc) 12.5 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { FormattedMessage } from 'react-intl'; import { isBlank } from '../../utils/string'; import React, { useEffect, useState, useRef, useMemo } from 'react'; import { makeStyles } from 'tss-react/mui'; import { useDispatch } from 'react-redux'; import IconButton from '@mui/material/IconButton'; import RefreshRounded from '@mui/icons-material/RefreshRounded'; import Paper from '@mui/material/Paper'; import ItemDisplay from '../ItemDisplay'; import PagesSearchAhead from '../PagesSearchAhead'; import SingleItemSelector from '../SingleItemSelector'; import MoreRounded from '@mui/icons-material/MoreVertRounded'; import { getOffsetLeft, getOffsetTop } from '@mui/material/Popover'; import { withIndex, withoutIndex } from '../../utils/path'; import { showItemMegaMenu } from '../../state/actions/dialogs'; import { getNumOfMenuOptionsForItem } from '../../utils/content'; import Tooltip from '@mui/material/Tooltip'; import { changeCurrentUrl, reloadRequest } from '../../state/actions/preview'; import { getHostToGuestBus, getHostToHostBus } from '../../utils/subjects'; import PreviewBackButton from '../PreviewBackButton'; import PreviewForwardButton from '../PreviewForwardButton'; import { usePreviewNavigation } from '../../hooks/usePreviewNavigation'; import CircularProgress from '@mui/material/CircularProgress'; import usePreviewState from '../../hooks/usePreviewState'; import Popover from '@mui/material/Popover'; import Box from '@mui/material/Box'; import Alert, { alertClasses } from '@mui/material/Alert'; import Button from '@mui/material/Button'; import ErrorOutlineOutlined from '@mui/icons-material/ErrorOutlineOutlined'; import useEnv from '../../hooks/useEnv'; const useAddressBarStyles = makeStyles()((theme) => ({ toolbar: { placeContent: 'center space-between' }, inputContainer: { marginLeft: theme.spacing(1) }, input: { border: 'none', background: 'transparent', '&:focus:invalid, &:focus': { border: 'none', boxShadow: 'none' } }, divider: { height: 28, margin: 4 }, selectorPopoverRoot: { width: 400, marginLeft: '4px' }, hidden: { visibility: 'hidden' }, itemActionSkeleton: { width: 40, margin: '0 5px' }, itemDisplayWrapper: { width: '100%', overflow: 'hidden', cursor: 'pointer', display: 'flex' }, itemPreviewUrl: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', marginLeft: '4px' }, itemDisplaySkeleton: { marginLeft: '5px', width: '100%' } })); export function PreviewAddressBar(props) { const { classes } = useAddressBarStyles(); const { site = '', item } = props; const noSiteSet = isBlank(site); const { error } = usePreviewState(); const { currentUrlPath = '' } = usePreviewNavigation(); const [internalUrl, setInternalUrl] = useState(currentUrlPath); const [openSelector, setOpenSelector] = useState(false); const [focus, setFocus] = useState(false); const dispatch = useDispatch(); const rootPath = '/site/website/index.xml'; // If the current item is not a page (e.g. a component), selectedItem will be null // The SingleItemSelector's selectedItem prop is optional, if it doesn't exist, it handles actions using the rootPath. const selectedItem = useMemo(() => { if (item && item.path.includes(withoutIndex(rootPath))) { return item; } else { return null; } }, [item]); const disableItemMenuButton = !item || Boolean(error); const onOptions = (e) => { const anchorRect = e.currentTarget.getBoundingClientRect(); const top = anchorRect.top + getOffsetTop(anchorRect, 'top'); const left = anchorRect.left + getOffsetLeft(anchorRect, 'left'); let path = item.path; if (path === '/site/website') { path = withIndex(item.path); } dispatch( showItemMegaMenu({ path: path, anchorReference: 'anchorPosition', anchorPosition: { top, left }, loaderItems: getNumOfMenuOptionsForItem(item) }) ); }; const onUrlChange = (url) => { dispatch(changeCurrentUrl(url)); }; const onRefresh = () => { const action = reloadRequest(); getHostToGuestBus().next(action); getHostToHostBus().next(action); }; useEffect(() => { currentUrlPath && setInternalUrl(currentUrlPath); }, [currentUrlPath]); // region XB communication detection let { xbDetectionTimeoutMs } = usePreviewState(); const timeoutRef = useRef(); const [alertLevel, setAlertLevel] = useState(0); const [popoverAnchorEl, setPopoverAnchorEl] = useState(null); const openPopover = Boolean(popoverAnchorEl); const { authoringBase } = useEnv(); const handlePopoverHover = (event) => { clearTimeout(timeoutRef.current); }; const handlePopoverOpen = (event) => { clearTimeout(timeoutRef.current); setPopoverAnchorEl(event.currentTarget); }; const handlePopoverClose = () => { timeoutRef.current = setTimeout(() => { setPopoverAnchorEl(null); }, 800); }; useEffect(() => { if (!item && xbDetectionTimeoutMs > 0) { let timeout, subscription; let beginStatusTimer = () => { timeout = setTimeout(() => { setAlertLevel(1); timeout = setTimeout(() => { setAlertLevel(2); }, xbDetectionTimeoutMs); }, xbDetectionTimeoutMs); }; subscription = getHostToHostBus().subscribe((action) => { if (action.type === reloadRequest.type) { clearTimeout(timeout); setAlertLevel(0); beginStatusTimer(); } }); beginStatusTimer(); return () => { clearTimeout(timeout); setAlertLevel(0); subscription.unsubscribe(); }; } else { setPopoverAnchorEl(null); } }, [item, xbDetectionTimeoutMs]); // endregion return React.createElement( React.Fragment, null, React.createElement(PreviewBackButton, null), React.createElement(PreviewForwardButton, null), React.createElement( Tooltip, { title: noSiteSet ? '' : React.createElement(FormattedMessage, { defaultMessage: 'Reload this page (r)' }) }, React.createElement( IconButton, { onClick: noSiteSet ? undefined : onRefresh, size: 'large', disabled: noSiteSet }, React.createElement(RefreshRounded, null) ) ), React.createElement( Paper, { variant: focus ? 'elevation' : 'outlined', elevation: focus ? 2 : 0, sx: (theme) => ({ width: '300px', borderRadius: 8, padding: '2px 2px 2px 10px', display: 'flex', alignItems: 'center', backgroundColor: theme.palette.mode === 'light' ? theme.palette.background.paper : theme.palette.grey['800'], borderColor: theme.palette.divider, ...(focus && { border: `2px solid ${theme.palette.primary.main}` }) }) }, !focus && item && React.createElement( 'div', { className: classes.itemDisplayWrapper, onClick: () => setFocus(true) }, React.createElement(ItemDisplay, { item: item, styles: { root: { maxWidth: '100%' } }, showNavigableAsLinks: false }) ), (focus || !item) && React.createElement(PagesSearchAhead, { autoFocus: focus, value: internalUrl, placeholder: noSiteSet ? '' : '/', onEnter: onUrlChange, classes: { input: classes.input }, onFocus: () => setFocus(true), onBlur: () => setFocus(false) }), React.createElement( Tooltip, { title: disableItemMenuButton ? '' : React.createElement(FormattedMessage, { defaultMessage: 'Options (a)' }) }, React.createElement( IconButton, { onClick: onOptions, disabled: disableItemMenuButton, sx: error ? { visibility: 'hidden' } : undefined, size: 'medium', id: 'previewAddressBarActionsMenuButton' }, React.createElement(MoreRounded, { sx: alertLevel === 2 ? { visibility: 'hidden' } : undefined }), !item && !error && React.createElement( Box, { sx: { top: 0, left: 0, right: 0, bottom: 0, display: 'flex', position: 'absolute', alignItems: 'center', placeContent: 'center', pointerEvents: 'all' }, onMouseEnter: handlePopoverOpen, onMouseLeave: handlePopoverClose }, alertLevel === 2 ? React.createElement(ErrorOutlineOutlined, { color: 'error' }) : React.createElement(CircularProgress, { sx: { position: 'absolute', pointerEvents: 'all' }, color: alertLevel ? (alertLevel === 1 ? 'warning' : 'error') : 'primary' }) ) ) ) ), React.createElement(SingleItemSelector, { rootPath: rootPath, selectedItem: selectedItem, open: openSelector, onClose: () => setOpenSelector(false), onDropdownClick: () => setOpenSelector(!openSelector), onItemClicked: (item) => { setOpenSelector(false); setInternalUrl(item.previewUrl); onUrlChange(item.previewUrl); }, hideUI: true, classes: { popoverRoot: classes.selectorPopoverRoot }, tooltip: 'Navigation' }), React.createElement( Popover, // Avoid backdrop from blocking the interaction with other elements { // Avoid backdrop from blocking the interaction with other elements sx: { pointerEvents: 'none' }, open: openPopover, anchorEl: popoverAnchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'center' }, slotProps: { paper: { sx: { pointerEvents: 'all' }, onMouseEnter: handlePopoverHover, onMouseLeave: handlePopoverClose } }, onClose: handlePopoverClose, disableRestoreFocus: true }, React.createElement( Alert, { severity: alertLevel ? (alertLevel === 1 ? 'warning' : 'error') : 'info', action: React.createElement( Button, { href: `${authoringBase}/help/preview-missing-app-connection`, target: '_blank' }, React.createElement(FormattedMessage, { defaultMessage: 'Learn more' }) ), sx: { [`.${alertClasses.icon},.${alertClasses.message}`]: { display: 'flex', alignItems: 'center' } } }, React.createElement(FormattedMessage, { defaultMessage: 'Awaiting a connection from the Preview application' }) ) ) ); } export default PreviewAddressBar;