Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ABLS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ A prime example of where this widget is used can be found in the [SEMA Daily Bri

Implementation is simple. Just drag the widget into your application, connect it to a map if it doesn't automatically connect, then create a view and select all of the layers you want visible (Including basemap layers). The layer order will be maintained, it's just the visibility that will be changed. The first view that you choose will be what the map will appear with the first time the ABLS widget is loaded.

Expandable layers can also be set, to act as a sort of "quick reference" layer visibility list. The layers in the expandable list can be either layers that would be visible within the view, or layers that would not be visible in the view. This can be helpful for setups where you have multiple themes of data, but within each theme there are reference layers that won't always be needed. Adding a default layers widget would be too much in this case, as there would be so many layers that it's not easy to scroll through, which is where the expandable layers fits in!

If the data you are switching views between is time enabled, you can also apply time filtering on the view. Time filtering is relatively self explanatory, but in general, is used to apply dynamic time filtering as an offset from the current time. This is good for forecast maps, or to view past information, like in an EOC setting. If your layers are not time enabled, do not turn on time filtering to speed up load time.

As of version 1.1.1, the layer selection in the settings panel has been improved in a couple of ways.

1. The layers show in the same order as the drawing order (Higher layers on top)
2. The layers support group layers and sub layers.

This improves the setup process for the widget when dealing with a map that has lots of layers, especially when it contains many group layers.
2 changes: 1 addition & 1 deletion ABLS/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "abls",
"version": "1.1.0",
"version": "1.1.1",
"description": "A Better Layer Switcher - A custom Experience Widget, to swith between sets of layers using a views navigation like interface.",
"keywords": [
"exb-widget",
Expand Down
16 changes: 0 additions & 16 deletions ABLS/src/runtime/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,4 @@
margin-left: 4px;
vertical-align: middle;
opacity: 0.75;
}

/* Wrapper that anchors the measurement ref */
.view-buttons-wrapper {
position: relative;
}

/* Floating layer list rendered via portal into document.body */
.expanded-layers-list {
position: fixed;
min-width: 220px;
max-width: 360px;
width: max-content;
max-height: 300px;
overflow-y: auto;
z-index: 9999;
}
233 changes: 56 additions & 177 deletions ABLS/src/runtime/widget.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { React, jsx } from "jimu-core"
import ReactDOM from "react-dom"
//import { lazy } from 'react'
import type { AllWidgetProps } from "jimu-core"
import { JimuMapViewComponent, type JimuMapView } from "jimu-arcgis"
Expand All @@ -22,11 +21,6 @@ export default function Widget (props: AllWidgetProps<Config>) {

const [expand, setExpand] = React.useState(false)
const [expandedLayers, setExpandedLayers] = React.useState<Layer[]>([])
const [visibleLayers, setVisibleLayers] = React.useState<Layer[]>([])
const [listStyle, setListStyle] = React.useState<React.CSSProperties>({})
const buttonsWrapperRef = React.useRef<HTMLDivElement>(null)
const listRef = React.useRef<any>(null)
const focusButtonOnCollapseRef = React.useRef(false)

// This is the way that the widget prevents itself from running itself, and from crashing. It checks to see if any maps have been selected, and if any views have been configured.
const isConfigured = useMapWidgetIds?.length > 0 && config.views?.length > 0
Expand All @@ -36,26 +30,18 @@ export default function Widget (props: AllWidgetProps<Config>) {
if (expand && activeViewId === view.id) {
setExpand(false)
setExpandedLayers([])
setVisibleLayers([])
return
}
if (jimuMapView?.view?.map?.allLayers) {
const allLayers = jimuMapView.view.map.allLayers.toArray().reverse()
allLayers.forEach((layer) => {
if (view.expandLayerIds.includes(layer.id)) {
setExpandedLayers((prev) => [...prev, layer])
if (layer.visible && !visibleLayers.includes(layer)) {
setVisibleLayers((prev) => [...prev, layer])
}
else if (!layer.visible && visibleLayers.includes(layer)) {
setVisibleLayers((prev) => prev.filter((l) => l !== layer))
}
}
})
setExpand(true)
const nextExpandedLayers = jimuMapView.view.map.allLayers
.toArray()
.reverse()
.filter((layer) => view.expandLayerIds.includes(layer.id))
setExpandedLayers(nextExpandedLayers)
setExpand(nextExpandedLayers.length > 0)
}
},
[expand, activeViewId, jimuMapView, visibleLayers]
[expand, activeViewId, jimuMapView]
)


Expand Down Expand Up @@ -121,113 +107,6 @@ export default function Widget (props: AllWidgetProps<Config>) {
[jimuMapView]
)

const focusActiveButton = useCallback(() => {
if (!activeViewId || !buttonsWrapperRef.current) return
const btn = buttonsWrapperRef.current.querySelector(`[data-view-id="${activeViewId}"]`)
if (!btn) return
const setFocusFn = Reflect.get(btn, 'setFocus')
if (typeof setFocusFn === 'function') {
setFocusFn.call(btn)
} else if (btn instanceof HTMLElement) {
btn.focus()
}
}, [activeViewId])

React.useEffect(() => {
if (expand && buttonsWrapperRef.current) {
const rect = buttonsWrapperRef.current.getBoundingClientRect()
const spaceAbove = rect.top
const spaceBelow = window.innerHeight - rect.bottom
const openUpward = spaceAbove >= 150 || spaceAbove >= spaceBelow
setListStyle(
openUpward
? { bottom: window.innerHeight - rect.top, left: rect.left }
: { top: rect.bottom, left: rect.left }
)
}
}, [expand])

// Focus the first list item shortly after the list opens
React.useEffect(() => {
if (!expand) return
const timer = setTimeout(() => {
const firstItem = listRef.current?.querySelector('calcite-list-item')
firstItem?.setFocus?.()
}, 50)
return () => { clearTimeout(timer) }
}, [expand])

// Keyboard navigation: Escape closes the list; ArrowDown on the last item returns focus to the active button
React.useEffect(() => {
const el = listRef.current
if (!expand || !el) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.stopPropagation()
focusButtonOnCollapseRef.current = true
setExpand(false)
setExpandedLayers([])
setVisibleLayers([])
return
}
if (e.key === 'ArrowDown') {
const allItems = Array.from(el.querySelectorAll('calcite-list-item'))
const lastItem = allItems[allItems.length - 1]
if (lastItem && document.activeElement === lastItem) {
e.preventDefault()
focusActiveButton()
}
}
}
el.addEventListener('keydown', handleKeyDown, true)
return () => { el.removeEventListener('keydown', handleKeyDown, true) }
}, [expand, focusActiveButton])

