import * as React from 'react' ; import { findCommentNode , arrayToObject , createLinkElement , isElement , } from './utils' ; enum Status { idle = 'idle' , loading = 'loading' , loaded = 'loaded' } interface IThemeSwitcherContext { currentTheme : string | undefined ; themes : Record < any , string > ; switcher : ( { theme } : { theme : string ; } ) => void ; status : Status ; } const ThemeSwitcherContext = React . createContext < IThemeSwitcherContext | undefined > ( undefined ) ; interface Props { themeMap : Record < any , string > ; children ? : React . ReactNode ; insertionPoint ? : string | HTMLElement | null ; id ? : string ; defaultTheme ? : string ; attr ? : string ; } export function ThemeSwitcherProvider ( { themeMap , insertionPoint , defaultTheme , id = 'current-theme-style' , attr = 'data-theme' , ... rest } ) { const [ status , setStatus ] = React . useState < Status > ( Status . idle ) ; const [ currentTheme , setCurrentTheme ] = React . useState < string > ( ) ; const insertStyle = React . useCallback ( ( linkElement ) : HTMLElement | void => { if ( insertionPoint || insertionPoint === null ) { const insertionPointElement = isElement ( insertionPoint ) ? ( insertionPoint as HTMLElement ) : findCommentNode ( insertionPoint as string ) ; if ( ! insertionPointElement ) { console . warn ( `Insertion point '${ insertionPoint }' does not exist. Be sure to add comment on head and that it matches the insertionPoint` ) ; return document . head . appendChild ( linkElement ) ; } const { parentNode } = insertionPointElement ; if ( parentNode ) { return parentNode . insertBefore ( linkElement , insertionPointElement . nextSibling ) ; } } else { return document . head . appendChild ( linkElement ) ; } } , [ insertionPoint ] ) ; const switcher = React . useCallback ( ( { theme } ) => { if ( theme === currentTheme ) return ; if ( themeMap [ theme ] ) { setStatus ( Status . loading ) ; const linkElement = createLinkElement ( { type : 'text/css' , rel : 'stylesheet' , id : id + '_temp' , href : themeMap [ theme ] , onload : ( ) => { const previousStyle = document . getElementById ( id ) ; if ( previousStyle ) { previousStyle . remove ( ) ; } const nextStyle = document . getElementById ( id + '_temp' ) ; if ( nextStyle ) { nextStyle . setAttribute ( 'id' , id ) ; } setStatus ( Status . loaded ) ; } } ) ; insertStyle ( linkElement ) ; setCurrentTheme ( theme ) ; } else { return console . warn ( 'Could not find specified theme' ) ; } document . body . setAttribute ( attr , theme ) ; } , [ themeMap , insertStyle , attr , id , currentTheme ] ) ; React . useEffect ( ( ) => { if ( defaultTheme ) { switcher ( { theme : defaultTheme } ) ; } } , [ defaultTheme ] ) ; React . useEffect ( ( ) => { const themes = Object . keys ( themeMap ) ; themes . map ( theme => { const themeAssetId = `theme-prefetch-${ theme }` ; if ( ! document . getElementById ( themeAssetId ) ) { const stylePrefetch = document . createElement ( 'link' ) ; stylePrefetch . rel = 'prefetch' ; stylePrefetch . type = 'text/css' ; stylePrefetch . id = themeAssetId ; stylePrefetch . href = themeMap [ theme ] ; insertStyle ( stylePrefetch ) ; } return '' ; } ) ; } , [ themeMap , insertStyle ] ) ; const value = React . useMemo ( ( ) => ( { switcher , status , currentTheme , themes : arrayToObject ( Object . keys ( themeMap ) ) } ) , [ switcher , status , currentTheme , themeMap ] ) ; return < ThemeSwitcherContext . Provider value = { value } { ... rest } />; } export function useThemeSwitcher ( ) { const context = React . useContext ( ThemeSwitcherContext ) ; if ( ! context ) { throw new Error ( 'To use `useThemeSwitcher`, component must be within a ThemeSwitcherProvider' ) ; } return context ; }