diff options
author | Thomas F. K. Jorna <[email protected]> | 2021-10-11 21:27:17 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2021-10-11 21:27:17 +0200 |
commit | 58b7030d45370072dee25214748670d6413343a9 (patch) | |
tree | 9632df7273415f4b197413c45ad11563af32d53a /components | |
parent | 89be3b67b2d10d35d72b5c54e1e166beeeef3095 (diff) | |
parent | 6e3dcf585c35620c6804f3c208e6882c29dfc17e (diff) |
Merge pull request #101 from org-roam/sidebar
feat: Add file preview functionality
Diffstat (limited to 'components')
-rw-r--r-- | components/Sidebar/Backlinks.tsx | 74 | ||||
-rw-r--r-- | components/Sidebar/Collapse.tsx | 146 | ||||
-rw-r--r-- | components/Sidebar/Link.tsx | 241 | ||||
-rw-r--r-- | components/Sidebar/Note.tsx | 73 | ||||
-rw-r--r-- | components/Sidebar/OrgImage.tsx | 42 | ||||
-rw-r--r-- | components/Sidebar/TagBar.tsx | 79 | ||||
-rw-r--r-- | components/Sidebar/Toolbar.tsx | 103 | ||||
-rw-r--r-- | components/Sidebar/index.tsx | 253 | ||||
-rw-r--r-- | components/Sidebar/noteStyle.ts | 139 | ||||
-rw-r--r-- | components/Sidebar/transition-utils.tsx | 144 | ||||
-rw-r--r-- | components/TagMenu.tsx | 117 | ||||
-rw-r--r-- | components/Tweaks/BehaviorPanel.tsx | 22 | ||||
-rw-r--r-- | components/Tweaks/ColorMenu.tsx | 73 | ||||
-rw-r--r-- | components/Tweaks/SliderWithInfo.tsx | 2 | ||||
-rw-r--r-- | components/Tweaks/index.tsx | 27 | ||||
-rw-r--r-- | components/config.ts | 14 | ||||
-rw-r--r-- | components/contextmenu.tsx | 249 |
17 files changed, 1644 insertions, 154 deletions
diff --git a/components/Sidebar/Backlinks.tsx b/components/Sidebar/Backlinks.tsx new file mode 100644 index 0000000..d82fbba --- /dev/null +++ b/components/Sidebar/Backlinks.tsx @@ -0,0 +1,74 @@ +import { LinksByNodeId, NodeByCite, NodeById } from '../../pages/index' + +import { GraphData, NodeObject, LinkObject } from 'force-graph' + +import { normalizeLinkEnds } from '../../pages/index' +import { VStack, Box, Button, Heading, StackDivider } from '@chakra-ui/react' +import React from 'react' +import { ProcessedOrg } from '../../util/processOrg' + +export interface BacklinksProps { + previewNode: any + setPreviewNode: any + nodeById: NodeById + linksByNodeId: LinksByNodeId + nodeByCite: NodeByCite + setSidebarHighlightedNode: OrgRoamNode + openContextMenu: any +} + +import { PreviewLink } from './Link' +import { OrgRoamNode } from '../../api' + +export const Backlinks = (props: BacklinksProps) => { + const { + previewNode, + setPreviewNode, + setSidebarHighlightedNode, + nodeById, + linksByNodeId, + nodeByCite, + openContextMenu, + } = props + const links = linksByNodeId[previewNode?.id] ?? [] + + const backLinks = links + .filter((link: LinkObject) => { + const [source, target] = normalizeLinkEnds(link) + return source !== previewNode?.id + }) + .map((l) => l.source) + + return ( + <Box> + <Heading pt={4}>{`Backlinks (${backLinks.length})`}</Heading> + <VStack + py={2} + spacing={3} + alignItems="start" + divider={<StackDivider borderColor="gray.500" />} + align="stretch" + color="gray.800" + > + {previewNode?.id && + backLinks.map((link) => { + const title = nodeById[link as string]?.title ?? '' + return ( + <Box overflow="hidden" p={3} bg="gray.300" width="100%" key={link}> + <PreviewLink + nodeByCite={nodeByCite} + setSidebarHighlightedNode={setSidebarHighlightedNode} + href={`id:${link as string}`} + nodeById={nodeById} + setPreviewNode={setPreviewNode} + openContextMenu={openContextMenu} + > + {nodeById[link as string]?.title} + </PreviewLink> + </Box> + ) + })} + </VStack> + </Box> + ) +} diff --git a/components/Sidebar/Collapse.tsx b/components/Sidebar/Collapse.tsx new file mode 100644 index 0000000..b297605 --- /dev/null +++ b/components/Sidebar/Collapse.tsx @@ -0,0 +1,146 @@ +// modified from https://github.com/chakra-ui/chakra-ui/blob/fc3b97d0978cf2adb9fc79157c6e42b4b68155c5/packages/transition/src/collapse.tsx + +import { cx, mergeWith, warn, __DEV__ } from '@chakra-ui/utils' +import { AnimatePresence, HTMLMotionProps, motion, Variants as _Variants } from 'framer-motion' +import * as React from 'react' +import { TransitionEasings, Variants, withDelay, WithTransitionConfig } from './transition-utils' + +const isNumeric = (value?: string | number) => value != null && parseInt(value.toString(), 10) > 0 + +export interface CollapseOptions { + /** + * If `true`, the opacity of the content will be animated + * @default true + */ + animateOpacity?: boolean + /** + * The size you want the content in its collapsed state. + * @default 0 + */ + startingSize?: number | string + /** + * The size you want the content in its expanded state. + * @default "auto" + */ + endingSize?: number | string + /** + * The dimension you want to collapse by. + * @default "size" + */ + dimension?: string +} + +const defaultTransitions = { + exit: { + size: { duration: 0.2, ease: TransitionEasings.ease }, + opacity: { duration: 0.3, ease: TransitionEasings.ease }, + }, + enter: { + size: { duration: 0.3, ease: TransitionEasings.ease }, + opacity: { duration: 0.4, ease: TransitionEasings.ease }, + }, +} + +const variants: Variants<CollapseOptions> = { + exit: ({ animateOpacity, startingSize, transition, transitionEnd, delay, dimension }) => ({ + ...(animateOpacity && { opacity: isNumeric(startingSize) ? 1 : 0 }), + overflow: 'hidden', + [dimension as string]: startingSize, + transitionEnd: transitionEnd?.exit, + transition: transition?.exit ?? withDelay.exit(defaultTransitions.exit, delay), + }), + enter: ({ animateOpacity, endingSize, transition, transitionEnd, delay, dimension }) => ({ + ...(animateOpacity && { opacity: 1 }), + [dimension as string]: endingSize, + transitionEnd: transitionEnd?.enter, + transition: transition?.enter ?? withDelay.enter(defaultTransitions.enter, delay), + }), +} + +export type ICollapse = CollapseProps + +export interface CollapseProps + extends WithTransitionConfig<HTMLMotionProps<'div'>>, + CollapseOptions {} + +export const Collapse = React.forwardRef<HTMLDivElement, CollapseProps>((props, ref) => { + const { + in: isOpen, + unmountOnExit, + animateOpacity = true, + startingSize = 0, + endingSize = 'auto', + dimension = 'height', + style, + className, + transition, + transitionEnd, + ...rest + } = props + + const [mounted, setMounted] = React.useState(false) + React.useEffect(() => { + const timeout = setTimeout(() => { + setMounted(true) + }) + return () => clearTimeout(timeout) + }, []) + + /** + * Warn 🚨: `startingSize` and `unmountOnExit` are mutually exclusive + * + * If you specify a starting size, the collapsed needs to be mounted + * for the size to take effect. + */ + warn({ + condition: Boolean(startingSize > 0 && unmountOnExit), + message: `startingSize and unmountOnExit are mutually exclusive. You can't use them together`, + }) + + const hasStartingSize = parseFloat(startingSize.toString()) > 0 + + const custom = { + startingSize, + endingSize, + animateOpacity, + dimension, + transition: !mounted ? { enter: { duration: 0 } } : transition, + transitionEnd: mergeWith(transitionEnd, { + enter: { overflow: 'initial' }, + exit: unmountOnExit + ? undefined + : { + display: hasStartingSize ? 'block' : 'none', + }, + }), + } + + const show = unmountOnExit ? isOpen : true + const animate = isOpen || unmountOnExit ? 'enter' : 'exit' + + return ( + <AnimatePresence initial={false} custom={custom}> + {show && ( + <motion.div + ref={ref} + {...rest} + className={cx('chakra-collapse', className)} + style={{ + overflow: 'hidden', + display: 'block', + ...style, + }} + custom={custom} + variants={variants as _Variants} + initial={unmountOnExit ? 'exit' : false} + animate={animate} + exit="exit" + /> + )} + </AnimatePresence> + ) +}) + +if (__DEV__) { + Collapse.displayName = 'Collapse' +} diff --git a/components/Sidebar/Link.tsx b/components/Sidebar/Link.tsx new file mode 100644 index 0000000..49fe9cf --- /dev/null +++ b/components/Sidebar/Link.tsx @@ -0,0 +1,241 @@ +/* eslint-disable react/display-name */ +import { + Box, + Button, + Link, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverHeader, + PopoverTrigger, + Portal, + Text, + useTheme, +} from '@chakra-ui/react' +import React, { ReactElement, useContext, useEffect, useMemo, useState } from 'react' + +import unified from 'unified' +//import createStream from 'unified-stream' +import uniorgParse from 'uniorg-parse' +import uniorg2rehype from 'uniorg-rehype' +//import highlight from 'rehype-highlight' +import katex from 'rehype-katex' +import 'katex/dist/katex.css' +import rehype2react from 'rehype-react' +import { ThemeContext } from '../../util/themecontext' +import { NodeByCite, NodeById } from '../../pages' + +export interface LinkProps { + href: any + children: any + previewNode?: any + setPreviewNode: any + setSidebarHighlightedNode: any + nodeByCite: NodeByCite + nodeById: NodeById + openContextMenu: any +} + +export interface NormalLinkProps { + setPreviewNode: any + nodeById: NodeById + nodeByCite: NodeByCite + href: any + children: any + setSidebarHighlightedNode: any + openContextMenu: any +} + +import { hexToRGBA, getThemeColor } from '../../pages/index' +import noteStyle from './noteStyle' +import { OrgImage } from './OrgImage' + +export const NormalLink = (props: NormalLinkProps) => { + const { setSidebarHighlightedNode, setPreviewNode, nodeById, openContextMenu, href, children } = + props + const { highlightColor } = useContext(ThemeContext) + + const theme = useTheme() + const coolHighlightColor = getThemeColor(highlightColor, theme) + const [whatever, type, uri] = [...href.matchAll(/(.*?)\:(.*)/g)][0] + return ( + <Text + onMouseEnter={() => setSidebarHighlightedNode(nodeById[uri])} + onMouseLeave={() => setSidebarHighlightedNode({})} + tabIndex={0} + display="inline" + overflow="hidden" + fontWeight={500} + color={highlightColor} + textDecoration="underline" + onContextMenu={(e) => { + e.preventDefault() + openContextMenu(nodeById[uri], e) + }} + onClick={() => setPreviewNode(nodeById[uri])} + // TODO don't hardcode the opacitycolor + _hover={{ textDecoration: 'none', cursor: 'pointer', bgColor: coolHighlightColor + '22' }} + _focus={{ outlineColor: highlightColor }} + > + {children} + </Text> + ) +} + +export const PreviewLink = (props: LinkProps) => { + const { + href, + children, + nodeById, + setSidebarHighlightedNode, + previewNode, + setPreviewNode, + nodeByCite, + openContextMenu, + } = props + // TODO figure out how to properly type this + // see https://github.com/rehypejs/rehype-react/issues/25 + const [orgText, setOrgText] = useState<any>(null) + const [whatever, type, uri] = [...href.matchAll(/(.*?)\:(.*)/g)][0] + const [hover, setHover] = useState(false) + + const getId = (type: string, uri: string) => { + if (type === 'id') { + return uri + } + + if (type.includes('cite')) { + const node = nodeByCite[uri] ?? false + if (!node) { + return '' + } + if (node?.properties.FILELESS) { + return '' + } + return node?.id + } + return '' + } + + const id = getId(type, uri) + const file = encodeURIComponent(encodeURIComponent(nodeById[id]?.file as string)) + + const processor = unified() + .use(uniorgParse) + .use(uniorg2rehype) + .use(katex) + .use(rehype2react, { + createElement: React.createElement, + components: { + // eslint-disable-next-line react/display-name + a: ({ children, href }) => ( + <PreviewLink + nodeByCite={nodeByCite} + setSidebarHighlightedNode={setSidebarHighlightedNode} + href={href} + nodeById={nodeById} + setPreviewNode={setPreviewNode} + openContextMenu={openContextMenu} + > + {children} + </PreviewLink> + ), + img: ({ src }) => { + return <OrgImage src={src as string} file={nodeById[id]?.file as string} /> + }, + }, + }) + + const getText = () => { + fetch(`http://localhost:35901/file/${file}`) + .then((res) => { + return res.text() + }) + .then((res) => { + if (res !== 'error') { + const text = processor.processSync(res).result + setOrgText(text) + return + } + }) + .catch((e) => { + console.log(e) + return 'Could not fetch the text for some reason, sorry!\n\n This can happen because you have an id with forward slashes (/) in it.' + }) + } + + useEffect(() => { + if (!!orgText) { + return + } + if (!hover) { + return + } + getText() + }, [hover, orgText]) + + if (id) { + return ( + <> + <Popover gutter={12} trigger="hover" placement="top-start"> + <PopoverTrigger> + <Box + display="inline" + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + <NormalLink + key={nodeById[id]?.title ?? id} + {...{ + setSidebarHighlightedNode, + setPreviewNode, + nodeById, + href, + children, + nodeByCite, + openContextMenu, + }} + /> + </Box> + </PopoverTrigger> + <Portal> + <PopoverContent + key={nodeById[id]?.title ?? id} + boxShadow="xl" + position="relative" + zIndex="tooltip" + onMouseEnter={() => { + setSidebarHighlightedNode(nodeById[id] ?? {}) + }} + onMouseLeave={() => { + setSidebarHighlightedNode({}) + }} + > + <PopoverArrow /> + <PopoverBody + pb={5} + fontSize="xs" + px={5} + position="relative" + zIndex="tooltip" + maxHeight={300} + overflow="scroll" + > + <Box color="black" sx={noteStyle}> + {orgText} + </Box> + </PopoverBody> + </PopoverContent> + </Portal> + </Popover> + </> + ) + } + return ( + <Text display="inline" color="base.700" cursor="not-allowed"> + {children} + </Text> + ) +} diff --git a/components/Sidebar/Note.tsx b/components/Sidebar/Note.tsx new file mode 100644 index 0000000..37e836d --- /dev/null +++ b/components/Sidebar/Note.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { NodeObject } from 'force-graph' + +import { NodeById, NodeByCite, LinksByNodeId } from '../../pages' +import { Box, Flex } from '@chakra-ui/react' +import { UniOrg } from '../../util/uniorg' +import { Backlinks } from '../../components/Sidebar/Backlinks' +import { noteStyle } from './noteStyle' + +export interface NoteProps { + setPreviewNode: any + previewNode: NodeObject + nodeById: NodeById + nodeByCite: NodeByCite + setSidebarHighlightedNode: any + justification: number + justificationList: string[] + linksByNodeId: LinksByNodeId + openContextMenu: any +} + +export const Note = (props: NoteProps) => { + const { + setPreviewNode, + justificationList, + justification, + previewNode, + nodeById, + nodeByCite, + setSidebarHighlightedNode, + linksByNodeId, + openContextMenu, + } = props + return ( + <Box + pr={8} + overflow="scroll" + height="100%" + className="org" + sx={{ + ...noteStyle, + + textAlign: justificationList[justification], + }} + > + {previewNode?.id && ( + <Flex height="100%" flexDirection="column" justifyContent="space-between"> + <UniOrg + {...{ + setPreviewNode, + previewNode, + nodeById, + nodeByCite, + setSidebarHighlightedNode, + openContextMenu, + }} + /> + <Backlinks + {...{ + setPreviewNode, + previewNode, + nodeById, + linksByNodeId, + nodeByCite, + setSidebarHighlightedNode, + openContextMenu, + }} + /> + </Flex> + )} + </Box> + ) +} diff --git a/components/Sidebar/OrgImage.tsx b/components/Sidebar/OrgImage.tsx new file mode 100644 index 0000000..f9f508a --- /dev/null +++ b/components/Sidebar/OrgImage.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from 'react' +import Image from 'next/image' +import path from 'path' +//import '../../../public/placeholder.png' + +export interface OrgImageProps { + src: string + file: string +} + +export const OrgImage = (props: OrgImageProps) => { + const { src, file } = props + + const [image, setImage] = useState<any>(null) + + /* ) +* .then((res) => res.blob()) +* .then((res) => setImage(res)) +* .catch((e) => { +* setImage(null) +* console.error(e) +* }) +}, [fullPath]) */ + + const dir = path.dirname(file) + const fullPath = encodeURIComponent(encodeURIComponent(path.join(dir, src))) + + const dumbLoader = ({ src, width, quality }: { [key: string]: string | number }) => { + return `http://localhost:35901/img/${src}` + } + + return ( + <Image + layout="responsive" + loader={dumbLoader} + src={fullPath} + alt="" + width="100%" + height="100%" + /> + ) +} diff --git a/components/Sidebar/TagBar.tsx b/components/Sidebar/TagBar.tsx new file mode 100644 index 0000000..0fe0a18 --- /dev/null +++ b/components/Sidebar/TagBar.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { initialFilter, TagColors } from '../config' +import { NodeObject } from 'force-graph' +import { OrgRoamNode } from '../../api' +import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' +import { Flex, Tag, TagLabel, TagRightIcon } from '@chakra-ui/react' + +export interface TagBarProps { + filter: typeof initialFilter + setFilter: any + tagColors: TagColors + setTagColors: any + openContextMenu: any + previewNode: NodeObject +} + +export const TagBar = (props: TagBarProps) => { + const { filter, setFilter, tagColors, setTagColors, openContextMenu, previewNode } = props + + const node = previewNode as OrgRoamNode + if (!node.tags || node?.tags[0] === null) { + return null + } + return ( + <Flex mb={2} flexWrap="wrap"> + {node?.tags?.map((tag: string) => { + const bl: string[] = filter.tagsBlacklist ?? [] + const wl: string[] = filter.tagsWhitelist ?? [] + const blackList: boolean = bl.includes(tag) + const whiteList = wl.includes(tag) + return ( + <Tag + tabIndex={0} + mr={2} + mt={2} + onContextMenu={(e) => { + e.preventDefault() + openContextMenu(tag, e) + }} + cursor="pointer" + onClick={() => { + if (blackList) { + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsBlacklist: filter.tagsBlacklist.filter((t) => t !== tag), + tagsWhitelist: [...filter.tagsWhitelist, tag], + })) + return + } + if (whiteList) { + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsWhitelist: filter.tagsWhitelist.filter((t) => t !== tag), + })) + return + } + + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsBlacklist: [...filter.tagsBlacklist, tag], + })) + }} + size="sm" + key={tag} + variant="outline" + colorScheme={tagColors[tag]?.replaceAll(/(.*?)\..*/g, '$1') || undefined} + > + <TagLabel>{tag}</TagLabel> + {blackList ? ( + <TagRightIcon as={ViewOffIcon} /> + ) : whiteList ? ( + <TagRightIcon as={ViewIcon} /> + ) : null} + </Tag> + ) + })} + </Flex> + ) +} diff --git a/components/Sidebar/Toolbar.tsx b/components/Sidebar/Toolbar.tsx new file mode 100644 index 0000000..6cbecae --- /dev/null +++ b/components/Sidebar/Toolbar.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import { Text, Flex, IconButton, ButtonGroup, Tooltip } from '@chakra-ui/react' +import { + BiAlignJustify, + BiAlignLeft, + BiAlignMiddle, + BiAlignRight, + BiFont, + BiRightIndent, +} from 'react-icons/bi' +import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons' +import { NodeObject } from 'force-graph' + +export interface ToolbarProps { + setJustification: any + justification: number + setIndent: any + setFont: any + setPreviewNode: any + canUndo: any + canRedo: any + resetPreviewNode: any + previousPreviewNode: any + nextPreviewNode: any +} + +export const Toolbar = (props: ToolbarProps) => { + const { + setJustification, + setIndent, + setFont, + justification, + setPreviewNode, + canUndo, + canRedo, + resetPreviewNode, + previousPreviewNode, + nextPreviewNode, + } = props + return ( + <Flex flex="0 1 40px" pb={3} alignItems="center" justifyContent="space-between" pl={1} pr={1}> + <Flex> + <ButtonGroup isAttached> + <Tooltip label="Go backward"> + <IconButton + variant="subtle" + icon={<ChevronLeftIcon />} + aria-label="Previous node" + disabled={!canUndo} + onClick={() => previousPreviewNode()} + /> + </Tooltip> + <Tooltip label="Go forward"> + <IconButton + variant="subtle" + icon={<ChevronRightIcon />} + aria-label="Next node" + disabled={!canRedo} + onClick={() => nextPreviewNode()} + /> + </Tooltip> + </ButtonGroup> + </Flex> + <Flex> + <Tooltip label="Justify content"> + <IconButton + variant="subtle" + aria-label="Justify content" + icon={ + [ + <BiAlignJustify key="justify" />, + <BiAlignLeft key="left" />, + <BiAlignRight key="right" />, + <BiAlignMiddle key="center" />, + ][justification] + } + onClick={() => setJustification((curr: number) => (curr + 1) % 4)} + /> + </Tooltip> + {/* <Tooltip label="Indent trees"> + <IconButton + variant="subtle" + aria-label="Indent Text" + icon={<BiRightIndent />} + onClick={() => { + setIndent((curr: number) => (curr ? 0 : 1)) + }} + /> + </Tooltip> + <Tooltip label="Switch betwwen sans and serif"> + <IconButton + variant="subtle" + aria-label="Change font" + icon={<BiFont />} + onClick={() => { + setFont((curr: string) => (curr === 'sans serif' ? 'serif' : 'sans serif')) + }} + /> + </Tooltip> */} + </Flex> + </Flex> + ) +} diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx new file mode 100644 index 0000000..957669c --- /dev/null +++ b/components/Sidebar/index.tsx @@ -0,0 +1,253 @@ +import React, { useContext, useEffect, useRef, useState } from 'react' + +import { Toolbar } from './Toolbar' +import { TagBar } from './TagBar' +import { Note } from './Note' + +import { + Button, + Slide, + VStack, + Flex, + Heading, + Box, + IconButton, + Tooltip, + HStack, + TagLabel, + Tag, + TagRightIcon, +} from '@chakra-ui/react' +import { Collapse } from './Collapse' +import { Scrollbars } from 'react-custom-scrollbars-2' +import { + ChevronLeftIcon, + ChevronRightIcon, + CloseIcon, + HamburgerIcon, + ViewIcon, + ViewOffIcon, +} from '@chakra-ui/icons' +import { BiDotsVerticalRounded, BiFile, BiNetworkChart } from 'react-icons/bi' +import { BsReverseLayoutSidebarInsetReverse } from 'react-icons/bs' + +import { GraphData, NodeObject, LinkObject } from 'force-graph' +import { OrgRoamNode } from '../../api' +import { ThemeContext } from '../../util/themecontext' +import { LinksByNodeId, NodeByCite, NodeById, Scope } from '../../pages/index' +import { Resizable } from 're-resizable' +import { usePersistantState } from '../../util/persistant-state' +import { initialFilter, TagColors } from '../config' + +export interface SidebarProps { + isOpen: boolean + onClose: any + onOpen: any + nodeById: NodeById + previewNode: NodeObject + setPreviewNode: any + linksByNodeId: LinksByNodeId + nodeByCite: NodeByCite + setSidebarHighlightedNode: any + canUndo: any + canRedo: any + resetPreviewNode: any + previousPreviewNode: any + nextPreviewNode: any + openContextMenu: any + scope: Scope + setScope: any + windowWidth: number + filter: typeof initialFilter + setFilter: any + tagColors: TagColors + setTagColors: any +} + +const Sidebar = (props: SidebarProps) => { + const { + isOpen, + onOpen, + onClose, + previewNode, + setPreviewNode, + nodeById, + linksByNodeId, + nodeByCite, + setSidebarHighlightedNode, + canUndo, + canRedo, + resetPreviewNode, + previousPreviewNode, + nextPreviewNode, + openContextMenu, + scope, + setScope, + windowWidth, + filter, + setFilter, + tagColors, + setTagColors, + } = props + + const { highlightColor } = useContext(ThemeContext) + const [previewRoamNode, setPreviewRoamNode] = useState<OrgRoamNode>() + const [sidebarWidth, setSidebarWidth] = usePersistantState<number>('sidebarWidth', 400) + + useEffect(() => { + if (!previewNode?.id) { + onClose() + return + } + onOpen() + setPreviewRoamNode(previewNode as OrgRoamNode) + }, [previewNode?.id]) + + const [justification, setJustification] = useState(1) + const justificationList = ['justify', 'start', 'end', 'center'] + const [font, setFont] = useState('sans serif') + const [indent, setIndent] = useState(0) + //maybe want to close it when clicking outside, but not sure + //const outsideClickRef = useRef(); + return ( + <Collapse + animateOpacity={false} + dimension="width" + in={isOpen} + //style={{ position: 'relative' }} + unmountOnExit + startingSize={0} + style={{ height: '100vh' }} + > + <Resizable + size={{ height: '100vh', width: sidebarWidth }} + onResizeStop={(e, direction, ref, d) => { + setSidebarWidth((curr: number) => curr + d.width) + }} + enable={{ + top: false, + right: false, + bottom: false, + left: true, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }} + minWidth="220px" + maxWidth={windowWidth - 200} + > + <Flex flexDir="column" h="100vh" pl={2} color="black" bg="alt.100" width="100%"> + <Flex + //whiteSpace="nowrap" + // overflow="hidden" + // textOverflow="ellipsis" + pl={4} + alignItems="center" + color="black" + width="100%" + > + <Flex flexShrink={0}> + <BiFile + onContextMenu={(e) => { + e.preventDefault() + openContextMenu(previewNode, e) + }} + /> + </Flex> + <Flex + whiteSpace="nowrap" + textOverflow="ellipsis" + overflow="hidden" + onContextMenu={(e) => { + e.preventDefault() + openContextMenu(previewNode, e) + }} + > + <Heading + pl={2} + whiteSpace="nowrap" + textOverflow="ellipsis" + overflow="hidden" + lineHeight={1} + size="sm" + fontWeight={600} + color={'gray.800'} + > + {previewRoamNode?.title} + </Heading> + </Flex> + <Flex flexDir="row" ml="auto"> + <IconButton + // eslint-disable-next-line react/jsx-no-undef + m={1} + icon={<BiDotsVerticalRounded />} + aria-label="Options" + variant="subtle" + onClick={(e) => { + openContextMenu(previewNode, e, { + left: undefined, + top: 12, + right: -windowWidth + 20, + bottom: undefined, + }) + }} + /> + </Flex> + </Flex> + <Toolbar + {...{ + setJustification, + setIndent, + setFont, + justification, + setPreviewNode, + canUndo, + canRedo, + resetPreviewNode, + previousPreviewNode, + nextPreviewNode, + }} + /> + {/* <Scrollbars + * //autoHeight + * autoHeightMax={600} + * autoHide + * renderThumbVertical={({ style, ...props }) => ( + * <Box + * style={{ + * ...style, + * borderRadius: 0, + * // backgroundColor: highlightColor, + * }} + * //color="alt.100" + * {...props} + * /> + * )} + * > */} + <VStack flexGrow={1} overflow="scroll" alignItems="left" bg="alt.100" paddingLeft={4}> + <TagBar + {...{ filter, setFilter, tagColors, setTagColors, openContextMenu, previewNode }} + /> + <Note + {...{ + setPreviewNode, + previewNode, + nodeById, + nodeByCite, + setSidebarHighlightedNode, + justification, + justificationList, + linksByNodeId, + openContextMenu, + }} + /> + </VStack> + {/*</Scrollbars>*/} + </Flex> + </Resizable> + </Collapse> + ) +} + +export default Sidebar diff --git a/components/Sidebar/noteStyle.ts b/components/Sidebar/noteStyle.ts new file mode 100644 index 0000000..42ecaf6 --- /dev/null +++ b/components/Sidebar/noteStyle.ts @@ -0,0 +1,139 @@ +export const noteStyle = { + '.katex': { overflowX: 'scroll' }, + h1: { color: 'black', lineHeight: '1.2', fontSize: '20', fontWeight: 'bold', marginBottom: 3 }, + h2: { + fontSize: '18', + marginBottom: 2, + color: 'black', + }, + h3: { + fontSize: '16', + fontWeight: '600 !important', + marginBottom: '.5em', + + color: 'black', + }, + h4: { + fontSize: '14', + fontWeight: '500 !important', + marginBottom: '.25em', + fontStyle: 'italic', + color: 'black', + }, + ol: { + paddingLeft: '5', + }, + ul: { + paddingLeft: '5', + }, + p: { + fontSize: '14', + fontWeight: '500 !important', + paddingBottom: '.5em', + }, + div: { + hyphens: 'auto !important', + }, + '.title': { + textAlign: 'center', + marginBottom: '.2em', + }, + '.subtitle': { + textAlign: 'center', + fontSize: 'medium', + fontWeight: 'bold', + marginTop: 0, + }, + '.TODO': { color: 'red.500' }, + '.equationContainer': { + display: 'table', + textAlign: 'center', + width: '100%', + }, + '.equation': { + verticalAlign: 'middle', + }, + '.equation-label': { + display: 'tableCell', + textAlign: 'right', + verticalAlign: 'middle', + }, + '.inlinetask': { + padding: '10px', + border: '2px solid gray', + margin: '10px', + background: '#ffffcc', + }, + '#org-div-home-and-up': { + textAlign: 'right', + fontSize: '70 % ', + whiteSpace: 'nowrap', + }, + textarea: { overflowX: 'auto' }, + '.linenr': { fontSize: 'smaller' }, + '.org-info-js_info-navigation': { borderStyle: 'none' }, + '#org-info-js_console-label': { + fontSize: '10px', + fontWeight: 'bold', + whiteSpace: 'nowrap', + }, + '.org-info-js_search-highlight': { + backgroundColor: '#ffff00', + color: '#000000', + fontWeight: 'bold', + }, + '.org-svg': { width: '90%' }, + '.DONE': { color: 'green' }, + '.priority': { fontFamily: 'monospace', color: 'orange' }, + '.tag': { + backgroundColor: 'white', + fontFamily: 'monospace', + padding: '2px', + fontSize: '80%', + fontWeight: 'normal', + }, + '.timestamp': { color: '#bebebe' }, + '.timestamp-kwd': { color: '#5f9ea0' }, + '.org-right': { marginLeft: 'auto', marginRight: '0px', textAlign: 'right' }, + '.org-left': { marginLeft: '0px', marginRight: 'auto', textAlign: 'left' }, + '.org-center': { marginLeft: 'auto', marginRight: 'auto', textAlign: 'center' }, + '.underline': { textDecoration: 'underline' }, + '#postamble p': { fontSize: '90%', margin: '.2em' }, + '#preamble p': { fontSize: '90%', margin: '.2em' }, + 'p.verse': { marginLeft: '3%' }, + pre: { + //border: '1px solid #e6e6e6', + borderRadius: '3px', + backgroundColor: 'white', + padding: '8pt', + fontFamily: 'monospace', + overflow: 'auto', + margin: '1.2em', + }, + 'pre.src': { + position: 'relative', + overflow: 'auto', + }, + 'pre.src:before': { + display: 'none', + position: 'absolute', + top: '-8px', + right: '12px', + padding: '3px', + //color: '#555', + backgroundColor: 'white', + }, + 'caption.t-above': { captionSide: 'top' }, + 'caption.t-bottom': { captionSide: 'bottom' }, + 'th.org-right': { textAlign: 'center' }, + 'th.org-left': { textAlign: 'center' }, + 'th.org-center': { textAlign: 'center' }, + 'td.org-right': { textAlign: 'right' }, + 'td.org-left': { textAlign: 'left' }, + 'td.org-center': { textAlign: 'center' }, + '.footpara': { display: 'inline' }, + '.footdef': { marginBottom: '1em' }, + '.figure': { padding: '1em' }, + '.figure p': { textAlign: 'center' }, +} +export default noteStyle diff --git a/components/Sidebar/transition-utils.tsx b/components/Sidebar/transition-utils.tsx new file mode 100644 index 0000000..68634bb --- /dev/null +++ b/components/Sidebar/transition-utils.tsx @@ -0,0 +1,144 @@ +// Taken from https://github.com/chakra-ui/chakra-ui/blob/fc3b97d0978cf2adb9fc79157c6e42b4b68155c5/packages/transition/src/transition-utils.ts +import { isNumber } from '@chakra-ui/utils' +import { Target, TargetAndTransition, Transition } from 'framer-motion' + +type TargetResolver<P = {}> = ( + props: P & { + transition?: TransitionConfig + transitionEnd?: TransitionEndConfig + delay?: number | DelayConfig + }, +) => TargetAndTransition + +type Variant<P = {}> = TargetAndTransition | TargetResolver<P> + +export type Variants<P = {}> = { + enter: Variant<P> + exit: Variant<P> + initial?: Variant<P> +} + +type WithMotionState<P> = Partial<Record<'enter' | 'exit', P>> + +export type TransitionConfig = WithMotionState<Transition> + +export type TransitionEndConfig = WithMotionState<Target> + +export type DelayConfig = WithMotionState<number> + +export const TransitionEasings = { + ease: [0.25, 0.1, 0.25, 1], + easeIn: [0.4, 0, 1, 1], + easeOut: [0, 0, 0.2, 1], + easeInOut: [0.4, 0, 0.2, 1], +} as const + +export const TransitionVariants = { + scale: { + enter: { scale: 1 }, + exit: { scale: 0.95 }, + }, + fade: { + enter: { opacity: 1 }, + exit: { opacity: 0 }, + }, + pushLeft: { + enter: { x: '100%' }, + exit: { x: '-30%' }, + }, + pushRight: { + enter: { x: '-100%' }, + exit: { x: '30%' }, + }, + pushUp: { + enter: { y: '100%' }, + exit: { y: '-30%' }, + }, + pushDown: { + enter: { y: '-100%' }, + exit: { y: '30%' }, + }, + slideLeft: { + position: { left: 0, top: 0, bottom: 0, width: '100%' }, + enter: { x: 0 }, + exit: { x: '-100%' }, + }, + slideRight: { + position: { right: 0, top: 0, bottom: 0, width: '100%' }, + enter: { x: 0 }, + exit: { x: '100%' }, + }, + slideUp: { + position: { top: 0, left: 0, right: 0, maxWidth: '100vw' }, + enter: { y: 0 }, + exit: { y: '-100%' }, + }, + slideDown: { + position: { bottom: 0, left: 0, right: 0, maxWidth: '100vw' }, + enter: { y: 0 }, + exit: { y: '100%' }, + }, +} + +export type SlideDirection = 'top' | 'left' | 'bottom' | 'right' + +export function slideTransition(options?: { direction?: SlideDirection }) { + const side = options?.direction ?? 'right' + switch (side) { + case 'right': + return TransitionVariants.slideRight + case 'left': + return TransitionVariants.slideLeft + case 'bottom': + return TransitionVariants.slideDown + case 'top': + return TransitionVariants.slideUp + default: + return TransitionVariants.slideRight + } +} + +export const TransitionDefaults = { + enter: { + duration: 0.2, + ease: TransitionEasings.easeOut, + }, + exit: { + duration: 0.1, + ease: TransitionEasings.easeIn, + }, +} as const + +export type WithTransitionConfig<P extends object> = Omit<P, 'transition'> & { + /** + * If `true`, the element will unmount when `in={false}` and animation is done + */ + unmountOnExit?: boolean + /** + * Show the component; triggers the enter or exit states + */ + in?: boolean + /** + * Custom `transition` definition for `enter` and `exit` + */ + transition?: TransitionConfig + /** + * Custom `transitionEnd` definition for `enter` and `exit` + */ + transitionEnd?: TransitionEndConfig + /** + * Custom `delay` definition for `enter` and `exit` + */ + delay?: number | DelayConfig +} + +export const withDelay = { + enter: (transition: Transition, delay?: number | DelayConfig) => ({ + ...transition, + delay: isNumber(delay) ? delay : delay?.['enter'], + }), + exit: (transition: Transition, delay?: number | DelayConfig) => ({ + ...transition, + delay: isNumber(delay) ? delay : delay?.['exit'], + }), +} diff --git a/components/TagMenu.tsx b/components/TagMenu.tsx new file mode 100644 index 0000000..432e9a2 --- /dev/null +++ b/components/TagMenu.tsx @@ -0,0 +1,117 @@ +import { MinusIcon, AddIcon, ViewOffIcon, ViewIcon } from '@chakra-ui/icons' +import { + Text, + Box, + Button, + Flex, + Menu, + MenuButton, + MenuItem, + MenuList, + Portal, + useDisclosure, +} from '@chakra-ui/react' +import React from 'react' +import { colorList, initialFilter, TagColors } from './config' +import { Collapse } from './Sidebar/Collapse' +import { ColorMenu } from './Tweaks/ColorMenu' + +export interface TagMenuProps { + setTagColors: any + tagColors: TagColors + setFilter: any + filter: typeof initialFilter + target: string | null +} + +export const TagMenu = (props: TagMenuProps) => { + const { setTagColors, setFilter, filter, tagColors, target } = props + const bl: string[] = filter.tagsBlacklist + const wl: string[] = filter.tagsWhitelist + const blacklist = bl.indexOf(target as string) > -1 + const whitelist = wl.indexOf(target as string) > -1 + const colors = useDisclosure() + return ( + <> + <MenuItem + icon={ + <Box + bgColor={tagColors[target as string]} + borderRadius="sm" + height={3} + width={3} + borderColor={tagColors[target as string] || 'gray.600'} + borderWidth={1} + ></Box> + } + closeOnSelect={false} + onClick={colors.onToggle} + > + <Text>Change color</Text> + </MenuItem> + <Collapse in={colors.isOpen}> + <Flex ml={2} mt={2} flexWrap="wrap"> + {colorList.map((color: string) => ( + <Box key={color}> + <Box + tabIndex={0} + cursor="pointer" + onClick={() => + setTagColors({ + ...tagColors, + [target as string]: color, + }) + } + bgColor={color} + m={1} + borderRadius="sm" + height={3} + width={3} + ></Box> + </Box> + ))} + </Flex> + </Collapse> + {!whitelist && ( + <MenuItem + onClick={() => { + if (!blacklist) { + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsBlacklist: [...filter.tagsBlacklist, target], + })) + return + } + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsBlacklist: filter.tagsBlacklist.filter((t) => t !== target), + })) + }} + icon={blacklist ? <MinusIcon /> : <ViewOffIcon />} + > + {blacklist ? 'Remove from blacklist' : 'Add to blacklist'} + </MenuItem> + )} + {!blacklist && ( + <MenuItem + onClick={() => { + if (!whitelist) { + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsWhitelist: [...filter.tagsWhitelist, target], + })) + return + } + setFilter((filter: typeof initialFilter) => ({ + ...filter, + tagsWhitelist: filter.tagsWhitelist.filter((t) => t !== target), + })) + }} + icon={whitelist ? <MinusIcon /> : <ViewIcon />} + > + {whitelist ? 'Remove from whitelist' : 'Add to whitelist'} + </MenuItem> + )} + </> + ) +} diff --git a/components/Tweaks/BehaviorPanel.tsx b/components/Tweaks/BehaviorPanel.tsx index 8edb986..5d61730 100644 --- a/components/Tweaks/BehaviorPanel.tsx +++ b/components/Tweaks/BehaviorPanel.tsx @@ -10,6 +10,8 @@ import { StackDivider, VStack, Text, + Box, + Switch, } from '@chakra-ui/react' import React from 'react' import { initialBehavior, initialMouse } from '../config' @@ -34,6 +36,26 @@ export const BehaviorPanel = (props: BehaviorPanelProps) => { color="gray.800" > <Flex alignItems="center" justifyContent="space-between"> + <Text>Preview node</Text> + <Menu isLazy placement="right"> + <MenuButton as={Button} rightIcon={<ChevronDownIcon />} colorScheme="" color="black"> + <Text> + {mouse.preview ? mouse.preview[0]!.toUpperCase() + mouse.preview!.slice(1) : 'Never'} + </Text> + </MenuButton> + <Portal> + {' '} + <MenuList bgColor="gray.200" zIndex="popover"> + <MenuItem onClick={() => setMouse({ ...mouse, preview: '' })}>Never</MenuItem> + <MenuItem onClick={() => setMouse({ ...mouse, preview: 'click' })}>Click</MenuItem> + <MenuItem onClick={() => setMouse({ ...mouse, preview: 'double' })}> + Double Click + </MenuItem> + </MenuList> + </Portal> + </Menu> + </Flex> + <Flex alignItems="center" justifyContent="space-between"> <Flex> <Text>Expand Node</Text> <InfoTooltip infoText="View only the node and its direct neighbors" /> diff --git a/components/Tweaks/ColorMenu.tsx b/components/Tweaks/ColorMenu.tsx index 3d90e36..b87f76b 100644 --- a/components/Tweaks/ColorMenu.tsx +++ b/components/Tweaks/ColorMenu.tsx @@ -9,6 +9,9 @@ import { MenuItem, MenuList, Portal, + PopoverTrigger, + PopoverContent, + Popover, } from '@chakra-ui/react' import React, { useCallback } from 'react' import { initialVisuals } from '../config' @@ -19,10 +22,11 @@ export interface ColorMenuProps { value: string visValue: string setVisuals?: any + noEmpty?: boolean } export const ColorMenu = (props: ColorMenuProps) => { - const { label, colorList, value, visValue, setVisuals } = props + const { label, colorList, value, visValue, setVisuals, noEmpty } = props const clickCallback = useCallback( (color) => @@ -37,35 +41,48 @@ export const ColorMenu = (props: ColorMenuProps) => { return ( <Flex alignItems="center" justifyContent="space-between"> <Text>{label}</Text> - <Menu isLazy placement="right"> - <MenuButton as={Button} colorScheme="" color="black" rightIcon={<ChevronDownIcon />}> - {<Box bgColor={visValue} borderRadius="sm" height={6} width={6}></Box>} - </MenuButton> + <Popover isLazy placement="right"> + <PopoverTrigger> + <Button colorScheme="" color="black" rightIcon={<ChevronDownIcon />}> + {<Box bgColor={visValue} borderRadius="sm" height={6} width={6}></Box>} + </Button> + </PopoverTrigger> <Portal> - {' '} - <MenuList minW={10} zIndex="popover" bgColor="gray.200"> - <MenuItem - onClick={() => clickCallback('')} - justifyContent="space-between" - alignItems="center" - display="flex" - > - <Box height={6} width={6}></Box> - </MenuItem> - {colorList.map((color: string) => ( - <MenuItem - key={color} - onClick={() => clickCallback(color)} - justifyContent="space-between" - alignItems="center" - display="flex" - > - <Box bgColor={color} borderRadius="sm" height={6} width={6}></Box> - </MenuItem> - ))} - </MenuList> + <PopoverContent zIndex="tooltip" maxW={36} position="relative"> + <Flex flexWrap="wrap" bgColor="gray.200"> + {!noEmpty && ( + <Box + onClick={() => clickCallback('')} + justifyContent="space-between" + alignItems="center" + display="flex" + m={1} + > + <Box + height={6} + width={6} + borderColor="gray.600" + borderRadius="xl" + borderWidth={1} + ></Box> + </Box> + )} + {colorList.map((color: string) => ( + <Box + m={1} + key={color} + onClick={() => clickCallback(color)} + justifyContent="space-between" + alignItems="center" + display="flex" + > + <Box bgColor={color} borderRadius="xl" height={6} width={6}></Box> + </Box> + ))} + </Flex> + </PopoverContent> </Portal> - </Menu> + </Popover> </Flex> ) } diff --git a/components/Tweaks/SliderWithInfo.tsx b/components/Tweaks/SliderWithInfo.tsx index f70faae..9d6903a 100644 --- a/components/Tweaks/SliderWithInfo.tsx +++ b/components/Tweaks/SliderWithInfo.tsx @@ -30,7 +30,7 @@ export const SliderWithInfo = ({ const { onChange, label, infoText } = rest const { highlightColor } = useContext(ThemeContext) return ( - <Box key={label}> + <Box key={label} pt={1} pb={2}> <Box display="flex" alignItems="flex-end"> <Text>{label}</Text> {infoText && <InfoTooltip infoText={infoText} />} diff --git a/components/Tweaks/index.tsx b/components/Tweaks/index.tsx index c60e670..afd0ea7 100644 --- a/components/Tweaks/index.tsx +++ b/components/Tweaks/index.tsx @@ -68,6 +68,7 @@ export const Tweaks = (props: TweakProps) => { tagColors, setTagColors, } = props + const [showTweaks, setShowTweaks] = usePersistantState('showTweaks', false) const { highlightColor, setHighlightColor } = useContext(ThemeContext) @@ -75,12 +76,12 @@ export const Tweaks = (props: TweakProps) => { <Box position="absolute" zIndex="overlay" - marginTop={10} - marginLeft={10} + marginTop={1} + marginLeft={0} display={showTweaks ? 'none' : 'block'} > <IconButton - variant="ghost" + variant="subtle" aria-label="Settings" icon={<SettingsIcon />} onClick={() => setShowTweaks(true)} @@ -88,17 +89,17 @@ export const Tweaks = (props: TweakProps) => { </Box> ) : ( <Box + position="absolute" bg="alt.100" w="xs" - marginTop={10} - marginLeft={10} - borderRadius="xl" + marginTop={2} + marginLeft={2} + borderRadius="lg" paddingBottom={5} - zIndex={300} - position="relative" + zIndex={10} boxShadow="xl" - maxH={0.92 * globalThis.innerHeight} - marginBottom={10} + maxH={'95vh'} + fontSize="sm" > <Box display="flex" @@ -108,7 +109,7 @@ export const Tweaks = (props: TweakProps) => { paddingTop={1} > <Tooltip label={'Switch to ' + threeDim ? '2D' : '3D' + ' view'}> - <Button onClick={() => setThreeDim(!threeDim)} variant="ghost" zIndex="overlay"> + <Button onClick={() => setThreeDim(!threeDim)} variant="subtle" zIndex="overlay"> {threeDim ? '3D' : '2D'} </Button> </Tooltip> @@ -124,7 +125,7 @@ export const Tweaks = (props: TweakProps) => { setPhysics(initialPhysics) setBehavior(initialBehavior) }} - variant="none" + variant="subtle" size="sm" /> </Tooltip> @@ -132,7 +133,7 @@ export const Tweaks = (props: TweakProps) => { size="sm" icon={<CloseIcon />} aria-label="Close Tweak Panel" - variant="ghost" + variant="subtle" onClick={() => setShowTweaks(false)} /> </Box> diff --git a/components/config.ts b/components/config.ts index 532f960..ec1c691 100644 --- a/components/config.ts +++ b/components/config.ts @@ -59,13 +59,13 @@ export const initialVisuals = { nodeOpacity: 1, nodeResolution: 12, labels: 2, - labelScale: 1.5, - labelFontSize: 13, + labelScale: 1, + labelFontSize: 10, labelLength: 40, labelWordWrap: 25, labelLineSpace: 1, highlight: true, - highlightNodeSize: 2, + highlightNodeSize: 1.2, highlightLinkSize: 2, highlightFade: 0.8, highlightAnim: true, @@ -88,7 +88,7 @@ export const initialVisuals = { linkHighlight: 'purple.500', backgroundColor: 'white', emacsNodeColor: 'gray.800', - labelTextColor: 'gray.900', + labelTextColor: 'black', labelBackgroundColor: '', labelBackgroundOpacity: 0.7, citeDashes: true, @@ -120,9 +120,11 @@ export const initialBehavior = { export const initialMouse = { highlight: 'hover', - local: 'click', - follow: 'double', + local: 'double', + follow: 'never', context: 'right', + preview: 'click', + backgroundExitsLocal: false, } export const colorList = [ diff --git a/components/contextmenu.tsx b/components/contextmenu.tsx index 755bd9d..77deb48 100644 --- a/components/contextmenu.tsx +++ b/components/contextmenu.tsx @@ -43,82 +43,105 @@ import { } from '@chakra-ui/icons' import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api' +import { deleteNodeInEmacs, openNodeInEmacs, createNodeInEmacs } from '../util/webSocketFunctions' +import { BiNetworkChart } from 'react-icons/bi' +import { TagMenu } from './TagMenu' +import { initialFilter, TagColors } from './config' export default interface ContextMenuProps { background: Boolean - node?: OrgRoamNode + target: OrgRoamNode | string | null nodeType?: string - coordinates: number[] + coordinates: { [direction: string]: number | undefined } handleLocal: (node: OrgRoamNode, add: string) => void - openNodeInEmacs: (node: OrgRoamNode) => void menuClose: () => void scope: { nodeIds: string[] } - deleteNodeInEmacs: (node: OrgRoamNode) => void - createNodeInEmacs: (node: OrgRoamNode) => void + webSocket: any + setPreviewNode: any + setTagColors: any + tagColors: TagColors + setFilter: any + filter: typeof initialFilter } export const ContextMenu = (props: ContextMenuProps) => { const { background, - node, + target, nodeType, coordinates, handleLocal, menuClose, scope, - openNodeInEmacs, - deleteNodeInEmacs, - createNodeInEmacs, + webSocket, + setPreviewNode, + setTagColors, + tagColors, + setFilter, + filter, } = props const { isOpen, onOpen, onClose } = useDisclosure() const copyRef = useRef<any>() return ( <> - <Box - position="absolute" - zIndex="overlay" - left={coordinates[0] + 10} - top={coordinates[1] - 10} - padding={5} - > - <Menu closeOnBlur={false} defaultIsOpen onClose={() => menuClose()}> - <MenuList zIndex="overlay" bgColor="alt.100" borderColor="gray.500" maxWidth="xs"> - {node && ( - <> - <Heading size="sm" isTruncated px={3} py={1}> - {node.title} - </Heading> - <MenuDivider borderColor="gray.500" /> - </> - )} - {scope.nodeIds.length !== 0 && ( - <> - <MenuItem onClick={() => handleLocal(node!, 'add')} icon={<PlusSquareIcon />}> - Expand local graph at node + <Menu defaultIsOpen closeOnBlur={false} onClose={() => menuClose()}> + <MenuList + zIndex="overlay" + bgColor="white" + color="black" + //borderColor="gray.500" + position="absolute" + left={coordinates.left} + top={coordinates.top} + right={coordinates.right} + bottom={coordinates.bottom} + fontSize="xs" + boxShadow="xl" + > + {typeof target !== 'string' ? ( + <> + {target && ( + <> + <Heading size="xs" isTruncated px={3} py={1}> + {target.title} + </Heading> + <MenuDivider borderColor="gray.500" /> + </> + )} + {scope.nodeIds.length !== 0 && ( + <> + <MenuItem onClick={() => handleLocal(target!, 'add')} icon={<PlusSquareIcon />}> + Expand local graph at node + </MenuItem> + <MenuItem + onClick={() => handleLocal(target!, 'replace')} + icon={<BiNetworkChart />} + > + Open local graph for this node + </MenuItem> + </> + )} + {!target?.properties?.FILELESS ? ( + <MenuItem + icon={<EditIcon />} + onClick={() => openNodeInEmacs(target as OrgRoamNode, webSocket)} + > + Open in Emacs </MenuItem> - <MenuItem onClick={() => handleLocal(node!, 'replace')} icon={<ViewIcon />}> - Open local graph for this node + ) : ( + <MenuItem icon={<AddIcon />} onClick={() => createNodeInEmacs(target, webSocket)}> + Create node </MenuItem> - </> - )} - {!node?.properties.FILELESS ? ( - <MenuItem icon={<EditIcon />} onClick={() => openNodeInEmacs(node as OrgRoamNode)}> - Open in Emacs - </MenuItem> - ) : ( - <MenuItem icon={<AddIcon />} onClick={() => createNodeInEmacs(node)}> - Create node - </MenuItem> - )} - {node?.properties.ROAM_REFS && ( - <MenuItem icon={<ExternalLinkIcon />}>Open in Zotero</MenuItem> - )} - {scope.nodeIds.length === 0 && ( - <MenuItem icon={<ViewIcon />} onClick={() => handleLocal(node!, 'replace')}> - Open local graph - </MenuItem> - )} - {/* Doesn't work at the moment + )} + {target?.properties?.ROAM_REFS && ( + <MenuItem icon={<ExternalLinkIcon />}>Open in Zotero</MenuItem> + )} + {scope.nodeIds.length === 0 && ( + <MenuItem icon={<BiNetworkChart />} onClick={() => handleLocal(target!, 'replace')}> + Open local graph + </MenuItem> + )} + {/* Doesn't work at the moment <MenuItem closeOnSelect={false} closeOnBlur={false}> <Box _hover={{ bg: 'gray.200' }} width="100%"> <Popover @@ -147,64 +170,78 @@ export const ContextMenu = (props: ContextMenuProps) => { </Popover> </Box> </MenuItem> */} - {node?.level === 0 && ( + <MenuItem - closeOnSelect={false} - icon={<DeleteIcon color="red.500" />} - color="red.500" - onClick={onOpen} + icon={<ViewIcon />} + onClick={() => { + setPreviewNode(target) + }} > - Permenantly delete note + Preview </MenuItem> - )} - </MenuList> - </Menu> - </Box> - <Modal isCentered isOpen={isOpen} onClose={onClose}> - <ModalOverlay /> - <ModalContent zIndex="popover"> - <ModalHeader>Delete node?</ModalHeader> - <ModalCloseButton /> - <ModalBody> - <VStack spacing={4} display="flex" alignItems="flex-start"> - <Text>This will permanently delete your note:</Text> - <Text fontWeight="bold">{node?.title}</Text> - {node?.level !== 0 && ( - <Text> - This will only delete the from this heading until but not including the next node. - Your parent file and all other nodes will not be deleted. - </Text> + {target?.level === 0 && ( + <MenuItem + closeOnSelect={false} + icon={<DeleteIcon color="red.500" />} + color="red.500" + onClick={onOpen} + > + Permenantly delete note + </MenuItem> )} - <Text>Are you sure you want to do continue?</Text> - </VStack> - </ModalBody> - <ModalFooter> - <Button - mr={3} - onClick={() => { - console.log('closing') - onClose() - menuClose() - }} - > - Cancel - </Button> - <Button - variant="link" - colorScheme="red" - ml={3} - onClick={() => { - console.log('aaaaa') - deleteNodeInEmacs(node!) - onClose() - menuClose() - }} - > - Delete node - </Button> - </ModalFooter> - </ModalContent> - </Modal> + </> + ) : ( + <TagMenu {...{ target, tagColors, filter, setTagColors, setFilter }} /> + )} + </MenuList> + </Menu> + {typeof target !== 'string' && ( + <Modal isCentered isOpen={isOpen} onClose={onClose}> + <ModalOverlay /> + <ModalContent zIndex="popover"> + <ModalHeader>Delete node?</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <VStack spacing={4} display="flex" alignItems="flex-start"> + <Text>This will permanently delete your note:</Text> + <Text fontWeight="bold">{target?.title}</Text> + {target?.level !== 0 && ( + <Text> + This will only delete the from this heading until but not including the next + node. Your parent file and all other nodes will not be deleted. + </Text> + )} + <Text>Are you sure you want to do continue?</Text> + </VStack> + </ModalBody> + <ModalFooter> + <Button + mr={3} + onClick={() => { + console.log('closing') + onClose() + menuClose() + }} + > + Cancel + </Button> + <Button + variant="link" + colorScheme="red" + ml={3} + onClick={() => { + console.log('aaaaa') + deleteNodeInEmacs(target!, webSocket) + onClose() + menuClose() + }} + > + Delete node + </Button> + </ModalFooter> + </ModalContent> + </Modal> + )} </> ) } |