// ArrowUp on the active button navigates to the last item in the open list
React.useEffect(() => {
if (!expand || !buttonsWrapperRef.current) return
const wrapper = buttonsWrapperRef.current
const handleButtonKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'ArrowUp') return
const target = e.target as HTMLElement
if (!target.closest(`[data-view-id="${activeViewId}"]`)) return
const allItems = Array.from(listRef.current?.querySelectorAll('calcite-list-item') ?? [])
const lastItem = allItems[allItems.length - 1] as any
if (lastItem) {
e.preventDefault()
lastItem.setFocus?.()
}
}
wrapper.addEventListener('keydown', handleButtonKeyDown, true)
return () => { wrapper.removeEventListener('keydown', handleButtonKeyDown, true) }
}, [expand, activeViewId])

// Click outside both the list and the buttons wrapper collapses the list
React.useEffect(() => {
if (!expand) return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node
if (
!(listRef.current?.contains(target)) &&
!(buttonsWrapperRef.current?.contains(target))
) {
setExpand(false)
setExpandedLayers([])
setVisibleLayers([])
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => { document.removeEventListener('mousedown', handleClickOutside) }
}, [expand])

// After a collapse-with-focus was requested, focus the active button once the list unmounts
React.useEffect(() => {
if (!expand && focusButtonOnCollapseRef.current) {
focusButtonOnCollapseRef.current = false
focusActiveButton()
}
}, [expand, focusActiveButton])

React.useEffect(() => {
// Check if the map is loaded, if views are configured, and if no view is active yet.
if (jimuMapView && !activeViewId && config.views?.length > 0) {
Expand Down Expand Up @@ -293,6 +172,20 @@ export default function Widget (props: AllWidgetProps<Config>) {
return (
// Outer Div for the entire widget
<div className="widget-abls jimu-widget">
<calcite-popover
referenceElement={activeViewId}
label="expanded layers"
open={expand && expandedLayers.length > 0}
heading="Layers"
>
<calcite-list
label="Expanded Layers"
selectionMode="multiple"
displayMode="nested"
>
{topLevelLayers.map((layer) => renderLayerItem(layer))}
</calcite-list>
</calcite-popover>
{useMapWidgetIds?.length > 0 && (
// Set a link to the map, so that when the views change, the layers on the map will actually change.
<JimuMapViewComponent
Expand All @@ -302,56 +195,42 @@ export default function Widget (props: AllWidgetProps<Config>) {
}}
/>
)}
<div className="view-buttons-wrapper" ref={buttonsWrapperRef}>
{expand && expandedLayers.length > 0 && ReactDOM.createPortal(
<calcite-list
ref={listRef}
label="Expanded Layers"
className="expanded-layers-list"
selectionMode="multiple"
displayMode="nested"
style={listStyle}
>
{topLevelLayers.map((layer) => renderLayerItem(layer))}
</calcite-list>,
document.body
<div className="view-buttons-container">
{config.views.map(
(
view //The .map function creates the contained items multiple times, one button for each view in this case.
) => (
<calcite-button
key={view.id}
data-view-id={view.id}
className={`view-button ${activeViewId === view.id ? "active" : ""
}`}
title={view.name}
id={view.id}
onClick={() => {
if (activeViewId === view.id) {
handleViewExpand(view)
return
}
handleViewChange(view)
}}
appearance={activeViewId === view.id ? "solid" : "transparent"}
kind={activeViewId === view.id ? "brand" : "neutral"}
>
{view.icon && (
<Icon icon={view.icon.svg} size="16" className="mr-2" />
)}
{view.name}
{view.expandLayerIds?.length > 0 && (
<Icon
icon={expand && activeViewId === view.id ? chevronUpSvg : chevronDownSvg}
size="12"
className="expand-handle-icon"
/>
)}
</calcite-button>
)
)}
<div className="view-buttons-container">
{config.views.map(
(
view //The .map function creates the contained items multiple times, one button for each view in this case.
) => (
<calcite-button
key={view.id}
data-view-id={view.id}
className={`view-button ${activeViewId === view.id ? "active" : ""
}`}
title={view.name}
onClick={() => {
if (activeViewId === view.id) {
handleViewExpand(view)
return
}
handleViewChange(view)
}}
appearance={activeViewId === view.id ? "solid" : "transparent"}
kind={activeViewId === view.id ? "brand" : "neutral"}
>
{view.icon && (
<Icon icon={view.icon.svg} size="16" className="mr-2" />
)}
{view.name}
{view.expandLayerIds?.length > 0 && (
<Icon
icon={expand && activeViewId === view.id ? chevronUpSvg : chevronDownSvg}
size="12"
className="expand-handle-icon"
/>
)}
</calcite-button>
)
)}
</div>
</div>
</div>
)
Expand Down
Loading
Loading