Refactor to use new primer navlist

Luke Karrys committed Oct 16, 2023 at 14:01 UTC 3adbd1d5febc0aba966172d9376763f54f351c84
16 files changed +334 -375
CONTRIBUTING.md
+13 -1
@@ -27,7 +27,19 @@ First, `npm install` the dependencies. This will install Gatsby, et al.
27
28 Next, `npm run develop` to start the test server to view your changes. The Gatsby server will be started on port 8000. You can navigate to `http://localhost:8000` to view the site live.
29
30 -In order to cut down on the time it takes `npm run develop` to complete, you can use the environment variable `GATSBY_PARTIAL_CONTENT` to only build some pages. For example, if the only pages you need to test locally are in `/cli/v10/commands`, then you can run the site locally with `GATSBY_PARTIAL_CONTENT=cli/v10/commands npm run develop`.
30 +In order to cut down on the time it takes `npm run develop` to complete, you can use the environment variables `GATSBY_CONTENT_ALLOW` and `GATSBY_CONTENT_IGNORE` to only build some pages.
31 +
32 +For example, if the only pages you need to test locally are in `/cli/v10/commands`, then you can run the the following to build the `cli/v10/commands` pages:
33 +
34 +```sh
35 +GATSBY_CONTENT_ALLOW=cli/v10/commands npm run develop
36 +```
37 +
38 +The `content/cli` directory has the most pages so it tends to be most helpful to ignore older CLI versions unless you need to explicitly test those pages:
39 +
40 +```sh
41 +GATSBY_CONTENT_IGNORE=cli/v6,cli/v7,cli/v8,cli/v9 npm run develop
42 +```
43
44 **For best results use Node 18 and npm 9+**
45
gatsby-config.js
+17 -7
@@ -1,7 +1,7 @@
1 const path = require('path')
2 const fs = require('fs')
3
4 -const {NODE_ENV, GATSBY_PARTIAL_CONTENT, GATSBY_CONTENT_IGNORE, GATSBY_CONTENT_DIR = 'content'} = process.env
4 +const {NODE_ENV, GATSBY_CONTENT_ALLOW, GATSBY_CONTENT_IGNORE, GATSBY_CONTENT_DIR = 'content'} = process.env
5 const DEV = NODE_ENV === 'development'
6 const CONTENT_DIR = path.resolve(GATSBY_CONTENT_DIR)
7
@@ -15,28 +15,38 @@ const walkDirs = dir => {
15 }
16
17 const getContentOptions = () => {
18 - if (!DEV || (!GATSBY_PARTIAL_CONTENT && !GATSBY_CONTENT_IGNORE)) {
18 + if (!DEV || (!GATSBY_CONTENT_ALLOW && !GATSBY_CONTENT_IGNORE)) {
19 return
20 }
21
22 - const partialContent = (GATSBY_PARTIAL_CONTENT ?? '').split(',')
22 + const allowContent = (GATSBY_CONTENT_ALLOW ?? '').split(',').filter(Boolean)
23 + const ignoreContent = (GATSBY_CONTENT_IGNORE ?? '').split(',').filter(Boolean)
24
25 const paths = walkDirs(CONTENT_DIR)
26 .map(p => path.relative(CONTENT_DIR, p))
27 .sort()
28 .reduce(
29 (acc, p) => {
29 - const include = partialContent.some(partial => partial.startsWith(p))
30 - acc[include ? 'include' : 'ignore'].push(p)
30 + const allow = allowContent.length ? allowContent.includes(p) : null
31 + const ignore = ignoreContent.length ? ignoreContent.includes(p) : null
32 + if (ignore === true || allow === false) {
33 + acc.ignore.push(p)
34 + } else {
35 + acc.include.push(p)
36 + }
37 return acc
38 },
39 {include: [], ignore: []},
40 )
41
36 - console.log(`Only including the following partial content in dev mode:\n - ${paths.include.join('\n - ')}`)
42 + const ignoreGlobs = paths.ignore.map(p => path.join('**', p, '**'))
43 +
44 + console.log(`Only including the following partial content in dev mode`)
45 + console.log(`Allow:\n - ${paths.include.join('\n - ')}`)
46 + console.log(`Ignore:\n - ${ignoreGlobs.join('\n - ')}`)
47
48 return {
39 - ignore: paths.ignore,
49 + ignore: ignoreGlobs,
50 }
51 }
52
src/components/breadcrumbs.js new
+30
@@ -0,0 +1,30 @@
1 +import React from 'react'
2 +import {Breadcrumbs as PrimerBreadcrumbs} from '@primer/react'
3 +import {withPrefix} from 'gatsby'
4 +import * as getNav from '../util/get-nav'
5 +import {useLocation} from '../layout'
6 +
7 +const Breadcrumbs = () => {
8 + const {pathname} = useLocation()
9 + const items = getNav.getItemBreadcrumbs(pathname)
10 +
11 + if (items.length <= 1) {
12 + return null
13 + }
14 +
15 + return (
16 + <PrimerBreadcrumbs sx={{mb: 4}}>
17 + {items.map(item => (
18 + <PrimerBreadcrumbs.Item
19 + key={item.url}
20 + href={withPrefix(item.url)}
21 + selected={getNav.isActiveUrl(pathname, item.url)}
22 + >
23 + {item.title}
24 + </PrimerBreadcrumbs.Item>
25 + ))}
26 + </PrimerBreadcrumbs>
27 + )
28 +}
29 +
30 +export default Breadcrumbs
src/components/nav-items.js
+38 -138
@@ -1,159 +1,59 @@
1 import React from 'react'
2 import {Link as GatsbyLink} from 'gatsby'
3 -import {Box, Octicon, Link, themeGet} from '@primer/react'
3 +import {NavList} from '@primer/react/drafts'
4 import {LinkExternalIcon} from '@primer/octicons-react'
5 -import styled from 'styled-components'
6 -import getNav from '../util/get-nav'
5 +import * as getNav from '../util/get-nav'
6 import {useLocation, usePageContext} from '../layout'
7 +import VisuallyHidden from './visually-hidden'
8
9 -const getActiveClass = props => {
10 - const location = getNav.getLocation(props.location.pathname)
11 - const href = getNav.getLocation(props.href)
12 - return getNav.isActiveUrl(location, href) ? 'active' : ''
13 -}
14 -
15 -const ActiveLink = ({className, children, ...props}) => (
16 - <Link as={GatsbyLink} getProps={p => ({className: `${className} ${getActiveClass(p)}`})} {...props}>
17 - {children}
18 - </Link>
19 -)
9 +const NavItem = ({item, path}) => {
10 + const href = getNav.getLocation(item.url)
11 + const isCurrent = getNav.isActiveUrl(path, href)
12 + const items = getNav.getHierarchy(item, {path: item.url, hideVariants: true})
13
21 -const withItems = Component => {
22 - const WithItems = ({parent, path}) => {
23 - if (!parent || getNav.isActiveUrl(path, parent.url)) {
24 - const items = getNav.getHierarchy(parent, {path, hideVariants: true})
25 - if (items) {
26 - return <Component items={items} path={path} />
27 - }
28 - }
29 - return null
30 - }
31 - return WithItems
14 + return (
15 + <NavList.Item as={GatsbyLink} to={href} defaultOpen={items && isCurrent} aria-current={isCurrent ? 'page' : null}>
16 + {item.title}
17 + {items ? (
18 + <NavList.SubNav>
19 + <NavItems items={items} path={path} />
20 + </NavList.SubNav>
21 + ) : null}
22 + </NavList.Item>
23 + )
24 }
25
34 -const NavLink = styled(ActiveLink)`
35 - color: inherit;
36 - text-decoration: none;
37 - :hover {
38 - text-decoration: underline;
39 - }
40 -`
41 -
42 -const TopLevelLink = styled(NavLink)`
43 - &.active {
44 - font-weight: ${themeGet('fontWeights.bold')};
45 - color: ${themeGet('colors.gray.8')};
46 - }
47 - &.activePage {
48 - color: ${themeGet('colors.gray.8')};
49 - }
50 -`
51 -
52 -const TopLevelItems = withItems(({items, path}) => (
26 +const NavItems = ({items, path}) => (
27 <>
28 {items.map(item => (
55 - <Box
56 - key={item.title}
57 - role="listitem"
58 - sx={{
59 - borderStyle: 'solid',
60 - borderColor: 'border.default',
61 - borderWidth: 0,
62 - borderRadius: 0,
63 - borderTopWidth: 1,
64 - py: 3,
65 - px: 4,
66 - }}
67 - >
68 - <Box sx={{display: 'flex', flexDirection: 'column'}}>
69 - <TopLevelLink to={item.url} key={item.title}>
70 - {item.title}
71 - </TopLevelLink>
72 - <SecondLevelItems parent={item} path={path} />
73 - </Box>
74 - </Box>
29 + <NavItem key={item.title} item={item} path={path} />
30 ))}
31 </>
77 -))
78 -
79 -const SecondLevelLink = styled(NavLink)`
80 - display: block;
81 - font-size: ${themeGet('fontSizes.1')};
82 - padding-top: ${themeGet('space.1')};
83 - padding-bottom: ${themeGet('space.1')};
84 - margin-top: ${themeGet('space.2')};
85 - &.active {
86 - font-weight: ${themeGet('fontWeights.bold')};
87 - color: ${themeGet('colors.gray.8')};
88 - }
89 -`
90 -
91 -const Description = styled(Box)`
92 - & {
93 - color: ${themeGet('colors.gray.6')};
94 - font-size: 0.8em;
95 - font-weight: normal;
96 - }
97 -`
98 -
99 -const SecondLevelItems = withItems(({items, path}) => (
100 - <Box sx={{display: 'flex', flexDirection: 'column', mt: 2}} role="list">
101 - {items.map(item => (
102 - <Box key={item.title} role="listitem">
103 - <SecondLevelLink key={item.url} to={item.url}>
104 - {item.title}
105 - {item.description ? <Description>{item.description}</Description> : null}
106 - </SecondLevelLink>
107 - <ThirdLevelItems parent={item} path={path} />
108 - </Box>
109 - ))}
110 - </Box>
111 -))
112 -
113 -const ThirdLevelLink = styled(NavLink)`
114 - display: block;
115 - font-size: ${themeGet('fontSizes.1')};
116 - padding-top: ${themeGet('space.1')};
117 - padding-bottom: ${themeGet('space.1')};
118 - border-left: solid 1px ${themeGet('colors.gray.3')};
119 - padding-left: calc(${themeGet('space.2')} + (${themeGet('space.1')} - 1px));
120 - color: ${themeGet('colors.blue.5')};
121 - &.active {
122 - border-left: solid ${themeGet('space.1')} ${themeGet('colors.gray.3')};
123 - padding-left: ${themeGet('space.2')};
124 - color: ${themeGet('colors.gray.8')};
125 - }
126 -`
127 -
128 -const ThirdLevelItems = withItems(({items}) => (
129 - <Box sx={{display: 'flex', flexDirection: 'column', mt: 2}} role="list">
130 - {items.map(item => (
131 - <Box key={item.title} role="listitem">
132 - <ThirdLevelLink key={item.url} to={item.url}>
133 - {item.title}
134 - </ThirdLevelLink>
135 - </Box>
136 - ))}
137 - </Box>
138 -))
32 +)
33
140 -function NavItems() {
141 - const {repositoryUrl} = usePageContext()
34 +const Navigation = () => {
35 const location = useLocation()
36 + const {repositoryUrl} = usePageContext()
37 + const path = getNav.getLocation(location.pathname)
38 + const items = getNav.getHierarchy(null, {path, hideVariants: true})
39
40 return (
41 <>
146 - <TopLevelItems path={getNav.getLocation(location.pathname)} />
147 - <Box sx={{borderStyle: 'solid', borderColor: 'border.default', borderWidth: 0, borderTopWidth: 1, py: 3, px: 4}}>
148 - <Link href={repositoryUrl} sx={{color: 'inherit'}}>
149 - <Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: 'gray.5'}}>
150 - GitHub
151 - <Octicon icon={LinkExternalIcon} sx={{color: 'gray.5'}} />
152 - </Box>
153 - </Link>
154 - </Box>
42 + <VisuallyHidden>
43 + <h3>Site navigation</h3>
44 + </VisuallyHidden>
45 + <NavList aria-label="Site">
46 + <NavItems items={items} path={path} />
47 + <NavList.Divider />
48 + <NavList.Item href={repositoryUrl}>
49 + GitHub
50 + <NavList.TrailingVisual>
51 + <LinkExternalIcon />
52 + </NavList.TrailingVisual>
53 + </NavList.Item>
54 + </NavList>
55 </>
56 )
57 }
58
159 -export default NavItems
59 +export default Navigation
src/components/search-results.js
+3 -8
@@ -1,18 +1,13 @@
1 import React from 'react'
2 import {Box, Text} from '@primer/react'
3 import useSiteMetadata from '../hooks/use-site-metadata'
4 -import getNav from '../util/get-nav'
4 +import * as getNav from '../util/get-nav'
5
6 const Breadcrumbs = ({item, highlighted}) => {
7 const siteMetadata = useSiteMetadata()
8 - const hierarchy = getNav.getItemHierarchy(item.path)
8 + const hierarchy = getNav.getItemBreadcrumbs(item.path)
9
10 - const text = hierarchy
11 - ? hierarchy
12 - .slice(0, -1)
13 - .map(item => item.shortName || item.title)
14 - .join(' / ')
15 - : siteMetadata.shortName
10 + const text = hierarchy ? hierarchy.slice(0, -1).join(' / ') : siteMetadata.shortName
11
12 return <Text sx={{fontSize: 0, color: highlighted ? 'blue.2' : 'gray.7'}}>{text}</Text>
13 }
src/components/sidebar.js
-1
@@ -47,7 +47,6 @@ const Sidebar = () => (
47 height: '100%',
48 borderStyle: 'solid',
49 borderColor: 'border.subtle',
50 - px: 2,
50 }}
51 >
52 <Box sx={{display: 'flex', flexDirection: 'column'}} role="list">
src/components/table-of-contents.js
-1
@@ -62,7 +62,6 @@ export const Desktop = withTableOfContents(({items}) => (
62 pr: 1,
63 pl: 1,
64 pb: 1,
65 - gridArea: 'table-of-contents',
65 overflow: 'auto',
66 }}
67 >
src/components/variant-select.js
+1 -1
@@ -1,7 +1,7 @@
1 import React from 'react'
2 import {ActionList, ActionMenu, Box} from '@primer/react'
3 import {navigate} from 'gatsby'
4 -import getNav from '../util/get-nav'
4 +import * as getNav from '../util/get-nav'
5 import {useLocation} from '../layout'
6
7 const VariantItem = ({match, active}) => {
src/components/visually-hidden.js new
+16
@@ -0,0 +1,16 @@
1 +import styled from 'styled-components'
2 +
3 +/** Visually hide an element, but keep it accessible to screen readers. */
4 +const VisuallyHidden = styled.div`
5 + position: absolute;
6 + width: 1px;
7 + height: 1px;
8 + padding: 0;
9 + margin: -1px;
10 + overflow: hidden;
11 + clip: rect(0, 0, 0, 0);
12 + white-space: nowrap;
13 + border-width: 0;
14 +`
15 +
16 +export default VisuallyHidden
src/constants.js
+1 -1
@@ -1,5 +1,5 @@
1 export const HEADER_HEIGHT = 66
2
3 -export const SKIP_NAV = 'skip-nav'
3 +export const SKIP_NAV = {id: 'skip-nav', as: 'main'}
4
5 export const NPM_RED = '#cb0000'
src/layout.js
+1 -4
@@ -4,7 +4,6 @@ import {Box} from '@primer/react'
4 import Slugger from 'github-slugger'
5 import Header from './components/header'
6 import Sidebar from './components/sidebar'
7 -import {SKIP_NAV} from './constants'
7 import useSiteMetdata from './hooks/use-site-metadata'
8
9 const SluggerContext = React.createContext(null)
@@ -48,9 +47,7 @@ const withLayout = Component => {
47 <Box sx={{display: ['none', null, null, 'block']}}>
48 <Sidebar />
49 </Box>
51 - <Box sx={{width: '100%'}} id={SKIP_NAV}>
52 - <Component {...props} />
53 - </Box>
50 + <Component {...props} />
51 </Box>
52 </Box>
53 </LocationContext.Provider>
src/layout/default.js
+11 -13
@@ -3,7 +3,9 @@ import {Box, Heading} from '@primer/react'
3 import PageFooter from '../components/page-footer'
4 import * as TableOfContents from '../components/table-of-contents'
5 import VariantSelect from '../components/variant-select'
6 +import Breadcrumbs from '../components/breadcrumbs'
7 import withLayout from '../layout'
8 +import {SKIP_NAV} from '../constants'
9
10 const Layout = ({children, pageContext: {frontmatter}}) => {
11 const {title, description} = frontmatter
@@ -24,23 +26,19 @@ const Layout = ({children, pageContext: {frontmatter}}) => {
26 alignSelf: 'start',
27 }}
28 >
29 + <Box css={{gridArea: 'table-of-contents'}}>
30 + <TableOfContents.Desktop />
31 + </Box>
32 <Box css={{gridArea: 'heading'}}>
28 - <Box
29 - sx={{
30 - borderStyle: 'solid',
31 - borderColor: 'border.default',
32 - borderWidth: 0,
33 - borderBottomWidth: 1,
34 - borderRadius: 0,
35 - pb: 2,
36 - }}
37 - >
38 - <Heading as="h1">{title}</Heading>
39 - {description}
33 + <Box {...SKIP_NAV} sx={{mb: 4}}>
34 + <Breadcrumbs />
35 + <Heading as="h1" sx={{fontSize: 7}}>
36 + {title}
37 + </Heading>
38 + {description ? <Box sx={{fontSize: 3, mb: 3}}>{description}</Box> : null}
39 </Box>
40 <VariantSelect />
41 </Box>
43 - <TableOfContents.Desktop />
42 <Box css={{gridArea: 'content'}}>
43 <TableOfContents.Mobile />
44 {children}
src/layout/hero.js
+2 -1
@@ -3,9 +3,10 @@ import {Box} from '@primer/react'
3 import Container from '../components/container'
4 import Hero from '../components/hero'
5 import withLayout from '../layout'
6 +import {SKIP_NAV} from '../constants'
7
8 const HeroLayout = ({children}) => (
8 - <Box sx={{width: '100%'}}>
9 + <Box sx={{width: '100%'}} {...SKIP_NAV}>
10 <Hero />
11 <Container>{children}</Container>
12 </Box>
src/mdx/index.js
+1 -1
@@ -24,7 +24,7 @@ export const Pre = ({children}) => children
24 const SkipLinkBase = props => (
25 <Link
26 {...props}
27 - href={`#${SKIP_NAV}`}
27 + href={`#${SKIP_NAV.id}`}
28 sx={{
29 p: 3,
30 color: 'fg.onEmphasis',
src/mdx/nav-hierarchy.js
+10 -14
@@ -1,10 +1,10 @@
1 import React from 'react'
2 import {Box, Link} from '@primer/react'
3 import {Link as GatsbyLink} from 'gatsby'
4 -import getNav from '../util/get-nav'
4 +import * as getNav from '../util/get-nav'
5 import {useLocation} from '../layout'
6
7 -const HierarchyItem = ({item, currentDepth, ...props}) => {
7 +const HierarchyItem = ({item, depth, ...props}) => {
8 const hierarchy = getNav.getHierarchy(item, props)
9
10 return (
@@ -13,38 +13,34 @@ const HierarchyItem = ({item, currentDepth, ...props}) => {
13 {item.title}
14 </Link>
15 {item.description ? <Box style={{fontSize: '0.85em', marginBottom: '0.5em'}}>{item.description}</Box> : null}
16 - {hierarchy ? <Hierarchy items={hierarchy} currentDepth={currentDepth + 1} {...props} /> : null}
16 + {hierarchy ? <Hierarchy items={hierarchy} depth={depth + 1} {...props} /> : null}
17 </Box>
18 )
19 }
20
21 -const Hierarchy = ({items, currentDepth = 1, ...props}) => {
22 - if (props.depth && currentDepth > props.depth) {
21 +const Hierarchy = ({items, ...props}) => {
22 + if (props.maxDepth && props.depth > props.maxDepth) {
23 return null
24 }
25
26 return (
27 <Box as="ul">
28 {items.map(item => (
29 - <HierarchyItem key={item.url} item={item} currentDepth={currentDepth} {...props} />
29 + <HierarchyItem key={item.url} item={item} {...props} />
30 ))}
31 </Box>
32 )
33 }
34
35 -function NavHierarchy(props) {
35 +function NavHierarchy({root, depth, ...props}) {
36 const location = useLocation()
37 const path = getNav.getLocation(location.pathname)
38 - const root = (props.root ? props.root : path).replace(/\/+$/g, '')
38 + const navRoot = (root || path).replace(/\/+$/g, '')
39
40 - const rootItem = getNav.getItem(root)
40 + const rootItem = getNav.getItem(navRoot)
41 const hierarchy = getNav.getHierarchy(rootItem, props)
42
43 - if (!hierarchy) {
44 - throw new Error(`could not find entry for ${root}`)
45 - }
46 -
47 - return <Hierarchy items={hierarchy} {...props} />
43 + return <Hierarchy items={hierarchy} maxDepth={depth} depth={1} {...props} />
44 }
45
46 export default NavHierarchy
src/util/get-nav.js
+190 -184
@@ -1,253 +1,259 @@
1 import {withPrefix} from 'gatsby'
2 import navItems from '../../content/nav.yml'
3
4 -const NavHierarchy = {
5 - getLocation(path) {
6 - const pathPrefix = withPrefix('/')
7 -
8 - if (!pathPrefix || pathPrefix === '/') {
9 - return path
10 - }
11 -
12 - const match = new RegExp(`^${pathPrefix}`)
13 - return path.replace(match, '/')
14 - },
4 +export const getItemBreadcrumbs = path => {
5 + const hierarchy = getItemHierarchy(path)
6 + return hierarchy
7 + ? hierarchy.map(item => {
8 + item.title = item.shortName || item.title
9 + return item
10 + })
11 + : null
12 +}
13
16 - getPath(path) {
17 - while (path && path.endsWith('/')) {
18 - path = path.substring(0, path.length - 1)
19 - }
14 +export const getLocation = path => {
15 + const pathPrefix = withPrefix('/')
16
17 + if (!pathPrefix || pathPrefix === '/') {
18 return path
22 - },
19 + }
20
24 - getVariantRoot(path) {
25 - path = this.getPath(path)
26 -
27 - return this.findItem(item => {
28 - if (item.variants && path.startsWith(`${item.url}/`)) {
29 - return item.url
30 - }
21 + const match = new RegExp(`^${pathPrefix}`)
22 + return path.replace(match, '/')
23 +}
24
32 - return null
33 - })
34 - },
25 +export const getPath = path => {
26 + while (path && path.endsWith('/')) {
27 + path = path.substring(0, path.length - 1)
28 + }
29
36 - findItem(fn, items = navItems) {
37 - for (let i = 0; i < items.length; i++) {
38 - const item = items[i]
39 - let result = fn(item)
30 + return path
31 +}
32
41 - if (!result && item.children) {
42 - result = this.findItem(fn, item.children)
43 - }
33 +export const getVariantRoot = path => {
34 + path = getPath(path)
35
45 - if (result) {
46 - return result
47 - }
36 + return findItem(item => {
37 + if (item.variants && path.startsWith(`${item.url}/`)) {
38 + return item.url
39 }
40
41 return null
51 - },
42 + })
43 +}
44 +
45 +export const findItem = (fn, items = navItems) => {
46 + for (let i = 0; i < items.length; i++) {
47 + const item = items[i]
48 + let result = fn(item)
49
53 - getItemHierarchy(path, items = navItems) {
54 - if (!path) {
55 - return null
50 + if (!result && item.children) {
51 + result = findItem(fn, item.children)
52 }
53
58 - for (let i = 0; i < items.length; i++) {
59 - const item = items[i]
54 + if (result) {
55 + return result
56 + }
57 + }
58
61 - if (this.getPath(item.url) === this.getPath(path)) {
62 - return [item]
63 - }
59 + return null
60 +}
61
65 - const children = item.variants ? item.variants : item.children
62 +export const getItemHierarchy = (path, items = navItems) => {
63 + if (!path) {
64 + return null
65 + }
66
67 - if (children) {
68 - const hierarchy = this.getItemHierarchy(path, children)
67 + for (let i = 0; i < items.length; i++) {
68 + const item = items[i]
69
70 - if (hierarchy) {
71 - return [item, ...hierarchy]
72 - }
73 - }
70 + if (getPath(item.url) === getPath(path)) {
71 + return [item]
72 }
73
76 - return null
77 - },
78 -
79 - getItem(path, items = navItems) {
80 - if (!path) {
81 - return {url: '/', children: items}
82 - }
74 + const children = item.variants ? item.variants : item.children
75
84 - for (let i = 0; i < items.length; i++) {
85 - const item = items[i]
76 + if (children) {
77 + const hierarchy = getItemHierarchy(path, children)
78
87 - if (this.getPath(item.url) === this.getPath(path)) {
88 - return item
79 + if (hierarchy) {
80 + return [item, ...hierarchy]
81 }
82 + }
83 + }
84
91 - const children = item.variants ? item.variants : item.children
85 + return null
86 +}
87
93 - if (children) {
94 - const child = this.getItem(path, children)
88 +export const getItem = (path, items = navItems) => {
89 + if (!path) {
90 + return {url: '/', children: items}
91 + }
92
96 - if (child) {
97 - return child
98 - }
99 - }
93 + for (let i = 0; i < items.length; i++) {
94 + const item = items[i]
95 +
96 + if (getPath(item.url) === getPath(path)) {
97 + return item
98 }
99
102 - return null
103 - },
100 + const children = item.variants ? item.variants : item.children
101
105 - isPathForItem(path, item) {
106 - return this.getPath(item.url) === this.getPath(path)
107 - },
102 + if (children) {
103 + const child = getItem(path, children)
104
109 - isChildItem(path, items = navItems) {
110 - if (!path) {
111 - return false
105 + if (child) {
106 + return child
107 + }
108 }
109 + }
110
114 - return this.findItem(item => (this.isPathForItem(path, item) ? item : null), items) != null
115 - },
111 + return null
112 +}
113
117 - getHierarchy(root, props = {}) {
118 - let children
114 +export const isPathForItem = (path, item) => {
115 + return getPath(item.url) === getPath(path)
116 +}
117
120 - if (!root) {
121 - children = navItems
122 - } else if (root.variants && props.hideVariants === true) {
123 - const variant = this.getCurrentOrDefaultVariant(root, props.path)
124 - children = variant.children
125 - } else if (root.variants) {
126 - children = root.variants
127 - } else {
128 - children = root.children
129 - }
118 +export const isChildItem = (path, items = navItems) => {
119 + if (!path) {
120 + return false
121 + }
122
131 - if (children && props.hideVariants === true) {
132 - children = this.hideVariantsForItems(children, props)
133 - }
123 + return findItem(item => (isPathForItem(path, item) ? item : null), items) != null
124 +}
125
135 - return children
136 - },
126 +export const getHierarchy = (root, props = {}) => {
127 + let children
128 +
129 + if (!root) {
130 + children = navItems
131 + } else if (root.variants && props.hideVariants === true) {
132 + const variant = getCurrentOrDefaultVariant(root, props.path)
133 + children = variant.children
134 + } else if (root.variants) {
135 + children = root.variants
136 + } else {
137 + children = root.children
138 + }
139 +
140 + if (children && props.hideVariants === true) {
141 + children = hideVariantsForItems(children, props.path)
142 + }
143 +
144 + return children
145 +}
146
138 - hideVariantsForItems(items, props) {
139 - if (!items) {
140 - return null
141 - }
147 +export const hideVariantsForItems = (items, path) => {
148 + if (!items) {
149 + return null
150 + }
151
143 - const updated = []
152 + const updated = []
153
145 - for (const item of items) {
146 - if (item.variants) {
147 - const {url} = this.getCurrentOrDefaultVariant(item, props.path)
148 - updated.push({
149 - ...item,
150 - url,
151 - })
152 - } else {
153 - updated.push(item)
154 - }
154 + for (const item of items) {
155 + if (item.variants) {
156 + const {url} = getCurrentOrDefaultVariant(item, path)
157 + updated.push({
158 + ...item,
159 + url,
160 + })
161 + } else {
162 + updated.push(item)
163 }
164 + }
165
157 - return updated
158 - },
166 + return updated
167 +}
168
160 - getCurrentOrDefaultVariant(root, path) {
161 - let variant = path ? this.getCurrentVariant(root, path) : null
169 +export const getCurrentOrDefaultVariant = (root, path) => {
170 + let variant = path ? getCurrentVariant(root, path) : null
171
163 - if (!variant) {
164 - variant = this.getDefaultVariant(root)
165 - }
172 + if (!variant) {
173 + variant = getDefaultVariant(root)
174 + }
175
167 - return variant
168 - },
176 + return variant
177 +}
178
170 - getCurrentVariant(root, path) {
171 - for (const v of root.variants) {
172 - if (this.isActiveItem(path, v)) {
173 - return v
174 - }
179 +export const getCurrentVariant = (root, path) => {
180 + for (const v of root.variants) {
181 + if (isActiveItem(path, v)) {
182 + return v
183 }
184 + }
185
177 - return null
178 - },
186 + return null
187 +}
188
180 - getDefaultVariant(root) {
181 - for (const v of root.variants) {
182 - if (v.default) {
183 - return v
184 - }
189 +export const getDefaultVariant = root => {
190 + for (const v of root.variants) {
191 + if (v.default) {
192 + return v
193 }
194 + }
195
187 - return root.variants[0]
188 - },
189 -
190 - getVariantAndPage(root, path) {
191 - if (!root || !path.startsWith(`${root}/`)) {
192 - return null
193 - }
196 + return root.variants[0]
197 +}
198
195 - path = path.substring(root.length + 1)
199 +export const getVariantAndPage = (root, path) => {
200 + if (!root || !path.startsWith(`${root}/`)) {
201 + return null
202 + }
203
197 - const match = /^([^/]+)(?:\/(.*))?/.exec(path)
204 + path = path.substring(root.length + 1)
205
199 - if (!match) {
200 - return null
201 - }
206 + const match = /^([^/]+)(?:\/(.*))?/.exec(path)
207
203 - return {variant: match[1], page: match[2]}
204 - },
208 + if (!match) {
209 + return null
210 + }
211
206 - getVariantsForPage(root, page) {
207 - const pages = []
208 - const rootItem = this.findItem(item => (this.getPath(item.url) === this.getPath(root) ? item : null))
212 + return {variant: match[1], page: match[2]}
213 +}
214
210 - if (rootItem && rootItem.variants) {
211 - for (const variant of rootItem.variants) {
212 - if (!variant.children) {
213 - continue
214 - }
215 +export const getVariantsForPage = (root, page) => {
216 + const pages = []
217 + const rootItem = findItem(item => (getPath(item.url) === getPath(root) ? item : null))
218
216 - const vp = this.getVariantAndPage(root, variant.url)
217 - let variantPage
219 + if (rootItem && rootItem.variants) {
220 + for (const variant of rootItem.variants) {
221 + if (!variant.children) {
222 + continue
223 + }
224
219 - if (vp.page === page) {
220 - variantPage = variant
221 - } else {
222 - variantPage = this.findItem(item => {
223 - const itemVp = this.getVariantAndPage(root, item.url)
224 - return itemVp && itemVp.page === page ? item : null
225 - }, variant.children)
226 - }
225 + const vp = getVariantAndPage(root, variant.url)
226 + let variantPage
227
228 - if (!variantPage) {
229 - continue
230 - }
228 + if (vp.page === page) {
229 + variantPage = variant
230 + } else {
231 + variantPage = findItem(item => {
232 + const itemVp = getVariantAndPage(root, item.url)
233 + return itemVp && itemVp.page === page ? item : null
234 + }, variant.children)
235 + }
236
232 - pages.push({variant, page: variantPage})
237 + if (!variantPage) {
238 + continue
239 }
234 - }
240
236 - return pages
237 - },
241 + pages.push({variant, page: variantPage})
242 + }
243 + }
244
239 - isActiveItem(currentPath, linkItem) {
240 - return (
241 - this.isPathForItem(currentPath, linkItem) ||
242 - (linkItem.children && this.isChildItem(currentPath, linkItem.children)) ||
243 - (linkItem.variants && this.isChildItem(currentPath, linkItem.variants))
244 - )
245 - },
245 + return pages
246 +}
247
247 - isActiveUrl(currentPath, linkPath) {
248 - const linkItem = this.getItem(linkPath)
249 - return linkItem ? this.isActiveItem(currentPath, linkItem) : false
250 - },
248 +export const isActiveItem = (currentPath, linkItem) => {
249 + return (
250 + isPathForItem(currentPath, linkItem) ||
251 + (linkItem.children && isChildItem(currentPath, linkItem.children)) ||
252 + (linkItem.variants && isChildItem(currentPath, linkItem.variants))
253 + )
254 }
255
253 -export default NavHierarchy
256 +export const isActiveUrl = (currentPath, linkPath) => {
257 + const linkItem = getItem(linkPath)
258 + return linkItem ? isActiveItem(currentPath, linkItem) : false
259 +}