|
| 1 | +import { createContext, useContext, useMemo, useState } from 'react' |
| 2 | +import type { ReactNode } from 'react' |
| 3 | + |
| 4 | +import { allPlatforms } from '@/tools/lib/all-platforms' |
| 5 | +import { allTools } from '@/tools/lib/all-tools' |
| 6 | + |
| 7 | +// React-native replacement for the imperative platform/tool visibility toggling |
| 8 | +// that PlatformPicker/ToolPicker used to do by walking the DOM and setting |
| 9 | +// `style.display` on `.ghd-tool`/`.platform-*`/`.tool-*` elements (#6619). The |
| 10 | +// selected platform + tool live in this context; the article body (rendered from |
| 11 | +// hast) maps the relevant elements to <ToggleableContent>, which reads the |
| 12 | +// selection and hides non-matching content instead of mutating React-owned nodes. |
| 13 | +// |
| 14 | +// Selections start empty so that the server render and the first client render |
| 15 | +// both show ALL variants (matching the pre-JS markup), which keeps hydration |
| 16 | +// stable. The pickers set the real selection in an effect after hydration, the |
| 17 | +// same moment the old imperative code used to run. |
| 18 | + |
| 19 | +export type SelectionContextT = { |
| 20 | + platform: string |
| 21 | + tool: string |
| 22 | + setPlatform: (value: string) => void |
| 23 | + setTool: (value: string) => void |
| 24 | +} |
| 25 | + |
| 26 | +const noop = () => {} |
| 27 | + |
| 28 | +export const SelectionContext = createContext<SelectionContextT>({ |
| 29 | + platform: '', |
| 30 | + tool: '', |
| 31 | + setPlatform: noop, |
| 32 | + setTool: noop, |
| 33 | +}) |
| 34 | + |
| 35 | +export function SelectionProvider({ children }: { children: ReactNode }) { |
| 36 | + const [platform, setPlatform] = useState('') |
| 37 | + const [tool, setTool] = useState('') |
| 38 | + |
| 39 | + const value = useMemo<SelectionContextT>( |
| 40 | + () => ({ platform, tool, setPlatform, setTool }), |
| 41 | + [platform, tool], |
| 42 | + ) |
| 43 | + |
| 44 | + return <SelectionContext.Provider value={value}>{children}</SelectionContext.Provider> |
| 45 | +} |
| 46 | + |
| 47 | +export function useSelection(): SelectionContextT { |
| 48 | + return useContext(SelectionContext) |
| 49 | +} |
| 50 | + |
| 51 | +const platformSet = new Set<string>(allPlatforms) |
| 52 | +const toolSet = new Set<string>(Object.keys(allTools)) |
| 53 | + |
| 54 | +export type ToggleClassification = { |
| 55 | + scope: 'platform' | 'tool' |
| 56 | + value: string |
| 57 | +} |
| 58 | + |
| 59 | +function toClassList(className: unknown): string[] { |
| 60 | + if (Array.isArray(className)) return className.map(String) |
| 61 | + if (typeof className === 'string') return className.split(/\s+/).filter(Boolean) |
| 62 | + return [] |
| 63 | +} |
| 64 | + |
| 65 | +// Determine whether an element is platform/tool-scoped and which value gates it. |
| 66 | +// `.ghd-tool <value>` is a block (the {% mac %}/{% webui %} liquid tags); the |
| 67 | +// extra class is the platform or tool value. `platform-<value>`/`tool-<value>` |
| 68 | +// are author-written inline spans. We classify strictly against the canonical |
| 69 | +// platform/tool vocabularies and return null on anything outside them, so an |
| 70 | +// unrecognized class never makes content disappear. When several recognized |
| 71 | +// markers are present the first match wins; in practice an element carries |
| 72 | +// exactly one platform/tool marker. |
| 73 | +export function classifyToggleClass(className: unknown): ToggleClassification | null { |
| 74 | + const classes = toClassList(className) |
| 75 | + if (!classes.length) return null |
| 76 | + |
| 77 | + if (classes.includes('ghd-tool')) { |
| 78 | + const platform = classes.find((c) => platformSet.has(c)) |
| 79 | + if (platform) return { scope: 'platform', value: platform } |
| 80 | + const tool = classes.find((c) => toolSet.has(c)) |
| 81 | + if (tool) return { scope: 'tool', value: tool } |
| 82 | + return null |
| 83 | + } |
| 84 | + |
| 85 | + for (const c of classes) { |
| 86 | + if (c.startsWith('platform-')) { |
| 87 | + const value = c.slice('platform-'.length) |
| 88 | + if (platformSet.has(value)) return { scope: 'platform', value } |
| 89 | + } |
| 90 | + if (c.startsWith('tool-')) { |
| 91 | + const value = c.slice('tool-'.length) |
| 92 | + if (toolSet.has(value)) return { scope: 'tool', value } |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + return null |
| 97 | +} |
| 98 | + |
| 99 | +export function isToggleClass(className: unknown): boolean { |
| 100 | + return classifyToggleClass(className) !== null |
| 101 | +} |
| 102 | + |
| 103 | +// Visible when no selection has been made yet (initial render shows everything, |
| 104 | +// matching the pre-JS markup) or when the element's value is the selected one. |
| 105 | +export function isContentVisible( |
| 106 | + classification: ToggleClassification, |
| 107 | + selection: { platform: string; tool: string }, |
| 108 | +): boolean { |
| 109 | + const selected = classification.scope === 'platform' ? selection.platform : selection.tool |
| 110 | + if (!selected) return true |
| 111 | + return classification.value === selected |
| 112 | +} |
0 commit comments