{
- const fullscreenElement =
- document.fullscreenElement || document.webkitFullscreenElement
-
- return fullscreenElement?.classList.contains(
- getGridItemDomElementClassName(itemId)
- )
-}
diff --git a/src/components/Item/VisualizationItem/styles/Item.module.css b/src/components/Item/VisualizationItem/styles/Item.module.css
new file mode 100644
index 000000000..1714dd947
--- /dev/null
+++ b/src/components/Item/VisualizationItem/styles/Item.module.css
@@ -0,0 +1,17 @@
+.content {
+ composes: content from '../../styles/Item.module.css';
+ position: relative;
+}
+
+.fullscreen {
+ composes: fullscreen from '../../styles/Item.module.css';
+}
+
+.scrollbox {
+ overflow: auto;
+}
+
+.edit,
+.print {
+ flex: 1;
+}
diff --git a/src/components/Item/styles/Item.module.css b/src/components/Item/styles/Item.module.css
new file mode 100644
index 000000000..4fde9469f
--- /dev/null
+++ b/src/components/Item/styles/Item.module.css
@@ -0,0 +1,11 @@
+.content {
+ margin-block-start: 0;
+ margin-block-end: var(--item-content-padding);
+ margin-inline: var(--item-content-padding);
+ overflow: hidden;
+}
+
+.fullscreen {
+ margin-block-end: 0;
+ margin-inline: 0;
+}
diff --git a/src/components/ProgressiveLoadingContainer.js b/src/components/ProgressiveLoadingContainer.js
index 5dc4d06a5..ce2bdb7f1 100644
--- a/src/components/ProgressiveLoadingContainer.js
+++ b/src/components/ProgressiveLoadingContainer.js
@@ -1,26 +1,76 @@
+import i18n from '@dhis2/d2-i18n'
+import { Divider, spacers, CenteredContent } from '@dhis2/ui'
import debounce from 'lodash/debounce.js'
import pick from 'lodash/pick.js'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
+import { getVisualizationName } from '../modules/item.js'
+import {
+ APP,
+ MESSAGES,
+ RESOURCES,
+ REPORTS,
+ isVisualizationType,
+} from '../modules/itemTypes.js'
+import ItemHeader from './Item/ItemHeader/ItemHeader.js'
const defaultDebounceMs = 100
const defaultBufferFactor = 0.25
const observerConfig = { attributes: true, childList: false, subtree: false }
+const getItemHeader = ({ item, apps }) => {
+ if (isVisualizationType(item)) {
+ const title = getVisualizationName(item)
+ return
+ }
+
+ let title
+ if ([MESSAGES, RESOURCES, REPORTS].includes(item.type)) {
+ const titleMap = {
+ [MESSAGES]: i18n.t('Messages'),
+ [RESOURCES]: i18n.t('Resources'),
+ [REPORTS]: i18n.t('Reports'),
+ }
+ title = titleMap[item.type]
+ } else if (item.type === APP) {
+ let appDetails
+ const appKey = item.appKey
+
+ if (appKey) {
+ appDetails = apps.find((app) => app.key === appKey)
+ }
+
+ const hideTitle = appDetails?.settings?.dashboardWidget?.hideTitle
+ title = hideTitle ? null : appDetails.name
+ }
+
+ return !title ? null : (
+ <>
+
+
+ >
+ )
+}
+
class ProgressiveLoadingContainer extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
+ item: PropTypes.object.isRequired,
+ apps: PropTypes.array,
bufferFactor: PropTypes.number,
className: PropTypes.string,
+ dashboardIsCached: PropTypes.bool,
debounceMs: PropTypes.number,
forceLoad: PropTypes.bool,
- itemId: PropTypes.string,
+ fullsreenView: PropTypes.bool,
+ isOffline: PropTypes.bool,
style: PropTypes.object,
}
static defaultProps = {
debounceMs: defaultDebounceMs,
bufferFactor: defaultBufferFactor,
forceLoad: false,
+ fullsreenView: false,
}
state = {
@@ -30,6 +80,7 @@ class ProgressiveLoadingContainer extends Component {
debouncedCheckShouldLoad = null
handlerOptions = { passive: true }
observer = null
+ isObserving = null
checkShouldLoad() {
if (!this.containerRef) {
@@ -39,7 +90,15 @@ class ProgressiveLoadingContainer extends Component {
// force load item regardless of its position
if (this.forceLoad && !this.state.shouldLoad) {
this.setState({ shouldLoad: true })
- this.removeHandler()
+ if (!this.props.isOffline || this.props.dashboardIsCached) {
+ this.removeHandler()
+ }
+ return
+ }
+
+ // when in fullscreen view, load is not based on
+ // position relative to viewport but instead on forceLoad only
+ if (this.props.fullsreenView) {
return
}
@@ -52,7 +111,9 @@ class ProgressiveLoadingContainer extends Component {
rect.top < window.innerHeight + bufferPx
) {
this.setState({ shouldLoad: true })
- this.removeHandler()
+ if (!this.props.isOffline || this.props.dashboardIsCached) {
+ this.removeHandler()
+ }
}
}
@@ -84,6 +145,7 @@ class ProgressiveLoadingContainer extends Component {
this.observer = new MutationObserver(mutationCallback)
this.observer.observe(this.containerRef, observerConfig)
+ this.isObserving = true
}
removeHandler() {
@@ -98,6 +160,7 @@ class ProgressiveLoadingContainer extends Component {
})
this.observer.disconnect()
+ this.isObserving = false
}
componentDidMount() {
@@ -116,9 +179,16 @@ class ProgressiveLoadingContainer extends Component {
}
render() {
- const { children, className, style, ...props } = this.props
-
- const shouldLoad = this.state.shouldLoad || props.forceLoad
+ const {
+ children,
+ className,
+ style,
+ apps,
+ item,
+ dashboardIsCached,
+ isOffline,
+ ...props
+ } = this.props
const eventProps = pick(props, [
'onMouseDown',
@@ -127,15 +197,38 @@ class ProgressiveLoadingContainer extends Component {
'onTouchEnd',
])
+ const renderContent = this.state.shouldLoad || props.forceLoad
+
+ const getContent = () => {
+ if (isOffline && !dashboardIsCached && this.isObserving !== false) {
+ return !renderContent ? null : (
+
+ {getItemHeader({ item, apps })}
+
+ {i18n.t('Not available offline')}
+
+
+ )
+ } else {
+ return renderContent && children
+ }
+ }
+
return (
(this.containerRef = ref)}
style={style}
className={className}
- data-test={`dashboarditem-${props.itemId}`}
+ data-test={`dashboarditem-${item.id}`}
{...eventProps}
>
- {shouldLoad && children}
+ {getContent()}
)
}
diff --git a/src/components/styles/App.css b/src/components/styles/App.css
index b55fbef81..3573b1982 100644
--- a/src/components/styles/App.css
+++ b/src/components/styles/App.css
@@ -56,29 +56,6 @@ table.pivot * {
background-color: #48a999;
}
-div:fullscreen,
-div:-webkit-full-screen {
- background-color: white;
-}
-
-div:-webkit-full-screen {
- object-fit: contain;
- position: fixed !important;
- inset-block-start: 0px !important;
- inset-inline-end: 0px !important;
- inset-block-end: 0px !important;
- inset-inline-start: 0px !important;
- box-sizing: border-box !important;
- min-inline-size: 0px !important;
- max-inline-size: none !important;
- min-block-size: 0px !important;
- max-block-size: none !important;
- inline-size: 100% !important;
- block-size: 100% !important;
- transform: none !important;
- margin: 0px !important;
-}
-
@media print {
body {
inline-size: 100% !important;
diff --git a/src/components/styles/ItemGrid.css b/src/components/styles/ItemGrid.css
index 5048f6bcb..b3201d553 100644
--- a/src/components/styles/ItemGrid.css
+++ b/src/components/styles/ItemGrid.css
@@ -14,8 +14,10 @@
.react-grid-item.edit,
.react-grid-item.EVENT_VISUALIZATION,
.react-grid-item.RESOURCES,
+.react-grid-item.REPORTS,
.react-grid-item.TEXT,
.react-grid-item.MESSAGES,
+.react-grid-item.EVENT_REPORT,
.react-grid-item.APP {
display: flex;
flex-direction: column;
@@ -54,47 +56,3 @@
.react-resizable-handle {
background: none;
}
-
-/* dashboard item - content */
-
-.dashboard-item-content {
- margin-block-start: 0;
- margin-block-end: var(--item-content-padding);
- margin-inline-start: var(--item-content-padding);
- margin-inline-end: var(--item-content-padding);
- overflow: auto;
-}
-
-.dashboard-item-content-hidden-title {
- margin-block-start: 5px;
- margin-block-end: 4px;
- margin-inline-start: 4px;
- margin-inline-end: 4px;
- overflow: auto;
-}
-
-.TEXT .dashboard-item-content {
- padding-block-end: var(--item-content-padding);
-}
-
-.EVENT_REPORT .dashboard-item-content {
- position: relative;
-}
-
-.CHART .dashboard-item-content,
-.VISUALIZATION .dashboard-item-content,
-.MAP .dashboard-item-content,
-.EVENT_CHART .dashboard-item-content,
-.EVENT_VISUALIZATION .dashboard-item-content {
- position: relative;
- overflow: hidden;
-}
-
-.react-grid-item.edit .dashboard-item-content,
-.react-grid-item.RESOURCES .dashboard-item-content,
-.react-grid-item.TEXT .dashboard-item-content,
-.react-grid-item.MESSAGES .dashboard-item-content,
-.react-grid-item.APP .dashboard-item-content,
-.react-grid-item.APP .dashboard-item-content-hidden-title {
- flex: 1;
-}
diff --git a/src/modules/itemTypes.js b/src/modules/itemTypes.js
index 985aa3cfd..d1281b088 100644
--- a/src/modules/itemTypes.js
+++ b/src/modules/itemTypes.js
@@ -71,6 +71,7 @@ export const itemTypeMap = {
appName: 'Data Visualizer',
appKey: 'data-visualizer',
defaultItemCount: 10,
+ supportsFullscreen: true,
},
[REPORT_TABLE]: {
id: REPORT_TABLE,
@@ -82,6 +83,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-data-visualizer/#/${id}`,
appName: 'Data Visualizer',
+ supportsFullscreen: true,
},
[CHART]: {
id: CHART,
@@ -93,6 +95,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-data-visualizer/#/${id}`,
appName: 'Data Visualizer',
+ supportsFullscreen: true,
},
[MAP]: {
id: MAP,
@@ -104,6 +107,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-maps/?id=${id}`,
appName: 'Maps',
+ supportsFullscreen: true,
},
[EVENT_REPORT]: {
id: EVENT_REPORT,
@@ -114,6 +118,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-event-reports/?id=${id}`,
appName: 'Event Reports',
+ supportsFullscreen: true,
},
[EVENT_CHART]: {
id: EVENT_CHART,
@@ -124,6 +129,7 @@ export const itemTypeMap = {
isVisualizationType: true,
appUrl: (id) => `dhis-web-event-visualizer/?id=${id}`,
appName: 'Event Visualizer',
+ supportsFullscreen: true,
},
[EVENT_VISUALIZATION]: {
id: EVENT_VISUALIZATION,
@@ -136,11 +142,13 @@ export const itemTypeMap = {
appUrl: (id) => `api/apps/line-listing/index.html#/${id}`,
appName: 'Line Listing',
appKey: 'line-listing',
+ supportsFullscreen: true,
},
[APP]: {
endPointName: 'apps',
propName: 'appKey',
pluralTitle: i18n.t('Apps'),
+ supportsFullscreen: true,
},
[REPORTS]: {
id: REPORTS,
@@ -158,6 +166,7 @@ export const itemTypeMap = {
return `api/reports/${id}/data.pdf?t=${new Date().getTime()}`
}
},
+ supportsFullscreen: true,
},
[RESOURCES]: {
id: RESOURCES,
@@ -165,6 +174,7 @@ export const itemTypeMap = {
propName: 'resources',
pluralTitle: i18n.t('Resources'),
appUrl: (id) => `api/documents/${id}/data`,
+ supportsFullscreen: true,
},
[USERS]: {
id: USERS,
@@ -173,22 +183,28 @@ export const itemTypeMap = {
pluralTitle: i18n.t('Users'),
appUrl: (id) =>
`dhis-web-dashboard-integration/profile.action?id=${id}`,
+ supportsFullscreen: false,
},
[TEXT]: {
id: TEXT,
propName: 'text',
+ supportsFullscreen: true,
},
[MESSAGES]: {
propName: 'messages',
+ supportsFullscreen: false,
},
[SPACER]: {
propName: 'text',
+ supportsFullscreen: false,
},
[PAGEBREAK]: {
propName: 'text',
+ supportsFullscreen: false,
},
[PRINT_TITLE_PAGE]: {
propName: 'text',
+ supportsFullscreen: false,
},
}
@@ -211,6 +227,9 @@ export const getItemUrl = (type, item, baseUrl) => {
return url
}
+export const itemTypeSupportsFullscreen = (type) =>
+ itemTypeMap[type].supportsFullscreen
+
export const getItemIcon = (type) => {
switch (type) {
case REPORT_TABLE:
diff --git a/src/pages/edit/ItemGrid.js b/src/pages/edit/ItemGrid.js
index 2e503d04a..fa2464fe7 100644
--- a/src/pages/edit/ItemGrid.js
+++ b/src/pages/edit/ItemGrid.js
@@ -60,7 +60,7 @@ const EditItemGrid = ({
'edit',
getGridItemDomElementClassName(item.id)
)}
- itemId={item.id}
+ item={item}
>
- {
+const ResponsiveItemGrid = ({ dashboardIsCached }) => {
+ const dashboardId = useSelector(sGetSelectedId)
+ const dashboardItems = useSelector(sGetSelectedDashboardItems)
const { width } = useWindowDimensions()
+ const { apps } = useCachedDataQuery()
const [expandedItems, setExpandedItems] = useState({})
const [displayItems, setDisplayItems] = useState(dashboardItems)
const [layoutSm, setLayoutSm] = useState([])
const [gridWidth, setGridWidth] = useState(0)
const [forceLoad, setForceLoad] = useState(false)
const { recordingState } = useCacheableSection(dashboardId)
+ const { isDisconnected: isOffline } = useDhis2ConnectionStatus()
const firstOfTypes = getFirstOfTypes(dashboardItems)
+ const slideshowElementRef = useRef(null)
+
+ const {
+ slideshowItemIndex,
+ sortedItems,
+ isEnteringSlideshow,
+ exitSlideshow,
+ nextItem,
+ prevItem,
+ } = useSlideshow(displayItems, slideshowElementRef)
+
+ const isSlideshowView = slideshowItemIndex !== null
useEffect(() => {
+ const getItemsWithAdjustedHeight = (items) =>
+ items.map((item) => {
+ const expandedItem = expandedItems[item.id]
+
+ if (expandedItem && expandedItem === true) {
+ const expandedHeight = isSmallScreen(width)
+ ? EXPANDED_HEIGHT_SM
+ : EXPANDED_HEIGHT
+ return Object.assign({}, item, {
+ h: item.h + expandedHeight,
+ smallOriginalH: getProportionalHeight(item, width),
+ })
+ }
+
+ return item
+ })
setLayoutSm(
getItemsWithAdjustedHeight(getSmallLayout(dashboardItems, width))
)
@@ -68,23 +104,6 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
setExpandedItems(newExpandedItems)
}
- const getItemsWithAdjustedHeight = (items) =>
- items.map((item) => {
- const expandedItem = expandedItems[item.id]
-
- if (expandedItem && expandedItem === true) {
- const expandedHeight = isSmallScreen(width)
- ? EXPANDED_HEIGHT_SM
- : EXPANDED_HEIGHT
- return Object.assign({}, item, {
- h: item.h + expandedHeight,
- smallOriginalH: getProportionalHeight(item, width),
- })
- }
-
- return item
- })
-
const getItemComponent = (item) => {
if (!layoutSm.length) {
return
@@ -94,16 +113,48 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
item.firstOfType = true
}
+ const itemIsFullscreen = isSlideshowView
+ ? sortedItems[slideshowItemIndex].id === item.id
+ : null
+
+ // Force load next and previous items for slideshow view
+ const nextslideshowItemIndex =
+ slideshowItemIndex === sortedItems.length - 1
+ ? 0
+ : slideshowItemIndex + 1
+ const prevslideshowItemIndex =
+ slideshowItemIndex === 0
+ ? sortedItems.length - 1
+ : slideshowItemIndex - 1
+
+ const itemIsNextPrevFullscreen =
+ isSlideshowView &&
+ (sortedItems[nextslideshowItemIndex].id === item.id ||
+ sortedItems[prevslideshowItemIndex].id === item.id)
+
return (
- {
dashboardMode={VIEW}
isRecording={forceLoad}
onToggleItemExpanded={onToggleItemExpanded}
+ isFullscreen={itemIsFullscreen}
+ sortIndex={sortedItems.findIndex((i) => i.id === item.id)}
/>
)
@@ -131,40 +184,50 @@ const ResponsiveItemGrid = ({ dashboardId, dashboardItems }) => {
}
return (
-
- {getItemComponents(displayItems)}
-
+
+ {getItemComponents(displayItems)}
+
+ {isSlideshowView && !isEnteringSlideshow && (
+
+ )}
+
)
}
ResponsiveItemGrid.propTypes = {
- dashboardId: PropTypes.string,
- dashboardItems: PropTypes.array,
+ dashboardIsCached: PropTypes.bool,
}
-const mapStateToProps = (state) => ({
- dashboardItems: sGetSelectedDashboardItems(state),
- dashboardId: sGetSelectedId(state),
-})
-
-export default connect(mapStateToProps)(ResponsiveItemGrid)
+export default ResponsiveItemGrid
diff --git a/src/pages/view/SlideshowControlbar.js b/src/pages/view/SlideshowControlbar.js
new file mode 100644
index 000000000..58b6deef8
--- /dev/null
+++ b/src/pages/view/SlideshowControlbar.js
@@ -0,0 +1,93 @@
+import i18n from '@dhis2/d2-i18n'
+import {
+ IconChevronRight24,
+ IconChevronLeft24,
+ IconCross24,
+ colors,
+} from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { SlideshowFiltersInfo } from './SlideshowFiltersInfo.js'
+import styles from './styles/SlideshowControlbar.module.css'
+
+const SlideshowControlbar = ({
+ slideshowItemIndex,
+ exitSlideshow,
+ nextItem,
+ prevItem,
+ numItems,
+}) => {
+ const navigationDisabled = numItems === 1
+
+ const NextArrow =
+ document.dir === 'ltr' ? IconChevronRight24 : IconChevronLeft24
+ const PrevArrow =
+ document.dir === 'ltr' ? IconChevronLeft24 : IconChevronRight24
+
+ return (
+
+
+
+
+
+
+
+
{`${slideshowItemIndex + 1} / ${numItems}`}
+
+
+
+
+
+
+
+ )
+}
+
+SlideshowControlbar.propTypes = {
+ exitSlideshow: PropTypes.func.isRequired,
+ nextItem: PropTypes.func.isRequired,
+ numItems: PropTypes.number.isRequired,
+ prevItem: PropTypes.func.isRequired,
+ slideshowItemIndex: PropTypes.number.isRequired,
+}
+
+export default SlideshowControlbar
diff --git a/src/pages/view/SlideshowFiltersInfo.js b/src/pages/view/SlideshowFiltersInfo.js
new file mode 100644
index 000000000..85f1514d0
--- /dev/null
+++ b/src/pages/view/SlideshowFiltersInfo.js
@@ -0,0 +1,101 @@
+import i18n from '@dhis2/d2-i18n'
+import { Layer, Popper, IconFilter16 } from '@dhis2/ui'
+import PropTypes from 'prop-types'
+import React, { useMemo, useState, useRef } from 'react'
+import { useSelector } from 'react-redux'
+import { sGetNamedItemFilters } from '../../reducers/itemFilters.js'
+import styles from './styles/SlideshowFiltersInfo.module.css'
+
+const popperModifiers = [
+ {
+ name: 'offset',
+ options: {
+ offset: [0, 8],
+ },
+ },
+]
+const FilterSection = ({ name, values }) => (
+
+
{name}
+
+ {values.map((value) => (
+ -
+ {value.name}
+
+ ))}
+
+
+)
+
+FilterSection.propTypes = {
+ name: PropTypes.string,
+ values: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string })),
+}
+
+export const SlideshowFiltersInfo = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const ref = useRef(null)
+ const filters = useSelector(sGetNamedItemFilters)
+ const totalFilterCount = useMemo(
+ () =>
+ filters.reduce((total, filter) => total + filter.values.length, 0),
+ [filters]
+ )
+
+ if (filters.length === 0) {
+ return null
+ }
+
+ let filterMessage = ''
+ let multipleFilters = true
+ if (filters.length === 1 && filters[0].values.length === 1) {
+ multipleFilters = false
+ filterMessage = i18n.t('{{name}}: {{filter}}', {
+ name: filters[0].name,
+ filter: filters[0].values[0].name,
+ nsSeparator: '>',
+ })
+ }
+
+ return (
+ <>
+ {!multipleFilters ? (
+
+
+ {filterMessage}
+
+ ) : (
+
+ )}
+ {isOpen && multipleFilters && (
+
setIsOpen(false)}>
+
+ {filters.map((filter) => (
+
+ ))}
+
+
+ )}
+ >
+ )
+}
diff --git a/src/pages/view/TitleBar/ActionsBar.js b/src/pages/view/TitleBar/ActionsBar.js
index 24da7ec8b..b8ba37ec6 100644
--- a/src/pages/view/TitleBar/ActionsBar.js
+++ b/src/pages/view/TitleBar/ActionsBar.js
@@ -19,10 +19,13 @@ import { Link, Redirect } from 'react-router-dom'
import { acSetDashboardStarred } from '../../../actions/dashboards.js'
import { acClearItemFilters } from '../../../actions/itemFilters.js'
import { acSetShowDescription } from '../../../actions/showDescription.js'
+import { acSetSlideshow } from '../../../actions/slideshow.js'
import { apiPostShowDescription } from '../../../api/description.js'
import ConfirmActionDialog from '../../../components/ConfirmActionDialog.js'
import DropdownButton from '../../../components/DropdownButton/DropdownButton.js'
import MenuItem from '../../../components/MenuItemWithTooltip.js'
+import { useSystemSettings } from '../../../components/SystemSettingsProvider.js'
+import { itemTypeSupportsFullscreen } from '../../../modules/itemTypes.js'
import { useCacheableSection } from '../../../modules/useCacheableSection.js'
import { orObject } from '../../../modules/util.js'
import { sGetDashboardStarred } from '../../../reducers/dashboards.js'
@@ -41,11 +44,13 @@ const ViewActions = ({
showDescription,
starred,
setDashboardStarred,
+ setSlideshow,
updateShowDescription,
removeAllFilters,
restrictFilters,
allowedFilters,
filtersLength,
+ dashboardItems,
}) => {
const [moreOptionsSmallIsOpen, setMoreOptionsSmallIsOpen] = useState(false)
const [moreOptionsIsOpen, setMoreOptionsIsOpen] = useState(false)
@@ -57,6 +62,7 @@ const ViewActions = ({
const { isDisconnected: offline } = useDhis2ConnectionStatus()
const { lastUpdated, isCached, startRecording, remove } =
useCacheableSection(id)
+ const { allowVisFullscreen } = useSystemSettings().systemSettings
const { show } = useAlert(
({ msg }) => msg,
@@ -133,6 +139,10 @@ const ViewActions = ({
const userAccess = orObject(access)
+ const hasSlideshowItems = dashboardItems?.some(
+ (i) => itemTypeSupportsFullscreen(i.type) || false
+ )
+
const getMoreMenu = () => (
{lastUpdated ? (
@@ -226,6 +236,12 @@ const ViewActions = ({
)
+ const slideshowTooltipContent = !hasSlideshowItems
+ ? i18n.t('No dashboard items to show in slideshow')
+ : offline && !isCached
+ ? i18n.t('Not available offline')
+ : null
+
return (
<>
@@ -256,6 +272,22 @@ const ViewActions = ({
) : null}
+ {allowVisFullscreen ? (
+
+
+
+ ) : null}
{
export default connect(mapStateToProps, {
setDashboardStarred: acSetDashboardStarred,
+ setSlideshow: acSetSlideshow,
removeAllFilters: acClearItemFilters,
updateShowDescription: acSetShowDescription,
})(ViewActions)
diff --git a/src/pages/view/TitleBar/styles/ActionsBar.module.css b/src/pages/view/TitleBar/styles/ActionsBar.module.css
index 405ff0e44..6f7602507 100644
--- a/src/pages/view/TitleBar/styles/ActionsBar.module.css
+++ b/src/pages/view/TitleBar/styles/ActionsBar.module.css
@@ -19,6 +19,7 @@
@media only screen and (max-width: 480px) {
.strip .editButton,
+ .strip .slideshowButton,
.strip .shareButton {
display: none;
}
diff --git a/src/pages/view/ViewDashboard.js b/src/pages/view/ViewDashboard.js
index 62baa8438..03739c1a6 100644
--- a/src/pages/view/ViewDashboard.js
+++ b/src/pages/view/ViewDashboard.js
@@ -157,7 +157,7 @@ const ViewDashboard = (props) => {
<>
-
+
>
)
}
diff --git a/src/pages/view/styles/ItemGrid.module.css b/src/pages/view/styles/ItemGrid.module.css
index 0a534e8e0..49ffd8a31 100644
--- a/src/pages/view/styles/ItemGrid.module.css
+++ b/src/pages/view/styles/ItemGrid.module.css
@@ -1,3 +1,36 @@
+.slideshowContainer {
+ background-color: #f4f6f8;
+ block-size: 100vh;
+}
.grid {
margin-block-start: var(--spacers-dp16);
+ background-color: #f4f6f8;
+}
+
+/* react-grid-layout uses width and height instead
+ of css logical properties,
+ so these are the properties
+ that need to be overridden */
+/* stylelint-disable csstools/use-logical */
+.slideshowGrid {
+ margin-block-start: 0;
+ height: calc(100vh - 40px) !important;
+}
+.fullscreenItem {
+ width: 100% !important;
+ height: 100% !important;
+ transform: none !important;
+}
+/* stylelint-enable csstools/use-logical */
+
+.hiddenItem,
+.enteringFullscreen {
+ opacity: 0;
+}
+
+.displayedItem {
+ opacity: 1;
+ transition-delay: 100ms !important;
+ transition-property: opacity !important;
+ z-index: 10 !important;
}
diff --git a/src/pages/view/styles/SlideshowControlbar.module.css b/src/pages/view/styles/SlideshowControlbar.module.css
new file mode 100644
index 000000000..341c606ce
--- /dev/null
+++ b/src/pages/view/styles/SlideshowControlbar.module.css
@@ -0,0 +1,74 @@
+.container {
+ position: fixed;
+ inset-block-end: 0;
+ inline-size: 100vw;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: black;
+ padding-inline-start: 4px;
+ padding-inline-end: 4px;
+ z-index: 10;
+}
+.start,
+.middle,
+.end {
+ display: flex;
+ align-items: center;
+ inline-size: calc(100% / 3);
+}
+
+.start {
+ justify-content: start;
+}
+
+.middle {
+ justify-content: center;
+}
+
+.end {
+ justify-content: end;
+}
+
+.container button {
+ color: var(--colors-grey300);
+ background-color: #171819;
+ border-radius: 3px;
+ border: none;
+ padding: 4px;
+ cursor: pointer;
+ flex-grow: 0;
+}
+
+.container button:hover {
+ background-color: #202124;
+}
+
+.container button:disabled {
+ cursor: not-allowed;
+}
+
+.container button.squareButton {
+ inline-size: 32px;
+ block-size: 32px;
+}
+
+.controls {
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.pageCounter {
+ font-family: 'Roboto', sans-serif;
+ font-weight: 400;
+ font-style: normal;
+ color: var(--colors-grey300);
+ font-size: 14px;
+ margin: 0 8px;
+ min-inline-size: 60px;
+ /* avoid moving buttons for most counts below 999 items */
+ text-align: center;
+ user-select: none;
+}
diff --git a/src/pages/view/styles/SlideshowFiltersInfo.module.css b/src/pages/view/styles/SlideshowFiltersInfo.module.css
new file mode 100644
index 000000000..356fe311b
--- /dev/null
+++ b/src/pages/view/styles/SlideshowFiltersInfo.module.css
@@ -0,0 +1,49 @@
+.filterButton {
+ inline-size: auto;
+ display: flex;
+ align-items: center;
+ gap: var(--spacers-dp4);
+ font-size: 14px;
+}
+
+.singleFilterText {
+ display: flex;
+ gap: var(--spacers-dp4);
+ padding: 0px var(--spacers-dp8) 0 0;
+ font-size: 14px;
+ inline-size: auto;
+ color: var(--colors-grey300);
+}
+
+.popover {
+ border: 1px solid #202124;
+ background-color: #171819;
+ border-radius: 6px;
+ box-shadow: var(--elevations-e400);
+ padding: var(--spacers-dp12) var(--spacers-dp12) var(--spacers-dp16);
+ font-size: 13px;
+ color: var(--colors-grey300);
+ min-width: 200px;
+ max-width: 420px;
+ margin-block-end: var(--spacers-dp4);
+}
+
+.filterSection:not(:last-of-type) {
+ margin-block-end: var(--spacers-dp12);
+}
+
+.filterSectionHeader {
+ margin: 0 0 var(--spacers-dp4) 0;
+ font-weight: 500;
+ padding: 0;
+}
+
+.filterSectionList {
+ margin: 0;
+ padding: 0;
+ padding-inline-start: 18px;
+ list-style-type: circle;
+}
+.filterSectionList li:not(:last-of-type) {
+ margin-block-end: 2px;
+}
diff --git a/src/pages/view/useSlideshow.js b/src/pages/view/useSlideshow.js
new file mode 100644
index 000000000..0d42fc27e
--- /dev/null
+++ b/src/pages/view/useSlideshow.js
@@ -0,0 +1,107 @@
+import sortBy from 'lodash/sortBy.js'
+import { useState, useEffect, useMemo, useCallback } from 'react'
+import { useSelector, useDispatch } from 'react-redux'
+import { acSetSlideshow } from '../../actions/slideshow.js'
+import { itemTypeSupportsFullscreen } from '../../modules/itemTypes.js'
+import { sGetSlideshow } from '../../reducers/slideshow.js'
+
+const useSlideshow = (displayItems, slideshowElementRef) => {
+ const dispatch = useDispatch()
+ const firstItemIndex = useSelector(sGetSlideshow)
+ const [itemIndex, setItemIndex] = useState(null)
+ const [isEnteringSlideshow, setIsEnteringSlideshow] = useState(false)
+
+ // Sort items into order on dashboard
+ // and filter out items that don't support fullscreen
+ const sortedItems = useMemo(
+ () =>
+ sortBy(displayItems, ['y', 'x']).filter((item) =>
+ itemTypeSupportsFullscreen(item.type)
+ ),
+ [displayItems]
+ )
+
+ // Slideshow button or Item "View fullscreen" menu clicked
+ // Fullscreen Exit button or ESC key pressed
+ useEffect(() => {
+ if (Number.isInteger(firstItemIndex)) {
+ const el = slideshowElementRef?.current
+ setIsEnteringSlideshow(true)
+ el?.requestFullscreen({ navigationUI: 'show' }).then(() => {
+ setItemIndex(firstItemIndex)
+ })
+ } else {
+ setItemIndex(null)
+ }
+ }, [firstItemIndex, slideshowElementRef])
+
+ // Exit button clicked
+ const exitSlideshow = () => {
+ if (document.fullscreenElement) {
+ document.exitFullscreen()
+ }
+ }
+
+ const nextItem = useCallback(() => {
+ if (itemIndex === sortedItems.length - 1) {
+ setItemIndex(0)
+ } else {
+ setItemIndex(itemIndex + 1)
+ }
+ }, [itemIndex, sortedItems])
+
+ const prevItem = useCallback(() => {
+ if (itemIndex === 0) {
+ setItemIndex(sortedItems.length - 1)
+ } else {
+ setItemIndex(itemIndex - 1)
+ }
+ }, [itemIndex, sortedItems])
+
+ // Handle keyboard navigation for the slideshow
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (document.fullscreenElement) {
+ if (event.key === 'ArrowRight') {
+ document.dir === 'ltr' ? nextItem() : prevItem()
+ } else if (event.key === 'ArrowLeft') {
+ document.dir === 'ltr' ? prevItem() : nextItem()
+ }
+ }
+ }
+
+ const handleFullscreenChange = () => {
+ if (!document.fullscreenElement) {
+ dispatch(acSetSlideshow(null))
+ } else {
+ setTimeout(() => {
+ setIsEnteringSlideshow(false)
+ }, 200)
+ }
+ }
+
+ // Attach the event listener to the window object
+ window.addEventListener('keydown', handleKeyDown)
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
+
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown)
+ document.removeEventListener(
+ 'fullscreenchange',
+ handleFullscreenChange
+ )
+ }
+ }, [dispatch, nextItem, prevItem])
+
+ return {
+ slideshowItemIndex: itemIndex,
+ slideshowElementRef,
+ exitSlideshow,
+ nextItem,
+ prevItem,
+ sortedItems,
+ isEnteringSlideshow,
+ }
+}
+
+export default useSlideshow
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 9f839cbb2..19756dc46 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -13,6 +13,7 @@ import passiveViewRegistered from './passiveViewRegistered.js'
import printDashboard from './printDashboard.js'
import selected from './selected.js'
import showDescription from './showDescription.js'
+import slideshow from './slideshow.js'
import visualizations from './visualizations.js'
export default combineReducers({
@@ -29,6 +30,7 @@ export default combineReducers({
activeModalDimension,
passiveViewRegistered,
showDescription,
+ slideshow,
itemActiveTypes,
iframePluginStatus,
})
diff --git a/src/reducers/slideshow.js b/src/reducers/slideshow.js
new file mode 100644
index 000000000..1c871f15c
--- /dev/null
+++ b/src/reducers/slideshow.js
@@ -0,0 +1,10 @@
+export const SET_SLIDESHOW = 'SET_SLIDESHOW'
+
+export default (state = null, action) => {
+ if (action.type === SET_SLIDESHOW) {
+ return action.value
+ }
+ return state
+}
+
+export const sGetSlideshow = (state) => state.slideshow
diff --git a/yarn.lock b/yarn.lock
index 1a5a667ff..c68661ab0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6843,6 +6843,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+cypress-real-events@^1.13.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.13.0.tgz#6b7cd32dcac172db1493608f97a2576c7d0bd5af"
+ integrity sha512-LoejtK+dyZ1jaT8wGT5oASTPfsNV8/ClRp99ruN60oPj8cBJYod80iJDyNwfPAu4GCxTXOhhAv9FO65Hpwt6Hg==
+
cypress@^13.13.1:
version "13.13.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.1.tgz#860c1142a6e58ea412a764f0ce6ad07567721129"