diff --git a/src/theme/DocItem/TagsWrapper.js b/src/theme/DocItem/TagsWrapper.js new file mode 100644 index 0000000000..74069322bf --- /dev/null +++ b/src/theme/DocItem/TagsWrapper.js @@ -0,0 +1,61 @@ +import React from "react"; + +function TagsWrapper({props, activeIDs}) { + const {children, ...rest} = props; // do we need 'rest' for anything? + + const removeEmptyH6 = (arr) => { + return arr.filter(i => !(i.props && i.props.originalType === 'h6' && i.props.children === '')); + } + + if (!activeIDs) { + const newProps = {...props, children: removeEmptyH6(children)}; + return ; // page does not contain any tags or nothing is selected or everything is selected. + } + + const isNextChildLower = (nextItem, currentHeader) => { + const headers = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + if (!headers.includes(nextItem.toLowerCase())) { + return true; // next item is not a header + } else if (headers.indexOf(currentHeader.toLowerCase()) < headers.indexOf(nextItem.toLowerCase())) { + return true; // next item is header but it is lower + } else { + return false; + } + } + + const addHighterHeaders = (acc, headerLevel, index, activeChildren) => { + if (headerLevel < 3) { + return acc.reverse(); + } + const highterHeader = children.slice(0, index).filter(i => i.props.originalType === `h${headerLevel - 1}`).pop(); + const isHeaderAlreadyDisplayed = !!(highterHeader && activeChildren && activeChildren.filter(c => c.props.id === highterHeader.props.id).length); + const accumulatedHeaders = highterHeader && !isHeaderAlreadyDisplayed ? [...acc, highterHeader] : acc; + return addHighterHeaders(accumulatedHeaders, headerLevel - 1, index); + } + + const activeChildren = children.reduce((acc, child, index) => { + // Works only with headers that aren't hidden under
+ // Could be modified to walk recursively through child.props.children if it exists, but i doubt that we need it. + + if (acc.showChild && (!child.props || isNextChildLower(child.props.originalType, acc.currentHeaderType))) { + acc.showChild = true; + } else if (child.props && activeIDs.includes(child.props.id)) { + acc.showChild = true; + acc.currentHeaderType = child.props.originalType; + const highterHeaders = addHighterHeaders([], parseInt(child.props.originalType.slice(1, 2), 10), index, acc.children); + acc.children = [...acc.children, ...highterHeaders]; + } else { + acc.showChild = false; + // Note: Manipulating children style is possible like that by adding the child.props.style, i.e. to gray out some of them instead of filtering. + // newChild = {...child, props: {...child.props, style: {opacity: '0.3'}}} + } + return acc.showChild ? {...acc, children: [...acc.children, child]} : acc; + + }, {showChild: false, currentHeaderType: '', children: []}); + + activeChildren.children.push(

Not found what you was looking for? Check the active filters

); + + return +}; + +export default TagsWrapper; \ No newline at end of file diff --git a/src/theme/DocItem/index.js b/src/theme/DocItem/index.js index 90f27caa55..e3908b241f 100644 --- a/src/theme/DocItem/index.js +++ b/src/theme/DocItem/index.js @@ -12,6 +12,7 @@ import TOC from "@theme/TOC"; import clsx from "clsx"; import styles from "./styles.module.css"; import { useActivePlugin, useVersions } from "@theme/hooks/useDocs"; +import TagsWrapper from "./TagsWrapper"; //Components import DocsInfo from "./DocsInfo"; @@ -57,6 +58,57 @@ function DocItem(props) { ); }); + const getActiveIDs = (selectedComponents) => { + if (!metadata.frontMatter.meta || !metadata.frontMatter.meta[0].tags || !Object.keys(metadata.frontMatter.meta[0].tags).length) { + return undefined; // No tags defined for that page + } + if (!selectedComponents || !(selectedComponents.tags instanceof Object)) { + return undefined; // No selectedComponents or object has wrong structure + } + const numberOfSelectedComponents = Object.values(selectedComponents.tags).filter(i => i.value).length; + const activeDownloadType = selectedComponents.downloadType && selectedComponents.downloadType.toLowerCase() !== 'all'; + const activeESM = selectedComponents.esm && selectedComponents.esm.toLowerCase() !== 'all'; + if (!numberOfSelectedComponents && !activeDownloadType && !activeESM) { + return undefined; // Nothing is selected + } + const activeTags = Object.keys(selectedComponents.tags).filter(tag => selectedComponents.tags[tag].value).map(tag => tag.toLowerCase()); + activeDownloadType && activeTags.push(selectedComponents.downloadType.toLowerCase()); + activeESM && activeTags.push(selectedComponents.esm.toLowerCase()); + const tagsDictionary = metadata.frontMatter.meta[0].tags; + const lowerCaseTagsDict = Object.keys(tagsDictionary).reduce((acc, i) => ({...acc, [i.toLowerCase()]: tagsDictionary[i]}), {}); + const activeHeaders = activeTags.reduce((acc, tag) => lowerCaseTagsDict[tag] ? [...acc, ...lowerCaseTagsDict[tag]] : acc, []); + return activeHeaders.map(i => i.toLowerCase().replaceAll(' ', '-').replaceAll(/[^\w-]/ig, '')); + } + + const getTOC = (activeIDs, toc) => { + if (!activeIDs) { + return toc; + } + const filterChildren = (arr) => { + return arr.reduce((acc, i) => { + if (activeIDs.includes(i.id)) { + acc.push(i); + } else if (i.children.length) { + const c = filterChildren(i.children); + if (c.length) { + acc.push({...i, children: c}); + } + } + return acc; + }, []); + } + return filterChildren(toc); + } + + const [activeIDs, setActiveIDs] = useState(getActiveIDs(JSON.parse(window.sessionStorage.getItem('ZoweDocs::selectedComponents') || "{}"))); + + window.onstorage = (e) => { + const newComponentsSelection = window.sessionStorage.getItem('ZoweDocs::selectedComponents'); + if (newComponentsSelection) { + setActiveIDs(getActiveIDs(JSON.parse(newComponentsSelection))); + } + }; + return ( <> @@ -108,7 +160,9 @@ function DocItem(props) { title={title} /> )} - + + }}>
@@ -125,7 +179,7 @@ function DocItem(props) { {!hideTableOfContents && DocContent.toc && (
- +
)} diff --git a/src/theme/DocSidebar/ComponentSelector.js b/src/theme/DocSidebar/ComponentSelector.js new file mode 100644 index 0000000000..af7766c16f --- /dev/null +++ b/src/theme/DocSidebar/ComponentSelector.js @@ -0,0 +1,111 @@ +import React, { useState } from "react"; +import styles from './styles.module.css'; + +// Notes: +// DocSideBarItem can take optional props - customProps: item={{...item, customProps: {testprop: true}}} +// Rewrite in TS? + +// FIXME: Cross dependency of Components and ESMs/Download types. Selecting of ESM should not affect components if no other ESMs are specified for page / topic? +const ESMs = ['All', 'RACF', 'TopSecret', 'ACF2']; +const downloadTypes = ['All', 'SMPE', 'Pax', 'Container']; +const makeNewComponentsObject = () => { + const item = { + tags: { + cli: {label: "CLI", value: false}, + desktop: {label: "Desktop", value: false}, + explorer: {label: "Explorer", value: false}, + apiml: {label: "Mediation Layer", value: false}, + zss: {label: "ZSS", value: false} + }, + esm: ESMs[0], + downloadType: downloadTypes[0], + } + window.sessionStorage.setItem("ZoweDocs::selectedComponents", JSON.stringify(item)); + return item; +} + +const ComponentSelector = () => { + const currentComponents = window.sessionStorage.getItem("ZoweDocs::selectedComponents"); + const [components, setComponents] = useState(currentComponents ? JSON.parse(currentComponents) : makeNewComponentsObject()); + const [showTagsSelector, toggleTagsSelector] = useState(false); + + const setStorage = item => { + window.sessionStorage.setItem("ZoweDocs::selectedComponents", item); + window.dispatchEvent( new Event('storage') ); + setComponents(JSON.parse(window.sessionStorage.getItem("ZoweDocs::selectedComponents"))); + } + + const setTags = tag => { + const tags = components.tags; + tags[tag].value = !tags[tag].value; + const item = JSON.stringify({...components, tags}); + setStorage(item); + } + + const setESM = type => { + const item = JSON.stringify({...components, esm: type}); + setStorage(item); + } + + const setDownloadType = type => { + const item = JSON.stringify({...components, downloadType: type}); + setStorage(item); + } + + return ( +
+ + ); +}; + +export default ComponentSelector; diff --git a/src/theme/DocSidebar/index.js b/src/theme/DocSidebar/index.js new file mode 100644 index 0000000000..8cddf05d4b --- /dev/null +++ b/src/theme/DocSidebar/index.js @@ -0,0 +1,354 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import React, {useState, useCallback, useEffect, useRef, memo} from 'react'; +import clsx from 'clsx'; +import { + useThemeConfig, + isSamePath, + usePrevious, + useAnnouncementBar, +} from '@docusaurus/theme-common'; +import useLockBodyScroll from '@theme/hooks/useLockBodyScroll'; +import useWindowSize, {windowSizes} from '@theme/hooks/useWindowSize'; +import useScrollPosition from '@theme/hooks/useScrollPosition'; +import Link from '@docusaurus/Link'; +import isInternalUrl from '@docusaurus/isInternalUrl'; +import Logo from '@theme/Logo'; +import IconArrow from '@theme/IconArrow'; +import IconMenu from '@theme/IconMenu'; +import IconExternalLink from '@theme/IconExternalLink'; +import {translate} from '@docusaurus/Translate'; +import styles from './styles.module.css'; +const MOBILE_TOGGLE_SIZE = 24; +import ComponentSelector from './ComponentSelector'; + +const isActiveSidebarItem = (item, activePath) => { + if (item.type === 'link') { + return isSamePath(item.href, activePath); + } + + if (item.type === 'category') { + return item.items.some((subItem) => + isActiveSidebarItem(subItem, activePath), + ); + } + + return false; +}; // Optimize sidebar at each "level" +// TODO this item should probably not receive the "activePath" props +// TODO this triggers whole sidebar re-renders on navigation + +const DocSidebarItems = memo(function DocSidebarItems({items, ...props}) { + return items.map((item, index) => ( + + )); +}); + +function DocSidebarItem(props) { + switch (props.item.type) { + case 'category': + return ; + + case 'link': + default: + return ; + } +} + +function DocSidebarItemCategory({ + item, + onItemClick, + collapsible, + activePath, + ...props +}) { + const {items, label} = item; + const isActive = isActiveSidebarItem(item, activePath); + const wasActive = usePrevious(isActive); // active categories are always initialized as expanded + // the default (item.collapsed) is only used for non-active categories + + const [collapsed, setCollapsed] = useState(() => { + if (!collapsible) { + return false; + } + + return isActive ? false : item.collapsed; + }); + const menuListRef = useRef(null); + const [menuListHeight, setMenuListHeight] = useState(undefined); + + const handleMenuListHeight = (calc = true) => { + setMenuListHeight( + calc ? `${menuListRef.current?.scrollHeight}px` : undefined, + ); + }; // If we navigate to a category, it should automatically expand itself + + useEffect(() => { + const justBecameActive = isActive && !wasActive; + + if (justBecameActive && collapsed) { + setCollapsed(false); + } + }, [isActive, wasActive, collapsed]); + const handleItemClick = useCallback( + (e) => { + e.preventDefault(); + + if (!menuListHeight) { + handleMenuListHeight(); + } + + setTimeout(() => setCollapsed((state) => !state), 100); + }, + [menuListHeight], + ); + + if (items.length === 0) { + return null; + } + + return ( +
  • + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + + {label} + +
      { + if (!collapsed) { + handleMenuListHeight(false); + } + }}> + +
    +
  • + ); +} + +function DocSidebarItemLink({ + item, + onItemClick, + activePath, + collapsible: _collapsible, + ...props +}) { + const {href, label} = item; + const isActive = isActiveSidebarItem(item, activePath); + return ( +
  • + + {isInternalUrl(href) ? ( + label + ) : ( + + {label} + + + )} + +
  • + ); +} + +function useShowAnnouncementBar() { + const {isClosed} = useAnnouncementBar(); + const [showAnnouncementBar, setShowAnnouncementBar] = useState(!isClosed); + useScrollPosition(({scrollY}) => { + if (!isClosed) { + setShowAnnouncementBar(scrollY === 0); + } + }); + return showAnnouncementBar; +} + +function useResponsiveSidebar() { + const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false); + useLockBodyScroll(showResponsiveSidebar); + const windowSize = useWindowSize(); + useEffect(() => { + if (windowSize === windowSizes.desktop) { + setShowResponsiveSidebar(false); + } + }, [windowSize]); + const closeResponsiveSidebar = useCallback( + (e) => { + e.target.blur(); + setShowResponsiveSidebar(false); + }, + [setShowResponsiveSidebar], + ); + const toggleResponsiveSidebar = useCallback(() => { + setShowResponsiveSidebar((value) => !value); + }, [setShowResponsiveSidebar]); + return { + showResponsiveSidebar, + closeResponsiveSidebar, + toggleResponsiveSidebar, + }; +} + +function HideableSidebarButton({onClick}) { + return ( + + ); +} + +function ResponsiveSidebarButton({responsiveSidebarOpened, onClick}) { + return ( + + ); +} + +function DocSidebar({ + path, + sidebar, + sidebarCollapsible = true, + onCollapse, + isHidden, +}) { + const showAnnouncementBar = useShowAnnouncementBar(); + const { + navbar: {hideOnScroll}, + hideableSidebar, + } = useThemeConfig(); + const {isClosed: isAnnouncementBarClosed} = useAnnouncementBar(); + const { + showResponsiveSidebar, + closeResponsiveSidebar, + toggleResponsiveSidebar, + } = useResponsiveSidebar(); + return ( +
    + {hideOnScroll && } + + {hideableSidebar && } +
    + ); +} + +export default DocSidebar; diff --git a/src/theme/DocSidebar/styles.module.css b/src/theme/DocSidebar/styles.module.css new file mode 100644 index 0000000000..2b51d3377f --- /dev/null +++ b/src/theme/DocSidebar/styles.module.css @@ -0,0 +1,186 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +:root { + --collapse-button-bg-color-dark: #2e333a; +} + +@media (min-width: 997px) { + .sidebar { + display: flex; + flex-direction: column; + max-height: 100vh; + height: 100%; + position: sticky; + top: 0; + padding-top: var(--ifm-navbar-height); + width: var(--doc-sidebar-width); + transition: opacity 50ms ease; + } + + .sidebarWithHideableNavbar { + padding-top: 0; + } + + .sidebarHidden { + opacity: 0; + height: 0; + overflow: hidden; + visibility: hidden; + } + + .sidebarLogo { + display: flex !important; + align-items: center; + margin: 0 var(--ifm-navbar-padding-horizontal); + min-height: var(--ifm-navbar-height); + max-height: var(--ifm-navbar-height); + color: inherit !important; + text-decoration: none !important; + } + + .sidebarLogo img { + margin-right: 0.5rem; + height: 2rem; + } + + .menu { + flex-grow: 1; + padding: 0.5rem; + } + + .menuLinkText { + cursor: initial; + } + + .menuLinkText:hover { + background: none; + } + + .menuWithAnnouncementBar { + margin-bottom: var(--docusaurus-announcement-bar-height); + } + + .tagsSelectorContainer { + padding: 24px 0px 0px; + } + + .tagsSelectorLabel { + margin: 0; + color: black; + } + + [data-theme="dark"] .tagsSelectorLabel { + color: inherit; + } + + .collapsibleTagsSelector { + cursor: pointer; + padding: 4px 16px; + border-radius: 4px; + display: flex; + justify-content: space-between; + align-items: center; + } + + .collapsibleTagsSelector:hover { + background: var(--ifm-menu-color-background-active); + } + + .tagsSeparator { + border-top: solid 1px var(--ifm-toc-border-color); + margin: 0 16px 8px 16px; + } + + .tagsSection { + padding-bottom: 12px; + } + + .tagSectionLabel { + color: var(--ifm-font-color-base) + } + + .tagOption { + display: flex; + align-items: center; + height: 24px; + } + + .tagRadioInput { + margin: 0 7px 0 4px; + } + + .arrowIcon { + transition: transform 0.2s linear; + max-width: 20px; + max-height: 20px; + } + + .tagsSelectorContent { + color: var(--ifm-menu-color); + padding: 8px 16px 0 32px; + overflow: hidden; + transition: height 0.2s linear; + } + + .collapseSidebarButton { + display: block !important; + background-color: var(--ifm-button-background-color); + height: 40px; + position: sticky; + bottom: 0; + border-radius: 0; + border: 1px solid var(--ifm-toc-border-color); + } + + .collapseSidebarButtonIcon { + transform: rotate(180deg); + margin-top: 4px; + } + html[dir='rtl'] .collapseSidebarButtonIcon { + transform: rotate(0); + } + + html[data-theme='dark'] .collapseSidebarButton { + background-color: var(--collapse-button-bg-color-dark); + } + + html[data-theme='dark'] .collapseSidebarButton:hover, + html[data-theme='dark'] .collapseSidebarButton:focus { + background-color: var(--ifm-color-emphasis-200); + } +} + +.sidebarLogo, +.collapseSidebarButton { + display: none; +} + +.sidebarMenuIcon { + vertical-align: middle; +} + +.sidebarMenuCloseIcon { + display: inline-flex; + justify-content: center; + align-items: center; + height: 24px; + font-size: 1.5rem; + font-weight: var(--ifm-font-weight-bold); + line-height: 0.9; + width: 24px; +} + +:global(.menu__list) :global(.menu__list) { + overflow-y: hidden; + will-change: height; + transition: height var(--ifm-transition-fast) linear; +} + +:global(.menu__list-item--collapsed) :global(.menu__list) { + height: 0 !important; +}