diff options
-rw-r--r-- | components/Sidebar/Backlinks.tsx | 55 | ||||
-rw-r--r-- | components/Sidebar/Link.tsx | 77 | ||||
-rw-r--r-- | components/Sidebar/index.tsx | 195 | ||||
-rw-r--r-- | components/contextmenu.tsx | 19 | ||||
-rw-r--r-- | org-roam-ui.el | 3 | ||||
-rw-r--r-- | package-lock.json | 15 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | pages/index.tsx | 36 | ||||
-rw-r--r-- | util/processOrg.tsx | 46 | ||||
-rw-r--r-- | util/uniorg.tsx | 44 |
10 files changed, 385 insertions, 106 deletions
diff --git a/components/Sidebar/Backlinks.tsx b/components/Sidebar/Backlinks.tsx new file mode 100644 index 0000000..f899d3e --- /dev/null +++ b/components/Sidebar/Backlinks.tsx @@ -0,0 +1,55 @@ +import { LinksByNodeId, 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 + getText: any +} + +import { PreviewLink } from './Link' + +export const Backlinks = (props: BacklinksProps) => { + const { previewNode, setPreviewNode, nodeById, linksByNodeId, getText } = props + const links = linksByNodeId[previewNode?.id] ?? [] + return ( + <Box> + <Heading pt={4}>{'Backlinks (' + links.length + ')'}</Heading> + <VStack + pt={2} + spacing={3} + alignItems="start" + divider={<StackDivider borderColor="gray.500" />} + align="stretch" + color="gray.800" + > + {previewNode?.id && + links.map((link: LinkObject, i: number) => { + const [source, target] = normalizeLinkEnds(link) + if (source === previewNode?.id) { + return + } + return ( + <Box overflow="hidden" p={3} bg="gray.300" width="100%" key={source}> + <PreviewLink + href={'id:' + source} + children={nodeById[source]?.title} + nodeById={nodeById} + setPreviewNode={setPreviewNode} + getText={getText} + /> + </Box> + ) + })} + </VStack> + </Box> + ) +} diff --git a/components/Sidebar/Link.tsx b/components/Sidebar/Link.tsx index 789873a..ff812ef 100644 --- a/components/Sidebar/Link.tsx +++ b/components/Sidebar/Link.tsx @@ -1,11 +1,78 @@ -import { Text } from '@chakra-ui/react' +import { + Box, + Button, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverHeader, + PopoverTrigger, + Portal, + Text, +} from '@chakra-ui/react' +import React, { useState } from 'react' +import UniOrg from '../../util/uniorg' + +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' export interface LinkProps { - id: string + href: string + children: any + testProp: string + getText: any + previewNode?: any + setPreviewNode: any } -export const Link = (props: LinkProps) => { - const { id } = props +export const PreviewLink = (props: any) => { + const { href, children, nodeById, getText, previewNode, setPreviewNode } = props + const [previewText, setPreviewText] = useState('') + const [whatever, type, uri] = [...href.matchAll(/(.*?)\:(.*)/g)][0] + + const processor = unified().use(uniorgParse).use(uniorg2rehype).use(katex).use(rehype2react, { + createElement: React.createElement, + // eslint-disable-next-line react/display-name + }) + + type === 'id' && getText(uri, setPreviewText) - return <Text>{id}</Text> + return ( + <> + <Popover trigger="hover" isLazy position="relative" zIndex="tooltip"> + <PopoverTrigger> + <Button size="sm" onClick={() => setPreviewNode(nodeById[uri])} variant="link"> + {children} + </Button> + </PopoverTrigger> + <Portal zIndex={100000} position="relative"> + <PopoverContent boxShadow="xl" position="relative" zIndex="tooltip"> + <PopoverHeader pl={5} fontSize="sm" zIndex="tooltip" fontWeight="semibold"> + {children} + </PopoverHeader> + <PopoverArrow zIndex={10000} /> + <PopoverCloseButton zIndex={10000} /> + <PopoverBody + pb={5} + fontSize="xs" + px={5} + position="relative" + zIndex="tooltip" + maxHeight={300} + overflow="scroll" + > + {uri && <Box>{processor.processSync(previewText).result}</Box>} + </PopoverBody> + </PopoverContent> + </Portal> + </Popover> + </> + ) } diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index 4f314da..e56a25d 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -1,6 +1,7 @@ import React, { useContext, useEffect, useState } from 'react' import { UniOrg } from '../../util/uniorg' +import { Backlinks } from './Backlinks' import { getOrgText } from '../../util/webSocketFunctions' import { @@ -22,90 +23,98 @@ import { IconButton, } from '@chakra-ui/react' import { Scrollbars } from 'react-custom-scrollbars-2' -import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons' +import { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon } from '@chakra-ui/icons' +import { + BiFont, + BiAlignJustify, + BiAlignLeft, + BiAlignMiddle, + BiAlignRight, + BiRightIndent, +} from 'react-icons/bi' import { GraphData, NodeObject, LinkObject } from 'force-graph' import { OrgRoamNode } from '../../api' import { ThemeContext } from '../../util/themecontext' +import { LinksByNodeId, NodeById } from '../../pages/index' export interface SidebarProps { isOpen: boolean onClose: any - //nodeById: any + onOpen: any + nodeById: NodeById previewNode: NodeObject + setPreviewNode: any + linksByNodeId: LinksByNodeId } const Sidebar = (props: SidebarProps) => { - const { isOpen, onClose, previewNode } = props + const { isOpen, onOpen, onClose, previewNode, setPreviewNode, nodeById, linksByNodeId } = props const { highlightColor } = useContext(ThemeContext) const [previewRoamNode, setPreviewRoamNode] = useState<OrgRoamNode>() - const [previewText, setPreviewText] = useState('') - const getText = (id: string) => { + const getText = (id: string, setText: any) => { fetch(`http://localhost:35901/note/${id}`) .then((res) => { return res.text() }) - .then((res) => { - console.log(res) - setPreviewText(res) - }) + .then((res) => setText(res)) .catch((e) => { - setPreviewText( + 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.', + console.log(e) ) }) } useEffect(() => { - if (!previewNode) { + if (!previewNode.id) { + onClose() return } - + onOpen() setPreviewRoamNode(previewNode as OrgRoamNode) - previewNode?.id && getText(previewNode?.id as string) - }, [previewNode]) + }, [previewNode.id]) + const [justification, setJustification] = useState(0) + 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 ( - <Slide direction="right" in={isOpen} style={{ zIndex: 200, width: 500 }} unmountOnExit> + <Slide direction="right" in={isOpen} style={{ maxWidth: '50%' }} unmountOnExit> <Flex flexDirection="row" height="100%"> - <IconButton - icon={<ChevronRightIcon height={30} />} - colorScheme="white" - aria-label="Close file-viewer" - height={100} - variant="ghost" - marginRight={-2} - bg="alt.100" - onClick={onClose} - marginTop={20} - /> - <Box - color="gray.800" - bg="alt.100" - boxShadow="xl" - w={500} - height="98%" - position="relative" - zIndex="overlay" - marginTop={10} - paddingBottom={15} - borderRadius="xl" - right={-2} - > + <Box pl={2} color="gray.800" bg="alt.100" w="100%" paddingBottom={15}> <Flex justifyContent="space-between" - padding={4} - paddingTop={10} - paddingLeft={10} - width="80%" + paddingTop={4} + pl={0} + pb={5} + pr={2} alignItems="center" color="black" > - <Heading size="md">{previewRoamNode?.title}</Heading> + <Flex> + <IconButton + variant="link" + size="md" + icon={<ChevronLeftIcon />} + aria-label="Previous node" + /> + <Heading size="md" fontWeight={400}> + {previewRoamNode?.title} + </Heading> + </Flex> + + <IconButton + // eslint-disable-next-line react/jsx-no-undef + icon={<HamburgerIcon />} + aria-label="Close file-viewer" + variant="link" + onClick={onClose} + /> </Flex> <Scrollbars //autoHeight @@ -113,40 +122,43 @@ const Sidebar = (props: SidebarProps) => { autoHide renderThumbVertical={({ style, ...props }) => ( <Box - {...props} style={{ ...style, borderRadius: 10, + backgroundColor: highlightColor, }} - bg={highlightColor} + color="black" + {...props} /> )} > - <VStack alignItems="left" bg="alt.100" paddingLeft={10} paddingRight={10}> + <VStack height="100%" alignItems="left" bg="alt.100" paddingLeft={10}> <Box + pr={8} + overflow="scroll" + height="85%" className="org" sx={{ - h1: { display: 'none' }, + '.katex': { overflowX: 'scroll' }, + h1: { fontSize: '20', fontWeight: 'bold', marginBottom: 3 }, h2: { - fontSize: '20', - fontWeight: 'bold !important', - marginBottom: '1em', + fontSize: '18', + marginBottom: 2, color: 'black', }, h3: { - fontSize: '18', + fontSize: '16', fontWeight: '600 !important', marginBottom: '.5em', }, h4: { - fontSize: '16', + fontSize: '14', fontWeight: '500 !important', marginBottom: '.25em', fontStyle: 'italic', }, a: { color: highlightColor, - pointerEvents: 'none', }, ol: { paddingLeft: '5', @@ -157,8 +169,11 @@ const Sidebar = (props: SidebarProps) => { p: { paddingBottom: '.5em', }, - '.katex-html': { visibility: 'hidden', width: '0px', position: 'absolute' }, - '#content': { textAlign: 'justify' }, + div: { + fontSize: 'small', + hyphens: 'auto !important', + textAlign: justificationList[justification], + }, '.title': { textAlign: 'center', marginBottom: '.2em', @@ -261,9 +276,75 @@ const Sidebar = (props: SidebarProps) => { '.footdef': { marginBottom: '1em' }, '.figure': { padding: '1em' }, '.figure p': { textAlign: 'center' }, + // org-like indents + 'h1, h1 ~ *,h2 ~ h1,h2 ~ h1 ~ *,h3 ~ h1,h3 ~ h1 ~ *': { + marginLeft: 0 * indent, + }, + 'h2 ~ *, h1 ~ h2,h1 ~ h2 ~ *:not(h1):not(h3)': { + marginLeft: 2 * indent, + }, + 'h3 ~ *,h1 ~ h3,h1 ~ h3 ~ *:not(h1):not(h2)': { + marginLeft: 4 * indent, + }, }} > - <UniOrg orgText={previewText} /> + {previewNode?.id && ( + <Box> + <Flex pt={1} alignItems="center" justifyContent="flex-end"> + <IconButton + variant="ghost" + aria-label="Justify content" + icon={ + [ + <BiAlignJustify key="justify" />, + <BiAlignLeft key="left" />, + <BiAlignRight key="right" />, + <BiAlignMiddle key="center" />, + ][justification] + } + onClick={() => setJustification((curr) => (curr + 1) % 4)} + /> + <IconButton + variant="ghost" + aria-label="Indent Text" + icon={<BiRightIndent />} + onClick={() => { + console.log(indent) + setIndent((curr: number) => (indent ? 0 : 1)) + }} + /> + <IconButton + variant="ghost" + aria-label="Change font" + icon={<BiFont />} + onClick={() => { + setFont((curr: string) => + curr === 'sans serif' ? 'serif' : 'sans serif', + ) + }} + /> + </Flex> + <Flex height="100%" flexDirection="column" justifyContent="space-between"> + <UniOrg + {...{ + getText, + setPreviewNode, + previewNode, + nodeById, + }} + /> + <Backlinks + {...{ + setPreviewNode, + previewNode, + nodeById, + linksByNodeId, + getText, + }} + /> + </Flex> + </Box> + )} </Box> </VStack> </Scrollbars> diff --git a/components/contextmenu.tsx b/components/contextmenu.tsx index 09d9cff..73758a5 100644 --- a/components/contextmenu.tsx +++ b/components/contextmenu.tsx @@ -44,6 +44,7 @@ import { import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api' import { deleteNodeInEmacs, openNodeInEmacs, createNodeInEmacs } from '../util/webSocketFunctions' +import { BiNetworkChart } from 'react-icons/bi' export default interface ContextMenuProps { background: Boolean @@ -116,7 +117,7 @@ export const ContextMenu = (props: ContextMenuProps) => { <MenuItem icon={<ExternalLinkIcon />}>Open in Zotero</MenuItem> )} {scope.nodeIds.length === 0 && ( - <MenuItem icon={<ViewIcon />} onClick={() => handleLocal(node!, 'replace')}> + <MenuItem icon={<BiNetworkChart />} onClick={() => handleLocal(node!, 'replace')}> Open local graph </MenuItem> )} @@ -149,6 +150,15 @@ export const ContextMenu = (props: ContextMenuProps) => { </Popover> </Box> </MenuItem> */} + + <MenuItem + icon={<ViewIcon />} + onClick={() => { + setPreviewNode(node) + }} + > + Preview + </MenuItem> {node?.level === 0 && ( <MenuItem closeOnSelect={false} @@ -159,13 +169,6 @@ export const ContextMenu = (props: ContextMenuProps) => { Permenantly delete note </MenuItem> )} - <MenuItem - onClick={() => { - setPreviewNode(node) - }} - > - Preview - </MenuItem> </MenuList> </Menu> </Box> diff --git a/org-roam-ui.el b/org-roam-ui.el index 6fbb279..0fbcf5e 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -240,8 +240,7 @@ This serves the web-build and API over HTTP." (let* ((node (org-roam-populate (org-roam-node-create :id id))) (file (org-roam-node-file node))) (insert-file-contents-literally file) -(httpd-send-header t "text/html" 200 - :Access-Control-Allow-Origin "*"))) +(httpd-send-header t "text/plain" 200 :Access-Control-Allow-Origin "*"))) (defun org-roam-ui--on-save () diff --git a/package-lock.json b/package-lock.json index 04a2164..04de502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-custom-scrollbars-2": "^4.4.0", "react-dom": "17.0.2", "react-force-graph": "^1.41.7", + "react-icons": "^4.3.1", "react-spring": "^9.2.4", "reconnecting-websocket": "^4.4.0", "rehype": "^11.0.0", @@ -9584,6 +9585,14 @@ "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0" } }, + "node_modules/react-icons": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -19592,6 +19601,12 @@ "prop-types": "^15.5.8" } }, + "react-icons": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 6db84ee..0bbb24c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-custom-scrollbars-2": "^4.4.0", "react-dom": "17.0.2", "react-force-graph": "^1.41.7", + "react-icons": "^4.3.1", "react-spring": "^9.2.4", "reconnecting-websocket": "^4.4.0", "rehype": "^11.0.0", diff --git a/pages/index.tsx b/pages/index.tsx index 6ebdc6c..48394c0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -43,7 +43,7 @@ import wrap from 'word-wrap' import ReconnectingWebSocket from 'reconnecting-websocket' import { deleteNodeInEmacs, openNodeInEmacs, createNodeInEmacs } from '../util/webSocketFunctions' -import { ChevronLeftIcon } from '@chakra-ui/icons' +import { ChevronLeftIcon, HamburgerIcon } from '@chakra-ui/icons' // react-force-graph fails on import when server-rendered // https://github.com/vasturiano/react-force-graph/issues/155 const ForceGraph2D = ( @@ -419,13 +419,6 @@ export function GraphPage() { height="100%" width="100%" > - <Sidebar - {...{ - isOpen, - onClose, - previewNode, - }} - /> <Tweaks {...{ physics, @@ -448,20 +441,18 @@ export function GraphPage() { <Flex height="100%" flexDirection="column" marginLeft="auto"> {!isOpen && ( <IconButton - icon={<ChevronLeftIcon />} - height={100} + icon={<HamburgerIcon />} aria-label="Open org-viewer" - position="relative" - zIndex="overlay" - colorScheme="purple" + zIndex={2} onClick={onOpen} variant="ghost" marginTop={10} + mr={8} /> )} </Flex> </Box> - <Box position="absolute" alignItems="top" overflow="hidden"> + <Flex position="absolute" alignItems="top" overflow="hidden"> <Graph ref={graphRef} nodeById={nodeByIdRef.current!} @@ -482,7 +473,18 @@ export function GraphPage() { setPreviewNode, }} /> - </Box> + <Sidebar + {...{ + isOpen, + onOpen, + onClose, + previewNode, + setPreviewNode, + }} + nodeById={nodeByIdRef.current!} + linksByNodeId={linksByNodeIdRef.current!} + /> + </Flex> </Box> ) } @@ -996,7 +998,7 @@ export const Graph = forwardRef(function (props: GraphProps, graphRef: any) { const [zoom, setZoom] = useState(1) const graphCommonProps: ComponentPropsWithoutRef<typeof TForceGraph2D> = { graphData: scope.nodeIds.length ? scopedGraphData : filteredGraphData, - width: windowWidth, + //width: windowWidth, height: windowHeight, backgroundColor: theme.colors.gray[visuals.backgroundColor], warmupTicks: scope.nodeIds.length === 1 ? 100 : scope.nodeIds.length > 1 ? 20 : 0, @@ -1262,7 +1264,7 @@ function numberWithinRange(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max) } -function normalizeLinkEnds(link: OrgRoamLink | LinkObject): [string, string] { +export function normalizeLinkEnds(link: OrgRoamLink | LinkObject): [string, string] { // we need to cover both because force-graph modifies the original data // but if we supply the original data on each render, the graph will re-render sporadically const sourceId = diff --git a/util/processOrg.tsx b/util/processOrg.tsx new file mode 100644 index 0000000..df6007e --- /dev/null +++ b/util/processOrg.tsx @@ -0,0 +1,46 @@ +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 { PreviewLink } from '../components/Sidebar/Link' +import { NodeById } from '../pages' +import React from 'react' + +export interface ProcessedOrgProps { + nodeById: NodeById + previewNode: any + setPreviewNode: any + getText: any + previewText: any +} + +export const ProcessedOrg = (props: ProcessedOrgProps) => { + const { nodeById, previewNode, setPreviewNode, getText, previewText } = props + console.log(previewText) + const processor = unified() + .use(uniorgParse) + .use(uniorg2rehype) + .use(katex) + .use(rehype2react, { + createElement: React.createElement, + // eslint-disable-next-line react/display-name + components: { + a: ({ props, children, href }) => ( + <PreviewLink + getText={getText} + nodeById={nodeById} + previewNode={previewNode} + setPreviewNode={setPreviewNode} + {...{ children, href }} + /> + ), + }, + }) + + return <div>{processor.processSync(previewText).result}</div> +} diff --git a/util/uniorg.tsx b/util/uniorg.tsx index 3304a32..e35a021 100644 --- a/util/uniorg.tsx +++ b/util/uniorg.tsx @@ -1,24 +1,34 @@ -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 rehype2react from 'rehype-react' -import React from 'react' +import React, { useEffect, useState } from 'react' +import { NodeById } from '../pages/index' +import { ProcessedOrg } from './processOrg' export interface UniOrgProps { - orgText: string + nodeById: NodeById + previewNode: any + setPreviewNode: any + getText: any } export const UniOrg = (props: UniOrgProps) => { - const { orgText } = props - const processor = unified() - .use(uniorgParse) - .use(uniorg2rehype) - .use(katex) - .use(rehype2react, { createElement: React.createElement }) + const { nodeById, previewNode, setPreviewNode, getText } = props - console.log(processor.processSync(orgText)) - return <div> {processor.processSync(orgText).result}</div> + const [previewText, setPreviewText] = useState('') + + useEffect(() => { + if (previewNode?.id) { + getText(previewNode?.id, setPreviewText) + } + }, [previewNode?.id]) + + return ( + <ProcessedOrg + {...{ + getText, + nodeById, + previewNode, + setPreviewNode, + previewText, + }} + /> + ) } |