src/lib/components/molecules/canvas-map/context/MapContext.jsx (111 lines of code) (raw):
import { createContext } from "preact"
import { useEffect, useState } from "preact/hooks"
/**
* @import { Layer } from "../lib/layers"
* @import { ReactNode } from "preact/compat"
*/
/**
* @typedef {{
* registerLayer: ((layer: Layer, comp: ReactNode) => void) | null,
* unregisterLayer: ((layer: Layer) => void) | null
* }} MapContext
*/
/**
* @type {import('preact').Context<MapContext | null>}
*/
export const MapContext = createContext(null)
/**
* @param {Object} params
* @param {import('../lib/Map').Map} params.map
* @param {import('preact').ComponentChildren} params.children
*/
export function MapProvider({ map, children }) {
const [layers, setLayers] = useState([])
const registerLayer = (layer, comp) => {
if (!layers.includes(layer)) {
const position = getCompTreePosition(comp, children)
if (position === null) {
console.warn(`failed to find target component in component tree`, comp)
return
}
setLayers((prevLayers) => {
// Insert the new layer at the correct position in the layers list
const newLayers = [...prevLayers]
newLayers.splice(position, 0, layer)
return newLayers
})
}
}
const unregisterLayer = (layerToRemove) => {
setLayers((prevLayers) =>
prevLayers.filter((layer) => layer !== layerToRemove),
)
}
useEffect(() => {
if (!map) return
map.setLayers(layers)
}, [map, layers])
return (
<MapContext.Provider value={{ registerLayer, unregisterLayer }}>
{children}
</MapContext.Provider>
)
}
/**
* Given a React component's children, find the in-order position of the target component in the
* component tree.
*
* Eg., for the following component tree, getCompTreePosition(C, A.children) will return 4:
*
* A
* / \
* B C
* / \
* D E
*
* @param {import('preact/compat').ReactNode} targetComponent
* @param {import('preact').ComponentChildren} children
*/
function getCompTreePosition(targetComponent, children, debug = false) {
let index = 0
let debugComponentPath = []
function traverse(nodes) {
for (const node of nodes) {
if (!node) continue
// Preact mangles property names, read as: node.component.vnode.children
const childNodes = node.__c?.__v?.__k
// If debug, keep track of the current search path
if (debug) {
let name =
node.__c?.constructor.displayName || node.__c?.constructor.name
if (name === "m") {
// "m" means Fragment, so store as "", so it'll be printed as </>
name = ""
}
debugComponentPath.push(name)
}
if (childNodes && childNodes.length > 0) {
// If node has children, traverse them. Nodes with no children have a .__k property of "[null]".
const result = traverse(childNodes)
if (result !== null) {
return result
}
}
// This __c property of a ReactNode returns the instance that's given by "this" in the component constructor
if (node?.__c === targetComponent) {
return index
}
if (debug) {
debugComponentPath.pop()
}
index++
}
return null
}
const result = traverse(Array.isArray(children) ? children : [children])
if (debug && result) {
// eslint-disable-next-line no-console
console.log(`<${debugComponentPath.join("/> → <")}/>`)
}
return result
}