Merge pull request #162 from life-itself/151-hover-over-previews

[site/mdx]: display short definitions of key words on hover
This commit is contained in:
Khalil Ali 2022-05-23 19:53:41 +03:00 committed by GitHub
commit 7def64b5c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 6437 additions and 115 deletions

25
site/components/Anchor.js Normal file
View File

@ -0,0 +1,25 @@
import { Tooltip } from './Tooltip';
import siteConfig from '../config/siteConfig.js'
/**
* Component for adding previews on hovering over anchor tags with relative paths
*/
export const Anchor = (props) => {
/* Check if the path is relative */
const pathIsRelative = (path) => {
return path &&
path.indexOf("http:") !== 0 &&
path.indexOf("https:") !== 0 &&
path.indexOf("#") !== 0
}
if (pathIsRelative(props.href)) {
return (
<Tooltip {...props} render={ tooltipTriggerProps => (
<a {...tooltipTriggerProps} />
)}
/>
)
}
return <a {...props} />;
};

View File

@ -1,12 +1,14 @@
import Head from 'next/head'
import ReactPlayer from 'react-player/lazy'
import { Paragraph } from './Link'
import { NextSeo } from 'next-seo'
import siteConfig from "../config/siteConfig"
import { Paragraph } from './Paragraph'
import { Anchor } from './Anchor'
const components = {
Head,
p: Paragraph
p: Paragraph,
a: Anchor
}
export default function MdxPage({ children, editUrl }) {
@ -128,7 +130,7 @@ export default function MdxPage({ children, editUrl }) {
<a className="flex no-underline font-semibold text-yellow-li" href={editUrl} target="_blank">
Edit this page
<span className="mx-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</span>

View File

@ -1,4 +1,3 @@
import Link from "next/link";
import ReactPlayer from "react-player";
const videoLinks = [

167
site/components/Tooltip.js Normal file
View File

@ -0,0 +1,167 @@
import { useState, useEffect, useRef, Fragment } from 'react'
import {
arrow,
autoPlacement,
FloatingPortal,
inline,
offset,
shift,
useDismiss,
useFloating,
useHover,
useFocus,
useInteractions,
useRole,
} from '@floating-ui/react-dom-interactions'
import { motion, AnimatePresence } from 'framer-motion';
import { allOtherPages } from 'contentlayer/generated';
import documentExtract from '../utils/documentExtract'
const tooltipBoxStyle = (theme) => ({
height: 'auto',
maxWidth: '40rem',
padding: '1rem',
background: theme === 'light' ? '#fff' : '#000',
color: theme === 'light' ? 'rgb(99, 98, 98)' : '#A8A8A8',
borderRadius: '4px',
boxShadow: 'rgba(0, 0, 0, 0.55) 0px 0px 16px -3px',
})
const tooltipBodyStyle = (theme) => ({
maxHeight: '4.8rem',
position: 'relative',
lineHeight: '1.2rem',
overflow: 'hidden',
})
const tooltipArrowStyle = ({ theme, x, y, side }) => ({
position: "absolute",
left: x != null ? `${x}px` : '',
top: y != null ? `${y}px` : '',
right: '',
bottom: '',
[side]: '-4px',
height: "8px",
width: "8px",
background: theme === 'light' ? '#fff' : '#000',
transform: "rotate(45deg)"
})
// export const Tooltip = ({ absolutePath, render, ...props }) => {
export const Tooltip = ({ render, ...props }) => {
const theme = 'light'; // temporarily hard-coded; light theme tbd in next PR
const arrowRef = useRef(null);
const [ showTooltip, setShowTooltip ] = useState(false);
const [ tooltipContent, setTooltipContent ] = useState("");
const [ tooltipContentLoaded, setTooltipContentLoaded ] = useState(false);
// floating-ui hook
const {
x,
y,
reference, // trigger element back ref
floating, // tooltip back ref
placement, // default: 'bottom'
strategy, // default: 'absolute'
context,
middlewareData: { arrow: { x: arrowX, y: arrowY } = {}} // data for arrow positioning
} = useFloating({
open: showTooltip, // state value binding
onOpenChange: setShowTooltip, // state value setter
middleware: [
offset(5), // offset from container border
autoPlacement({ padding: 5 }), // auto place vertically
shift({ padding: 5 }), // flip horizontally if necessary
arrow({ element: arrowRef, padding: 4 }), // add arrow element
inline(), // correct position for multiline anchor tags
]
});
// floating-ui hook
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, { delay: 100 }),
useFocus(context),
useRole(context, { role: 'tooltip' }),
useDismiss(context, { ancestorScroll: true }),
]);
const triggerElementProps = getReferenceProps({ ...props, ref: reference});
const tooltipProps = getFloatingProps({
ref: floating,
style: {
position: strategy,
left: x ?? '',
top: y ?? '',
},
});
const arrowPlacement = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]];
const fetchTooltipContent = async () => {
setTooltipContentLoaded(false);
let content;
// get tooltip content
try {
// create a temporary anchor tag to convert relative href to absolute path
const tempLink = document.createElement("a");
tempLink.href = props.href;
const filePath = tempLink.pathname.slice(1) // remove slash from the beginning
// disallow tooltips for 'notes' pages for now due to their different structure
if (filePath.includes('notes')) {
return
}
const page = allOtherPages.find(p => p._raw.sourceFilePath === filePath)
content = documentExtract(page.body.raw);
} catch {
return
}
setTooltipContent(content);
setTooltipContentLoaded(true);
}
useEffect(() => {
if (showTooltip) {
fetchTooltipContent();
}
}, [showTooltip])
return (
<Fragment>
{ render(triggerElementProps) }
<FloatingPortal>
<AnimatePresence>
{ showTooltip && tooltipContentLoaded &&
<motion.div
{...tooltipProps}
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", damping: 20, stiffness: 300 }}
>
<div className="tooltip-box" style={ tooltipBoxStyle(theme) }>
<div className="tooltip-body" style={ tooltipBodyStyle(theme) }>
{ tooltipContent }
</div>
</div>
<div ref={arrowRef} className="tooltip-arrow" style={ tooltipArrowStyle({
theme,
x: arrowX,
y: arrowY,
side: arrowPlacement
})}>
</div>
</motion.div>
}
</AnimatePresence>
</FloatingPortal>
</Fragment>
)
}

6319
site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@
"start": "next start"
},
"dependencies": {
"@floating-ui/react-dom-interactions": "^0.6.0",
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@mdx-js/loader": "^2.0.0",
@ -15,7 +16,9 @@
"@silvenon/remark-smartypants": "^1.0.0",
"@tailwindcss/typography": "^0.5.2",
"contentlayer": "^0.1.2",
"framer-motion": "^6.3.3",
"gray-matter": "^4.0.3",
"hast-util-to-string": "^2.0.0",
"next": "^12.1.0",
"next-contentlayer": "^0.1.2",
"next-seo": "^4.28.1",
@ -26,9 +29,11 @@
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.0",
"remark-parse": "^10.0.1",
"remark-slug": "^7.0.0",
"remark-toc": "^8.0.0",
"remark-wiki-link-plus": "^1.0.0"
"remark-wiki-link-plus": "^1.0.0",
"unist-util-find": "^1.0.2"
},
"devDependencies": {
"autoprefixer": "^10.2.6",

View File

@ -27,3 +27,14 @@ body {
.extra-small {
font-size: 0.50rem;
}
/* tooltip fade-out clip */
.tooltip-body::after {
content: "";
position: absolute;
right: 0;
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
width: 10rem;
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 100%);
}

View File

@ -0,0 +1,14 @@
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import find from 'unist-util-find'
import { toString } from 'mdast-util-to-string'
// get first paragraph found in the document
const documentExtract = (md) => {
const mdast = unified().use(remarkParse).parse(md);
let paragraph = find(mdast, (node) => node.type === "paragraph");
return toString(paragraph);
}
export default documentExtract;