From 213c293fa8b9c9a5be4d030803fbc76fb9a052ba Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Fri, 10 Dec 2021 18:13:47 +0100 Subject: [PATCH 01/13] Epic/flows (#301) * add cell identifier * use special outputplugin * Add article V2 component folder * add style for ArticleLayer_placeholder_bg * update ArticleV2 * add useRefWithCallback hook for when DOM is ready * use hook useInjectTrustedJavascript for cellFigure * Update ArticleCellOutputPlugin.js * Update ArticleCell.js * mask layer now undergoes the header in ArticleFlow * add style module * remove manual height offsets in ArticleLayers * move ArticleLayer style from scss global file to specific module file * add text-hermeneutics as global class * use search query param to discern between component version to use * add ArticleVersionQueryParam ('v=') as constant * add observer feature * add info for the table of contents * add store function for V2 article table of contents * add "active" class based on ArticleCellObserver * Create ArticleFlow.module.css * add back button to go back from the layer * add DisplayPreviousLayerQueryParam in query params * Update ArticleLayer.js --- src/components/Article/ArticleCell.js | 4 +- src/components/Article/ArticleCellFigure.js | 20 +- src/components/Article/ArticleCellOutput.js | 55 ++++- .../Article/ArticleCellOutputPlugin.js | 26 +++ src/components/ArticleV2/Article.js | 86 ++++++++ .../ArticleV2/ArticleCellObserver.js | 23 ++ .../ArticleV2/ArticleCellPlaceholder.js | 43 ++++ src/components/ArticleV2/ArticleFlow.js | 102 +++++++++ .../ArticleV2/ArticleFlow.module.css | 11 + src/components/ArticleV2/ArticleLayer.js | 207 ++++++++++++++++++ .../ArticleV2/ArticleLayer.module.css | 142 ++++++++++++ src/components/ArticleV2/ArticleLayerCopy.js | 58 +++++ .../ArticleV2/ArticleLayerSwitch.js | 36 +++ src/components/ArticleV2/ArticleLayers.js | 99 +++++++++ src/components/ArticleV2/ArticleToC.js | 176 +++++++++++++++ src/components/ArticleV2/ArticleToCStep.js | 50 +++++ src/components/ArticleV2/index.js | 1 + src/components/Echo.js | 8 + src/components/ScrollToTop.js | 30 +-- src/constants.js | 5 +- src/hooks/graphics.js | 69 +++++- src/logic/ipynb.js | 6 +- src/pages/NotebookViewer.js | 15 +- src/store.js | 16 ++ src/styles/article.scss | 6 +- src/styles/index.scss | 14 ++ 26 files changed, 1271 insertions(+), 37 deletions(-) create mode 100644 src/components/Article/ArticleCellOutputPlugin.js create mode 100644 src/components/ArticleV2/Article.js create mode 100644 src/components/ArticleV2/ArticleCellObserver.js create mode 100644 src/components/ArticleV2/ArticleCellPlaceholder.js create mode 100644 src/components/ArticleV2/ArticleFlow.js create mode 100644 src/components/ArticleV2/ArticleFlow.module.css create mode 100644 src/components/ArticleV2/ArticleLayer.js create mode 100644 src/components/ArticleV2/ArticleLayer.module.css create mode 100644 src/components/ArticleV2/ArticleLayerCopy.js create mode 100644 src/components/ArticleV2/ArticleLayerSwitch.js create mode 100644 src/components/ArticleV2/ArticleLayers.js create mode 100644 src/components/ArticleV2/ArticleToC.js create mode 100644 src/components/ArticleV2/ArticleToCStep.js create mode 100644 src/components/ArticleV2/index.js create mode 100644 src/components/Echo.js diff --git a/src/components/Article/ArticleCell.js b/src/components/Article/ArticleCell.js index f04b7f0c..e4d64376 100644 --- a/src/components/Article/ArticleCell.js +++ b/src/components/Article/ArticleCell.js @@ -87,7 +87,7 @@ const ArticleCell = ({ figureColumnLayout={cellObjectBootstrapColumnLayout} isNarrativeStep={isNarrativeStep} > - + ) } @@ -122,7 +122,7 @@ const ArticleCell = ({
{outputs.length - ? outputs.map((output,i) => ) + ? outputs.map((output,i) => ) :
no output
} diff --git a/src/components/Article/ArticleCellFigure.js b/src/components/Article/ArticleCellFigure.js index 62e93a2c..e150dcb6 100644 --- a/src/components/Article/ArticleCellFigure.js +++ b/src/components/Article/ArticleCellFigure.js @@ -2,13 +2,13 @@ import React from 'react' import ArticleCellOutput from './ArticleCellOutput' import ArticleFigure from './ArticleFigure' import { markdownParser } from '../../logic/ipynb' +import { useInjectTrustedJavascript } from '../../hooks/graphics' import {BootstrapColumLayout} from '../../constants' import { Container, Row, Col} from 'react-bootstrap' const ArticleCellFigure = ({ figure, metadata={}, outputs=[], sourceCode, figureColumnLayout, children }) => { const isFluidContainer = figure.isCover || (metadata.tags && metadata.tags.includes('full-width')) const captions = outputs.reduce((acc, output) => { - if (output.metadata && Array.isArray(output.metadata?.jdh?.object?.source)) { acc.push(markdownParser.render(output.metadata.jdh.object.source.join('\n'))) } @@ -29,8 +29,24 @@ const ArticleCellFigure = ({ figure, metadata={}, outputs=[], sourceCode, figure if (metadata.jdh?.object?.bootstrapColumLayout) { figureColumnLayout = { ...figureColumnLayout, ...metadata.jdh?.object?.bootstrapColumLayout } } + + // use scripts if there areany + const trustedScripts = outputs.reduce((acc, output) => { + if (typeof output.data === 'object') { + if (Array.isArray(output.data['application/javascript'])) { + return acc.concat(output.data['application/javascript']) + } + } + return acc + }, []) + + const refTrustedJavascript = useInjectTrustedJavascript({ + id: `trusted-script-for-${figure.ref}`, + contents: trustedScripts + }) + return ( -
+
diff --git a/src/components/Article/ArticleCellOutput.js b/src/components/Article/ArticleCellOutput.js index a57d1fda..4ee5f816 100644 --- a/src/components/Article/ArticleCellOutput.js +++ b/src/components/Article/ArticleCellOutput.js @@ -1,21 +1,50 @@ -import React from 'react' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import {markdownParser} from '../../logic/ipynb' +import ArticleCellOutputPlugin from './ArticleCellOutputPlugin' + const getOutput = (output) => { return Array.isArray(output) ? output.join(' ') : output } -const ArticleCellOutput = ({ output, height, width, hideLabel=false }) => { +const ArticleCellOutput = ({ output, height, width, hideLabel=false, isTrusted=true, cellIdx=-1 }) => { const outputTypeClassName= `ArticleCellOutput_${output.output_type}` const { t } = useTranslation() - const style = !isNaN(width) && !isNaN(height) ? { // constrain output to this size. used for images. width, height, } : {} + const trustedScripts = !!output.data && isTrusted && Array.isArray(output.data['application/javascript']) + ? output.data['application/javascript'] + : [] + // apply scripts if found on data. + useEffect(() => { + if (!trustedScripts.length || isNaN(cellIdx)) { + return + } + console.debug('[ArticleCellOutput] @useEffect found javscript trusted scripts at cellIdx:', cellIdx) + const scriptDomElementId = 'trusted-article-cell-output-' + String(cellIdx) + let scriptDomElement = document.getElementById(scriptDomElementId) + if (scriptDomElement === null) { + const script = document.createElement('script'); + script.setAttribute('id', scriptDomElementId) + script.appendChild(document.createTextNode(trustedScripts.join('\n'))); + document.body.appendChild(script) + } else { + // replace contents of the script + scriptDomElement.appendChild(document.createTextNode(trustedScripts.join('\n'))); + } + return () => { + try { + document.body.removeChild(scriptDomElement) + } catch(e) { + console.warn('document.body.removeChild failed for ', cellIdx, e.message) + } + } + }, [trustedScripts, cellIdx]) if(output.output_type === 'display_data' && output.data['text/markdown']) { return ( @@ -25,9 +54,23 @@ const ArticleCellOutput = ({ output, height, width, hideLabel=false }) => { ) } if (['execute_result', 'display_data'].includes(output.output_type) && output.data['text/html']) { - return (
) + if (trustedScripts.length) { + // use DOM to handle this + return ( + + ) + } + return ( +
+ ) } return ( diff --git a/src/components/Article/ArticleCellOutputPlugin.js b/src/components/Article/ArticleCellOutputPlugin.js new file mode 100644 index 00000000..488ef3c9 --- /dev/null +++ b/src/components/Article/ArticleCellOutputPlugin.js @@ -0,0 +1,26 @@ +import React from 'react' + +class ArticleCellOutputPlugin extends React.Component { + constructor(props) { + super(props) + console.info('ArticleCellOutputPlugin created for cell idx:', props.cellIdx) + } + + componentDidMount() { + console.info('ArticleCellOutputPlugin @componentDidMount for cell idx:', this.props.cellIdx) + this.el.innerHTML = this.props.trustedInnerHTML + } + + componentDidUpdate(prevProps) { + console.info('ArticleCellOutputPlugin @componentDidUpdate for cell idx:', this.props.cellIdx) + if (prevProps.trustedInnerHTML !== this.props.trustedInnerHTML) { + console.info('ArticleCellOutputPlugin changed.', prevProps.trustedInnerHTML) + } + } + + render() { + return
this.el = el} />; + } +} + +export default ArticleCellOutputPlugin diff --git a/src/components/ArticleV2/Article.js b/src/components/ArticleV2/Article.js new file mode 100644 index 00000000..2bc953b6 --- /dev/null +++ b/src/components/ArticleV2/Article.js @@ -0,0 +1,86 @@ +import React, { useEffect } from 'react' +import { useIpynbNotebookParagraphs } from '../../hooks/ipynb' +import { useCurrentWindowDimensions } from '../../hooks/graphics' +import ArticleHeader from '../Article/ArticleHeader' +import ArticleFlow from './ArticleFlow' + +import { setBodyNoScroll } from '../../logic/viewport' + +const Article = ({ + // pid, + // Notebook instance, an object containing {cells:[], metadata:{}} + ipynb, + url, + publicationDate = new Date(), + publicationStatus, + issue, + // plainTitle, + // plainContributor = '', + // plainKeywords = [], + // excerpt, + doi, + binderUrl, + bibjson, + emailAddress +}) => { + + const { height, width } = useCurrentWindowDimensions() + const articleTree = useIpynbNotebookParagraphs({ + id: url, + cells: ipynb?.cells ?? [], + metadata: ipynb?.metadata ?? {} + }) + const { + title, + abstract, + keywords, + contributor, + collaborators, + disclaimer = [] + } = articleTree.sections + console.debug(`[Article] component rendered ${width}x${height}px`) + + useEffect(() => { + setBodyNoScroll(true) + return function() { + setBodyNoScroll(false) + } + },[]) + + return ( + <> +
+ + + + +
+ + ) +} + +export default Article diff --git a/src/components/ArticleV2/ArticleCellObserver.js b/src/components/ArticleV2/ArticleCellObserver.js new file mode 100644 index 00000000..dfea22ab --- /dev/null +++ b/src/components/ArticleV2/ArticleCellObserver.js @@ -0,0 +1,23 @@ +import React, {useEffect } from 'react' +import { useOnScreen } from '../../hooks/graphics' + +const ArticleCellObserver = ({ cell, children, onCellIntersectionChange, className, ...rest }) => { + const [{ isIntersecting, intersectionRatio }, ref] = useOnScreen({ + rootMargin: '-20% 0% -25% 0%', + threshold: [0, 0.25, 0.75, 1] + }) + useEffect(() => { + if (typeof onCellIntersectionChange === 'function') { + onCellIntersectionChange({ idx: cell.idx, isIntersecting }) + } else { + console.debug('[ArticleCellObserver] cell:', cell.idx, isIntersecting ? 'is in view' : 'disappeared' ) + } + }, [intersectionRatio, isIntersecting]) + return ( +
+ {children} +
+ ) +} + +export default ArticleCellObserver diff --git a/src/components/ArticleV2/ArticleCellPlaceholder.js b/src/components/ArticleV2/ArticleCellPlaceholder.js new file mode 100644 index 00000000..44daf753 --- /dev/null +++ b/src/components/ArticleV2/ArticleCellPlaceholder.js @@ -0,0 +1,43 @@ +import React from 'react' +import { Container, Row, Col } from 'react-bootstrap' +import { BootstrapColumLayout } from '../../constants' +import ArticleCellContent from '../Article/ArticleCellContent' +import ArticleCellSourceCode from '../Article/ArticleCellSourceCode' + +const ArticleCellPlaceholder = ({ + type='code', + layer, + num=1, + content='', + idx, + headingLevel=0, +}) => { + return ( + + + + {type === 'markdown' + ? ( + + ) + : ( + + ) + } + + + + ) +} + +export default ArticleCellPlaceholder diff --git a/src/components/ArticleV2/ArticleFlow.js b/src/components/ArticleV2/ArticleFlow.js new file mode 100644 index 00000000..b4464bc9 --- /dev/null +++ b/src/components/ArticleV2/ArticleFlow.js @@ -0,0 +1,102 @@ +import React from 'react' +import { LayerNarrative, LayerHermeneutics, LayerData, LayerHidden } from '../../constants' +import ArticleLayers from './ArticleLayers' +import ArticleToC from './ArticleToC' +import styles from './ArticleFlow.module.css' + +import { useArticleToCStore } from '../../store' + +const ArticleFlow = ({ + memoid='', + paragraphs=[], + height, + width, + // onCellChange, + // onCellClick, + // onVisibilityChange + // hasBibliography, + // binderUrl, + // emailAddress, + headingsPositions=[], + tocOffset=99, + layers=[LayerNarrative, LayerHermeneutics, LayerData], + children +}) => { + const setVisibleCell = useArticleToCStore(store => store.setVisibleCell) + const paragraphsGroups = React.useMemo(() => { + const buffers = [] + let previousLayer = null + let buffer = new Array() + paragraphs.forEach((cell,i) => { + // skip hidden paragraphs + if (cell.layer === LayerHidden) { + return + } + if (i > 0 && (cell.layer !== previousLayer || cell.isHeading)) { + buffers.push([...buffer]) + buffer = [] + } + buffer.push(i) + // copy value + previousLayer = String(cell.layer) + }) + if (buffer.length) { + buffers.push(buffer) + } + return buffers + }, [memoid]) + + const onPlaceholderClickHandler = (e,cell) => { + console.debug('[ArticleFlow] @onPlaceholderClickHandler', e,cell) + } + const onCellIntersectionChangeHandler = ({ idx, isIntersecting }) => { + console.debug('[ArticleFlow] @onCellIntersectionChangeHandler', idx) + setVisibleCell(idx, isIntersecting) + } + console.debug(`[ArticleFlow] component rendered, size: ${width}x${height}px`) + return ( + <> +
+
+ +
+ +
+ + + {children} + +
+ + ) +} + +export default React.memo(ArticleFlow, (nextProps, prevProps) => { + if (nextProps.width !== prevProps.width || nextProps.height !== prevProps.height) { + return false + } + return nextProps.memoid === prevProps.memoid +}) diff --git a/src/components/ArticleV2/ArticleFlow.module.css b/src/components/ArticleV2/ArticleFlow.module.css new file mode 100644 index 00000000..4df1609c --- /dev/null +++ b/src/components/ArticleV2/ArticleFlow.module.css @@ -0,0 +1,11 @@ +.tocWrapper{ + position: fixed; + /* width according to 2 col in bootstrap grid system, see breakpoints */ + width: 16%; + right: 0; + pointer-events: none; + z-index: 5; + display: flex; + /* border-top: 1px solid var(--dark); */ + flex-direction: column; +} diff --git a/src/components/ArticleV2/ArticleLayer.js b/src/components/ArticleV2/ArticleLayer.js new file mode 100644 index 00000000..419655b7 --- /dev/null +++ b/src/components/ArticleV2/ArticleLayer.js @@ -0,0 +1,207 @@ +import React, { useEffect } from 'react' +import { LayerNarrative } from '../../constants' +import ArticleCell from '../Article/ArticleCell' +import ArticleCellObserver from './ArticleCellObserver' +import ArticleCellPlaceholder from './ArticleCellPlaceholder' +import {a, useSpring, config} from 'react-spring' +import { useRefWithCallback } from '../../hooks/graphics' +import { Button } from 'react-bootstrap' +import { ArrowRight, ArrowLeft, Bookmark } from 'react-feather' +import styles from './ArticleLayer.module.css' + +function getCellAnchorFromIdx(idx, prefix='c') { + return `${prefix}${idx}` +} + +function layerTransition(x, y, width, height) { + return `polygon(${x}px 0px, ${x}px ${height}px, ${width}px ${height}px, ${width}px 0px)` +} + +function cx(...rest) { + return styles[rest.join('_')] +} + +const ArticleLayer = ({ + memoid='', + layer=LayerNarrative, + // previousLayer=null, + // nextLayer=null, + paragraphsGroups=[], + paragraphs=[], + // index of selected cell (cell.idx) + selectedCellIdx=-1, + selectedCellTop=0, + onCellPlaceholderClick, + onCellIntersectionChange, + isSelected=false, + selectedLayer='', + previousLayer='', + layers=[], + children, + width=0, height=0, + style, + FooterComponent = function({ width, height }) { return
}, +}) => { + const [mask, setMask] = useSpring(() => ({ + clipPath: [width, 0, width, height], x:0, y:0, + config: config.slow + })) + const layerRef = useRefWithCallback((layerDiv) => { + if (!isSelected || selectedCellIdx === -1) { // discard + return + } + // get cellEmeemnt in current layer (as it can be just a placeholder,too) + const cellElement = document.getElementById(getCellAnchorFromIdx(selectedCellIdx, layer)) + if (!cellElement) { + console.warn('Not found! celleElment with given id:', selectedCellIdx) + return + } + console.debug('[ArticleLayer] useRefWithCallback:', selectedCellIdx, layer, 'selectedCellTop', selectedCellTop, cellElement.offsetTop, getCellAnchorFromIdx(selectedCellIdx, layer)) + layerDiv.scrollTo({ top: cellElement.offsetTop + layerDiv.offsetTop - selectedCellTop }) + }) + + const onCellPlaceholderClickHandler = (e, cell) => { + if (typeof onCellPlaceholderClick === 'function') { + onCellPlaceholderClick(e, { + layer: cell.layer, + idx: cell.idx, + height, // ref height + y: e.currentTarget.parentNode.parentNode.getBoundingClientRect().y + }) + } else { + console.warn('[ArticleLayer] misses a onCellPlaceholderClick listener') + } + } + + const onSelectedCellClickHandler = (e, cell) => { + if (typeof onCellPlaceholderClick === 'function') { + onCellPlaceholderClick(e, { + layer: previousLayer, + idx: cell.idx, + height, // ref height + y: e.currentTarget.parentNode.getBoundingClientRect().y + }) + } + } + + useEffect(() => { + if (layers.indexOf(layer) <= layers.indexOf(selectedLayer)) { + console.info('open', layer, layers.indexOf(selectedLayer), layers.indexOf(layer)) + setMask.start({ clipPath: [0, 0, width, height], x:-width, y:0 }) + } else if (layers.indexOf(layer) > layers.indexOf(selectedLayer)) { + console.info('close', layer, layers.indexOf(selectedLayer), layers.indexOf(layer)) + setMask.start({ clipPath: [width, 0, width, height], x:0, y:0 }) + } + }, [isSelected, layer, layers]) + + console.debug('[ArticleLayer] rendered: ',layer,'- n. groups:', paragraphsGroups.length) + + return ( + +
+
+ {children} + {paragraphsGroups.map((paragraphsIndices, i) => { + const firstCellInGroup = paragraphs[paragraphsIndices[0]] + const isPlaceholder = firstCellInGroup.layer !== layer + if (isPlaceholder) { + return ( + + {paragraphsIndices.map((k) => ( + + ))} +
+ {paragraphsIndices.slice(0,2).map((j) => ( + + + + ))} +
+
+ +
+
+ +
+
+ + ) + } + + return ( + + {paragraphsIndices.map((j) => { + const cell = paragraphs[j] + if(!cell) { + // eslint-disable-next-line + debugger + } + return ( + + + +
+ +
+ { cell.idx === selectedCellIdx && previousLayer !== '' && previousLayer !== layer ? ( + + ) : null} + {/* debug && selectedCellIdx === cell.idx && previousLayer ? ( +
+ +
+ ):null */} + {/* debug && selectedCellIdx === cell.idx && nextLayer ? ( +
+ +
+ ):null */} + +
+
+ ) + })} +
+ ) + })} +
+ + + ) +} + +export default ArticleLayer diff --git a/src/components/ArticleV2/ArticleLayer.module.css b/src/components/ArticleV2/ArticleLayer.module.css new file mode 100644 index 00000000..f5424f09 --- /dev/null +++ b/src/components/ArticleV2/ArticleLayer.module.css @@ -0,0 +1,142 @@ +.mask{ + position: absolute; +} +.mask_narrative{ + composes: mask; +} +.mask_hermeneutics{ + composes: mask; + background-color: #B1FBEC; +} +.mask_data{ + composes: mask; + background-color: #cccfff; +} + +.push{ + /* equals the top header */ + height: 100px; +} +.pushFixed{ + position: fixed; + background-color: red; + top: 0; + height: 100px; + width:100%; + z-index:1; + /* border-bottom: 1px solid var(--secondary); */ +} + +.pushFixed_narrative{ + composes: pushFixed; + background-color: rgb(244, 241, 248); +} +.pushFixed_hermeneutics{ + composes: pushFixed; + background-color: #B1FBEC; +} + +.pushFixed_data{ + composes: pushFixed; + background-color: #cccfff; +} + +.anchor{ + position: relative; +} +.placeholder{ + max-height: 200px; + overflow: hidden; +} +.placeholderGradient{ + position: absolute; + z-index:0; + bottom: 0; + pointer-events: none; + left: 0; + right:0; + height: 50%; + background: transparent +} +.placeholderButton{ + position: absolute; + z-index: 0; + left: 50%; + width: 250px; + margin-left: -125px; + bottom: var(--spacer-3); +} + +.placeholder_narrative_hermeneutics { + composes: placeholder; + color: var(--primary-dark); +} +.placeholder_hermeneutics_narrative { + composes: placeholder; + color: var(--dark); +} + +.placeholder_data_narrative { + composes: placeholder; + color: var(--dark); +} +.placeholder_data_hermeneutics { + composes: placeholder; + color: var(--dark); +} + +.placeholderGradient_narrative_hermeneutics{ + composes: placeholderGradient; + background: linear-gradient(180deg, rgb(244, 241, 248, 0) 0%, rgb(244, 241, 248, .8) 50%); +} +.placeholderGradient_hermeneutics_narrative{ + composes: placeholderGradient; + background: linear-gradient(180deg, #B1FBEC99 0%, #B1FBECff 90%); +} +.placeholderGradient_data_narrative{ + composes: placeholderGradient; + background: linear-gradient(180deg, #cccfff99 0%, #cccfffff 90%); +} +.placeholderGradient_data_hermeneutics +{ + composes: placeholderGradient; + background: linear-gradient(180deg, #cccfff99 0%, #cccfffff 90%); +} + +.placeholderActive { + position: absolute; + left: var(--spacer-2); + background: transparent; + height: 100%; + right: 20%; + z-index: -1; + top: 0; +} +/* .placeholderActive:after{ + content: ''; + position: absolute; + top: 5px; + left: 5px; + width: 2px; + background: var(--dark); + bottom: 5px; +} */ +.placeholderActive_narrative_on, +.placeholderActive_hermeneutics_on, +.placeholderActive_data_on, .cellActive_on{ + border-top: 1px solid; + /* border-bottom: 1px double; */ + composes: placeholderActive; + /* background: #ffffff77; */ +} +.placeholderActive_off, .cellActive_off{ + composes: placeholderActive; + background: transparent; + opacity: 0; +} +.cellActiveBackButton{ + position: absolute; + left: 25px; + top: var(--spacer-2); + /* border-radius: 100%; */ +} diff --git a/src/components/ArticleV2/ArticleLayerCopy.js b/src/components/ArticleV2/ArticleLayerCopy.js new file mode 100644 index 00000000..d96c6f29 --- /dev/null +++ b/src/components/ArticleV2/ArticleLayerCopy.js @@ -0,0 +1,58 @@ +import React from 'react' +import { LayerNarrative } from '../../constants' +import { useGesture } from "react-use-gesture" +import { useSpring, animated, config } from '@react-spring/web' +import ArticleCell from '../Article/ArticleCell' + +const ArticleLayer = ({ + memoid='', + layer=LayerNarrative, + cells=[], + onPlaceholderClick, + ...rest +}) => { + const [styles,api] = useSpring(() => ({ y:0, config: config.stiff})) + // const runSprings = useCallback((y, vy) => { + // console.info('ArticleLayer', layer, y, vy) + // api.start({ y: -y }) + // }, []) + // const wheelOffset = useRef(0) + // const dragOffset = useRef(0) + const bind = useGesture({ + onWheel: ({ offset: [, y], vxvy: [, vy] }) => { + console.info('onWheel', vy) + api.start({ y: -y }) + } + // onDrag: ({ offset: [x], vxvy: [vx] }) => vx && ((dragOffset.current = -x), runSprings(wheelOffset.current + -x, -vx)), + // onWheel: ({ offset: [, y], vxvy: [, vy] }) => vy && ((wheelOffset.current = Math.max(0, y)), runSprings(dragOffset.current + Math.max(0, y), vy)) + }) + return ( + + {layer} + + {cells.map((cell, i) => { + if (cell.isPlaceholder) { + return ( +
PLACEHOLDER! {cell.layer} {cell.idx}
+ ) + } + return ( +
{cell.layer} {cell.idx} + +
+ + ) + })} +
+
+ ) +} + +export default ArticleLayer diff --git a/src/components/ArticleV2/ArticleLayerSwitch.js b/src/components/ArticleV2/ArticleLayerSwitch.js new file mode 100644 index 00000000..7dc0bd12 --- /dev/null +++ b/src/components/ArticleV2/ArticleLayerSwitch.js @@ -0,0 +1,36 @@ +import React, { useEffect } from 'react' +import { useQueryParams, StringParam, NumberParam, withDefault, } from 'use-query-params' +import { + DisplayLayerQueryParam, + DisplayLayerNarrative, + DisplayLayerCellIdxQueryParam +} from '../../constants' + +const ArticleLayerSwitch = ({ + layers=[], + onChange +}) => { + const [{[DisplayLayerQueryParam]:layer}, setQuery] = useQueryParams({ + [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, 0), + [DisplayLayerQueryParam]: withDefault(StringParam, DisplayLayerNarrative) + }) + useEffect(() => { + if (typeof onChange === 'function') { + onChange(layer) + } else { + console.warn('No onChange function detected, can\'t forward layer from useQueryParam', layer); + } + }, [layer]) + + return ( + <> + {layers.map((d,i) => ( +
setQuery({ + [DisplayLayerQueryParam]: d + })}>{d}
+ ))} + + ) +} + +export default ArticleLayerSwitch diff --git a/src/components/ArticleV2/ArticleLayers.js b/src/components/ArticleV2/ArticleLayers.js new file mode 100644 index 00000000..70fb335f --- /dev/null +++ b/src/components/ArticleV2/ArticleLayers.js @@ -0,0 +1,99 @@ +import React from 'react' +import ArticleLayer from './ArticleLayer' +import { useQueryParams, StringParam, NumberParam, withDefault, } from 'use-query-params' +import { + DisplayLayerQueryParam, + LayerNarrative, + DisplayLayerCellIdxQueryParam, + DisplayLayerCellTopQueryParam, + DisplayPreviousLayerQueryParam +} from '../../constants' + + +const ArticleLayers = ({ + memoid='', + layers=[], + paragraphsGroups=[], + paragraphs=[], + width=0, + height=0, + onCellPlaceholderClick, + onCellIntersectionChange, + children, +}) => { + // Store indicies as a local ref, this represents the item order [0,1,2] + // const order = React.useRef(layers.map((_, index) => index)) + const [{ + [DisplayLayerQueryParam]:selectedLayer, + [DisplayLayerCellIdxQueryParam]:selectedCellIdx, + [DisplayLayerCellTopQueryParam]: selectedCellTop, + [DisplayPreviousLayerQueryParam]: previousLayer, + }, setQuery] = useQueryParams({ + [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1), + [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative), + [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 0), + [DisplayPreviousLayerQueryParam]: StringParam, + }) + + // const [springs, api] = useSprings(layers.length, fn(order.current)) // Create springs, each corresponds to an item, controlling its transform, scale, etc. + + const onCellPlaceholderClickHandler = (e, { layer, idx, y }) => { + console.debug('[ArticleLayers] @onCellPlaceholderClickHandler:', layer, idx, y) + // replaceIn not to trgger the changes. This is helpful whenever the user + // hits the back Button in the browser (or uses the swipe left on mobile) + setQuery({ + [DisplayLayerQueryParam]: selectedLayer, + [DisplayLayerCellIdxQueryParam]: idx, + [DisplayLayerCellTopQueryParam]: y, + [DisplayPreviousLayerQueryParam]: previousLayer, + }, 'replaceIn') + // this query + setQuery({ + [DisplayLayerQueryParam]: layer, + [DisplayLayerCellIdxQueryParam]: idx, + [DisplayLayerCellTopQueryParam]: y, + [DisplayPreviousLayerQueryParam]: selectedLayer, + }) + if (typeof onCellPlaceholderClick === 'function') { + onCellPlaceholderClick(e, { layer, idx, y }) + } + } + console.debug('[ArticleLayers] rendered, selected:', selectedLayer) + return ( + <> + {layers.map((layer, i) => ( + 0 ? layers[i-1] : null} + nextLayer={i < layers.length - 1 ? layers[i+1]: null} + onCellPlaceholderClick={onCellPlaceholderClickHandler} + onCellIntersectionChange={onCellIntersectionChange} + selectedCellIdx={selectedCellIdx} + selectedCellTop={selectedCellTop} + isSelected={selectedLayer === layer} + selectedLayer={selectedLayer} + previousLayer={previousLayer} + height={height} + width={width} + layers={layers} + style={{ + width, + height, + top: 0, + overflow: selectedLayer === layer ? "scroll": "hidden", + zIndex: i, + // left: i * width + }} + > + {children} + + ))} + + ) +} + +export default ArticleLayers diff --git a/src/components/ArticleV2/ArticleToC.js b/src/components/ArticleV2/ArticleToC.js new file mode 100644 index 00000000..30e0cfb5 --- /dev/null +++ b/src/components/ArticleV2/ArticleToC.js @@ -0,0 +1,176 @@ +import React from 'react' +import { useQueryParams, withDefault, NumberParam, StringParam } from 'use-query-params' +import { Bookmark } from 'react-feather' +import { Button } from 'react-bootstrap' +import { useArticleToCStore } from '../../store' +import { + LayerHermeneutics, + LayerNarrative, + DisplayLayerQueryParam, + DisplayLayerCellIdxQueryParam, + DisplayLayerCellTopQueryParam, + DisplayPreviousLayerQueryParam, +} from '../../constants' +import ArticleToCStep from './ArticleToCStep' + + +const ArticleToC = ({ + memoid='', + layers=[], + paragraphs=[], + headingsPositions=[], + width=100 +}) => { + const visibleCellsIdx = useArticleToCStore(state=>state.visibleCellsIdx) + + const [{ + [DisplayLayerQueryParam]:selectedLayer, + [DisplayLayerCellIdxQueryParam]:selectedCellIdx, + }, setQuery] = useQueryParams({ + [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1), + [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative), + [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 0), + }) + + let count = 0 + const cellIndex = React.useMemo(() => paragraphs.reduce((acc, cell) => { + acc[cell.idx] = cell + return acc + }, {}), [memoid]) + + const steps = React.useMemo(() => headingsPositions.reduce((acc, idx, i) => { + const cell = cellIndex[idx] + if(!cell) { + // is possible that there are headingPositions outside of the + // articleTree.paragraphs list (e.g in the metadata section). + // In this case, we just skip. + return acc + } + const nextCell = i < headingsPositions.length - 1 + ? cellIndex[headingsPositions[i + 1]] + : null + let isSectionStart = false + let isSectionEnd = false + if(cell.isHeading && cell.heading.level === 2) { + isSectionStart = true + } + if (count === 0) { + isSectionEnd = true + isSectionStart = true + } + if (nextCell && nextCell.isHeading && nextCell.heading.level === 2 ) { + isSectionEnd = true + } + if (!nextCell) { + isSectionEnd = true + } + + count++ + // is last only if next heading is higher than this one, or it is a hermeneutic + const isHermeneutics = cell.layer === LayerHermeneutics + return acc.concat([{ + isSectionStart, + isSectionEnd, + isHermeneutics, + cell, + count + }]) + }, []), [memoid]) + + const firstVisibleCellIdx = visibleCellsIdx.length ? visibleCellsIdx[0] : -1 + const lastVisibleCellIdx = visibleCellsIdx.length ? visibleCellsIdx[visibleCellsIdx.length -1] : -1 + + const { previousHeadingIdx } = React.useMemo(()=> { + let previousHeadingIdx = -1 + let nextHeadingIdx = -1 + for(let i = 0; i < headingsPositions.length; i++) { + if (headingsPositions[i] <= firstVisibleCellIdx) { + previousHeadingIdx = headingsPositions[i] + } + if (nextHeadingIdx === -1 && headingsPositions[i] >= lastVisibleCellIdx) { + nextHeadingIdx = headingsPositions[i] + // nextVisibleLoopIndex = i + } + } + return { + previousHeadingIdx, + nextHeadingIdx + } + }, [ firstVisibleCellIdx ]) + + const onStepClickHandler = (step) => { + console.debug('[ArticleToC] @onClickHandler step:', step, selectedLayer) + // go to the cell + setQuery({ + [DisplayLayerCellIdxQueryParam]: step.cell.idx, + [DisplayPreviousLayerQueryParam]: undefined + }) + } + + const onBookmarkClickHandler = () => { + setQuery({ + [DisplayLayerCellIdxQueryParam]: selectedCellIdx, + [DisplayPreviousLayerQueryParam]: undefined + }) + } + return ( + <> +
+ {layers.map((d,i) => ( +
setQuery({ + [DisplayLayerQueryParam]: d + })}>{selectedLayer === d ? {d}: d}
+ ))} +
+
+ {steps.map((step, i) => { + const showBookmark = i < steps.length - 1 + ? selectedCellIdx >= step.cell.idx && selectedCellIdx < steps[i+1].cell.idx + : false + return ( +
+ {showBookmark ? ( + + ): null} + = previousHeadingIdx && step.cell.idx <= lastVisibleCellIdx} + className={step.isHermeneutics ? 'hermeneutics': ''} + > + {step.cell.isHeading + ? step.cell.heading.content + : step.cell.isFigure + ? step.cell.figure.ref + : '(na)' + } + +
+ )})} +
+ + ) +} +export default React.memo(ArticleToC, (prevProps, nextProps) => { + if (prevProps.width !== nextProps.width) { + return false + } + return prevProps.memoid === nextProps.memoid +}) diff --git a/src/components/ArticleV2/ArticleToCStep.js b/src/components/ArticleV2/ArticleToCStep.js new file mode 100644 index 00000000..a31ed7e1 --- /dev/null +++ b/src/components/ArticleV2/ArticleToCStep.js @@ -0,0 +1,50 @@ +import React from 'react' +import { Layers, Image, Grid } from 'react-feather' +import { useArticleStore } from '../../store' + + +const ArticleToCStep = ({ + cell, active=false, + isSectionStart=false, + isSectionEnd=false, + children, + width=0, + marginLeft=70, + className='', + onStepClick, +}) => { + const displayLayer = useArticleStore(state=>state.displayLayer) + + const availableWidth = width - marginLeft + const levelClassName = `ArticleToCStep_Level_${cell.level}` + const labelClassName = cell.isHermeneutics + ? 'ArticleToCStep_labelHermeneutics' + : !cell.isHermeneutics && !cell.isTable && !cell.isFigure + ? 'ArticleToCStep_labelCircle' + : cell.isFigure && !cell.isTable + ? 'ArticleToCStep_labelFigure' + : 'ArticleToCStep_labelTable' + + const handleClick = () => { + if (typeof onStepClick === 'function') { + onStepClick({ cell }) + } + } + return ( +
+ +
+ {cell.isHermeneutics && !cell.isTable && !cell.isFigure && } + {!cell.isHermeneutics && !cell.isTable && !cell.isFigure &&
} + {cell.isFigure && !cell.isTable && } + {cell.isTable && } +
+
+ ) +} + +export default ArticleToCStep diff --git a/src/components/ArticleV2/index.js b/src/components/ArticleV2/index.js new file mode 100644 index 00000000..0d748e76 --- /dev/null +++ b/src/components/ArticleV2/index.js @@ -0,0 +1 @@ +export { default } from './Article'; diff --git a/src/components/Echo.js b/src/components/Echo.js new file mode 100644 index 00000000..b31d4633 --- /dev/null +++ b/src/components/Echo.js @@ -0,0 +1,8 @@ +import React from 'react' + +const Echo = () => { + console.info('Echo rendered') + return
ECHO
+} + +export default Echo diff --git a/src/components/ScrollToTop.js b/src/components/ScrollToTop.js index ae4adfd5..7ea858fa 100644 --- a/src/components/ScrollToTop.js +++ b/src/components/ScrollToTop.js @@ -4,22 +4,22 @@ import { scrollToElementById } from '../logic/viewport' const ScrollToTop = () => { - const { pathname, hash, search } = useLocation(); + const { pathname, hash } = useLocation(); // for shadow hermeneutic layer - useEffect(() => { - let timer - const qs = new URLSearchParams(search) - const idx = qs.get('idx') - clearTimeout(timer) - if (!isNaN(idx)) { - timer = setTimeout(() => { - scrollToElementById('C-' + idx) - }, 0); - } - return () => { - clearTimeout(timer) - } - }, [search]) + // useEffect(() => { + // let timer + // const qs = new URLSearchParams(search) + // const idx = qs.get('idx') + // clearTimeout(timer) + // if (!isNaN(idx)) { + // timer = setTimeout(() => { + // scrollToElementById('C-' + idx) + // }, 0); + // } + // return () => { + // clearTimeout(timer) + // } + // }, [search]) useEffect(() => { let timer; diff --git a/src/constants.js b/src/constants.js index a335ea04..de430a26 100644 --- a/src/constants.js +++ b/src/constants.js @@ -126,8 +126,11 @@ export const DisplayLayerHermeneutics = 'h' export const DisplayLayerNarrative = 'n' export const DisplayLayerAll = 'all' export const DisplayLayerQueryParam = 'layer' +export const DisplayPreviousLayerQueryParam = 'pl' export const DisplayLayerCellIdxQueryParam = 'idx' - +export const DisplayLayerCellTopQueryParam = 'y' // article status export const ArticleStatusPublished = 'PUBLISHED' export const ArticleStatusDraft = 'Draft' +// article component version +export const ArticleVersionQueryParam = 'v' diff --git a/src/hooks/graphics.js b/src/hooks/graphics.js index 860bae2c..b378f91e 100644 --- a/src/hooks/graphics.js +++ b/src/hooks/graphics.js @@ -1,4 +1,9 @@ -import { useRef, useState, useEffect} from 'react' +import { + useRef, + useState, + useEffect, + useCallback +} from 'react' /** * Calculate available rectangle for the given ref. @@ -147,3 +152,65 @@ export function useOnScreen({ threshold = [0, 1], rootMargin='0% 0% 0% 0%'} = {} }, []) return [entry, ref]; } + + +/** + * @method useRefWithCallback + * Guarantees that the DOM is ready before calling the + * onMount callback. + * Usage: + * ``` + * const refWithCallback = useRefWithCallback(...) + * + * return ( + *
...
+ * ) + * ``` + * @return + */ +export function useRefWithCallback(onMount, onUnmount) { + const nodeRef = useRef(null); + const setRef = useCallback(node => { + if (nodeRef.current && typeof onUnmount === 'function') { + onUnmount(nodeRef.current); + } + nodeRef.current = node; + if (nodeRef.current && typeof onMount === 'function') { + onMount(nodeRef.current); + } + }, [onMount, onUnmount]); + return setRef; +} + + +export function useInjectTrustedJavascript({ id='', contents=[], onMount, onUnmount }) { + const setRefWithCallback = useRefWithCallback((node) => { + if (contents.length) { + console.debug('useInjectTrustedJavascript', id, contents.length) + let scriptDomElement = document.getElementById(id) + if (scriptDomElement === null) { + const script = document.createElement('script') + script.setAttribute('id', id) + script.appendChild(document.createTextNode(contents.join('\n'))) + node.appendChild(script) + } + } + if (node && typeof onMount === 'function') { + onMount(node) + } + }, (node) => { + if (contents.length) { + let scriptDomElement = document.getElementById(id) + try { + node.removeChild(scriptDomElement) + } catch(e) { + console.warn('document.body.removeChild failed with id:', id, e.message) + } + if (node && typeof onUnmount === 'function') { + onUnmount(node) + } + } + }) + + return setRefWithCallback +} diff --git a/src/logic/ipynb.js b/src/logic/ipynb.js index 3033abe2..38db4d0e 100644 --- a/src/logic/ipynb.js +++ b/src/logic/ipynb.js @@ -182,7 +182,7 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { if (citationsFromMetadata instanceof Object) { bibliography = new Cite(Object.values(citationsFromMetadata).filter(d => d)) } - + let paragraphNumber = 0 // cycle through notebook cells to fill ArticleCells, figures, headings cells.map((cell, idx) => { const sources = Array.isArray(cell.source) @@ -251,9 +251,11 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { cell.source = Array.isArray(cell.source) ? cell.source : [cell.source] + paragraphNumber += 1 + cell.num = paragraphNumber return cell }).forEach((cell, idx) => { - // console.info('ipynb', cell.role) + // console.info('ipynb', cell.role, cell.num) if (cell.cell_type === CellTypeMarkdown) { const sources = cell.source.join('') // exclude rendering of reference references diff --git a/src/pages/NotebookViewer.js b/src/pages/NotebookViewer.js index 29b8019d..de724916 100644 --- a/src/pages/NotebookViewer.js +++ b/src/pages/NotebookViewer.js @@ -1,14 +1,12 @@ import React, { useMemo, useEffect } from 'react' import { useSpring, animated, config } from 'react-spring' -// import { useTranslation } from 'react-i18next' - +import { useQueryParams, NumberParam, withDefault } from 'use-query-params' import { useGetJSON } from '../logic/api/fetchData' import { decodeNotebookURL } from '../logic/ipynb' -import { StatusSuccess, StatusFetching } from '../constants' +import { StatusSuccess, StatusFetching, ArticleVersionQueryParam } from '../constants' import Article from '../components/Article' -// import ArticleKeywords from '../components/Article/ArticleKeywords' +import ArticleV2 from '../components/ArticleV2' import ArticleHeader from '../components/Article/ArticleHeader' - /** * Loading bar inspired by * https://codesandbox.io/s/github/pmndrs/react-spring/tree/master/demo/src/sandboxes/animating-auto @@ -31,7 +29,10 @@ const NotebookViewer = ({ bibjson, pid, }) => { - // const { t } = useTranslation() + const [{[ArticleVersionQueryParam]: version}] = useQueryParams({ + [ArticleVersionQueryParam]: withDefault(NumberParam, 0) + }) + const ArticleComponent = version === 2 ? ArticleV2 : Article const [animatedProps, api] = useSpring(() => ({ width : 0, opacity:1, config: config.slow })) const url = useMemo(() => { @@ -84,7 +85,7 @@ const NotebookViewer = ({
{status === StatusSuccess ? ( -
({ setIssue: (issue) => set(() => ({ issue })) })) +export const useArticleToCStore = create((set) => ({ + visibleCellsIdx: [], + setVisibleCell: (cellIdx, isVisible) => set((state) => { + const copy = [...state.visibleCellsIdx] + const idx = copy.indexOf(cellIdx) + if (idx === -1 && isVisible) { + copy.push(cellIdx) + } else if(idx > -1 && !isVisible){ + copy.splice(idx, 1) + } + copy.sort((a,b)=> a - b) + console.debug('[useArticleToCStore] visibleCellsIdx:', copy) + return { visibleCellsIdx: copy } + }) +})) + export const useArticleStore = create((set) => ({ // visible shadow cells according to Accordion visibleShadowCellsIdx: [], diff --git a/src/styles/article.scss b/src/styles/article.scss index f5d353df..730d38a9 100644 --- a/src/styles/article.scss +++ b/src/styles/article.scss @@ -11,6 +11,10 @@ $breakpoint-lg: 992px; margin-left: var(--spacer-4); } +.ArticleCellObserver.active .ArticleCellContent_num{ + color: var(--secondary); +} + .ArticleCellContent_num { position: absolute; left: -50px; @@ -29,7 +33,7 @@ $breakpoint-lg: 992px; top: 14px; } &.level_H3, &.level_H4, &.level_H5, &.level_H6{ - top: 21px; + top: 20px; } } diff --git a/src/styles/index.scss b/src/styles/index.scss index 123ab58a..c73b9808 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -48,6 +48,9 @@ --line-height-3: #{$font-size-base * 2.5}; --line-height-1-m: #{$font-size-base * (1.5 / 2)}; --line-height-5: #{$font-size-base * 2.75 * 1.25}; + --layer-narrative-bg-0: rgb(244, 241, 248, 0); + --layer-narrative-bg-1: rgb(244, 241, 248, 1); + --layer-narrative-hermeneutics-text: #51f6e0; } body, @@ -181,6 +184,9 @@ html, .bg-hermeneutics{ background-color: var(--hermeneutics) !important; } +.text-hermeneutics{ + color: var(--primary-dark); +} body{ background-color: var(--gray-100); color: var(--dark); @@ -457,3 +463,11 @@ blockquote.code{ padding-bottom: 0; } } + +.left-0{ + left: 0 +} + +.right-0{ + right: 0 +} From 4442c759cc758c51177ff8245201b6e43cad1a42 Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Wed, 22 Dec 2021 18:56:58 +0100 Subject: [PATCH 02/13] Fix/v2 anchor links (#304) * use scrolltop to get to the correct paragraph * fix toc memo to take into account the last visible cellIdx changes * clear cells in store ToC * clear VisibleCellsIdx list in store when layer change in ArticleLayer * add cell popup when wlick on a paragraph * add reference height in the URL query params from ArticleLayers component * Update article.scss * Update ArticleLayer.module.css * add ArticleCellPopup and use selectedLayerHeight from URL params to calculate the scroll position * add onNumClick function (works only in V2) in ArticleCellContent * add onNumClick function (works only in V2) in ArticleCellContent * add anchor-xyZ among possible figure candidate to make easy to move from one layer to the other in ipynb logic * add link support and go back nicely to the source cell in ArticleLayer component * fix bookmark icon in TableOfContent * add onDataHrefClick handler to open ArticleNote component for references * style ArticleToC * Update ArticleToCBookmark.js * Update store.js * display articleTree.anchors in console debug * add anchors from paragraph having tags "anchor" * create model ArticleAnchor.js * add AnchorRefPrefix in constants * style ArticleToc header * fix links inside placeholders * add numbering in cellplaceholder and cellsourcecode * block flow of paragraph for both figures and tables * add clickehander for placeholder number * use v2 as default * remove bogus { let cellBootstrapColumnLayout = metadata.jdh?.text?.bootstrapColumLayout || BootstrapColumLayout; // we override or set the former layout if it appears in narrative-step @@ -71,7 +72,14 @@ const ArticleCell = ({ - + @@ -87,7 +95,13 @@ const ArticleCell = ({ figureColumnLayout={cellObjectBootstrapColumnLayout} isNarrativeStep={isNarrativeStep} > - + ) } @@ -95,7 +109,15 @@ const ArticleCell = ({ - + diff --git a/src/components/Article/ArticleCellContent.js b/src/components/Article/ArticleCellContent.js index 03845354..3f3e8cb0 100644 --- a/src/components/Article/ArticleCellContent.js +++ b/src/components/Article/ArticleCellContent.js @@ -1,11 +1,34 @@ import React from 'react' -const ArticleCellContent = ({ idx, className='', content, num, hideNum=false, hideIdx=true, headingLevel=0}) => { +const ArticleCellContent = ({ + idx, className='', + content, + num, + layer, + hideNum=false, + hideIdx=true, + headingLevel=0, + onNumClick, +}) => { + const numClassNames = [ + headingLevel > 0 ? `level_H${headingLevel}`: '', + typeof onNumClick === 'function'? 'selectable': '' + ].join(' ') + const onClickHandler = (e) => { + if (typeof onNumClick === 'function') { + onNumClick(e, { idx, layer }) + } + } return (
{!hideIdx && (
{idx}
)} - {!hideNum && (
0? `level_H${headingLevel}`:''}`}>{num}
)} + {!hideNum && ( +
+ {num} +
+ )}
) diff --git a/src/components/Article/ArticleCellSourceCode.js b/src/components/Article/ArticleCellSourceCode.js index e90ba405..91e273ae 100644 --- a/src/components/Article/ArticleCellSourceCode.js +++ b/src/components/Article/ArticleCellSourceCode.js @@ -5,7 +5,10 @@ import { Eye, EyeOff } from 'react-feather' import hljs from "highlight.js"; // import hljs library -const ArticleCellSourceCode = ({ content, language, toggleVisibility, visible, right }) => { +const ArticleCellSourceCode = ({ + content, language, toggleVisibility, visible, right, + num=-1 +}) => { const [isSourceCodeVisible, setIsSourceCodeVisible] = useState(visible) const { t } = useTranslation() const highlighted = language @@ -13,7 +16,12 @@ const ArticleCellSourceCode = ({ content, language, toggleVisibility, visible, r : hljs.highlightAuto(content); return ( -
+
+ {num !== -1 ? ( +
+ {num} +
+ ):null} {toggleVisibility ? (
diff --git a/src/components/ArticleV2/Article.js b/src/components/ArticleV2/Article.js index 2bc953b6..86a7c0e4 100644 --- a/src/components/ArticleV2/Article.js +++ b/src/components/ArticleV2/Article.js @@ -1,7 +1,8 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { useIpynbNotebookParagraphs } from '../../hooks/ipynb' import { useCurrentWindowDimensions } from '../../hooks/graphics' import ArticleHeader from '../Article/ArticleHeader' +import ArticleNote from '../Article/ArticleNote' import ArticleFlow from './ArticleFlow' import { setBodyNoScroll } from '../../logic/viewport' @@ -23,7 +24,7 @@ const Article = ({ bibjson, emailAddress }) => { - + const [selectedDataHref, setSelectedDataHref] = useState(null) const { height, width } = useCurrentWindowDimensions() const articleTree = useIpynbNotebookParagraphs({ id: url, @@ -39,7 +40,12 @@ const Article = ({ disclaimer = [] } = articleTree.sections console.debug(`[Article] component rendered ${width}x${height}px`) + console.debug('[Article] loading articleTree anchors:', articleTree.anchors) + const onDataHrefClickHandler = (d) => { + console.debug('DataHref click handler') + setSelectedDataHref(d) + } useEffect(() => { setBodyNoScroll(true) return function() { @@ -60,6 +66,7 @@ const Article = ({ hasBibliography={typeof articleTree.bibliography === 'object'} binderUrl={binderUrl} emailAddress={emailAddress} + onDataHrefClick={onDataHrefClickHandler} > + {articleTree.citationsFromMetadata + ? + : null + } +
) diff --git a/src/components/ArticleV2/ArticleCellPlaceholder.js b/src/components/ArticleV2/ArticleCellPlaceholder.js index 44daf753..04a15378 100644 --- a/src/components/ArticleV2/ArticleCellPlaceholder.js +++ b/src/components/ArticleV2/ArticleCellPlaceholder.js @@ -3,15 +3,36 @@ import { Container, Row, Col } from 'react-bootstrap' import { BootstrapColumLayout } from '../../constants' import ArticleCellContent from '../Article/ArticleCellContent' import ArticleCellSourceCode from '../Article/ArticleCellSourceCode' +import {ArrowDown} from 'react-feather' + const ArticleCellPlaceholder = ({ type='code', layer, - num=1, + // whenever the placeholder stands for more than one paragraphs + nums=[], content='', idx, headingLevel=0, + // isFigure=false + onNumClick }) => { + const paragraphNumbers = nums.length + ? nums.length === 1 + ? nums[0] + : ( + + {nums[0]} +
+ +
+ {nums[nums.length -1]} +
+ ) + : null + const onNumClickHandler = (e) => { + onNumClick(e, {layer, idx}) + } return ( @@ -23,7 +44,8 @@ const ArticleCellPlaceholder = ({ layer={layer} content={content} idx={idx} - num={num} + num={paragraphNumbers} + onNumClick={onNumClickHandler} /> ) : ( @@ -31,6 +53,8 @@ const ArticleCellPlaceholder = ({ visible content={content} language="python" + num={paragraphNumbers} + onNumClick={onNumClick} /> ) } diff --git a/src/components/ArticleV2/ArticleCellPopup.js b/src/components/ArticleV2/ArticleCellPopup.js new file mode 100644 index 00000000..932b870d --- /dev/null +++ b/src/components/ArticleV2/ArticleCellPopup.js @@ -0,0 +1,20 @@ +import React from 'react' +import {a} from 'react-spring' +import { Link } from 'react-feather' +import '../../styles/components/Article/ArticleCellPopup.scss' + + +const ArticleCellPopup = ({ style, onClick }) => { + return ( + + + + + + + ) +} +export default ArticleCellPopup diff --git a/src/components/ArticleV2/ArticleFlow.js b/src/components/ArticleV2/ArticleFlow.js index b4464bc9..cce54233 100644 --- a/src/components/ArticleV2/ArticleFlow.js +++ b/src/components/ArticleV2/ArticleFlow.js @@ -1,5 +1,5 @@ import React from 'react' -import { LayerNarrative, LayerHermeneutics, LayerData, LayerHidden } from '../../constants' +import { LayerNarrative, LayerHermeneutics, LayerHidden } from '../../constants' import ArticleLayers from './ArticleLayers' import ArticleToC from './ArticleToC' import styles from './ArticleFlow.module.css' @@ -15,11 +15,12 @@ const ArticleFlow = ({ // onCellClick, // onVisibilityChange // hasBibliography, - // binderUrl, + onDataHrefClick, + binderUrl=null, // emailAddress, headingsPositions=[], tocOffset=99, - layers=[LayerNarrative, LayerHermeneutics, LayerData], + layers=[LayerNarrative, LayerHermeneutics], children }) => { const setVisibleCell = useArticleToCStore(store => store.setVisibleCell) @@ -32,7 +33,7 @@ const ArticleFlow = ({ if (cell.layer === LayerHidden) { return } - if (i > 0 && (cell.layer !== previousLayer || cell.isHeading)) { + if (i > 0 && (cell.layer !== previousLayer || cell.isHeading || cell.isFigure || cell.isTable )) { buffers.push([...buffer]) buffer = [] } @@ -50,7 +51,7 @@ const ArticleFlow = ({ console.debug('[ArticleFlow] @onPlaceholderClickHandler', e,cell) } const onCellIntersectionChangeHandler = ({ idx, isIntersecting }) => { - console.debug('[ArticleFlow] @onCellIntersectionChangeHandler', idx) + // console.debug('[ArticleFlow] @onCellIntersectionChangeHandler', idx) setVisibleCell(idx, isIntersecting) } console.debug(`[ArticleFlow] component rendered, size: ${width}x${height}px`) @@ -64,6 +65,7 @@ const ArticleFlow = ({ height: height - tocOffset }}> }, }) => { + const [popupProps, setPopupProps] = useSpring(() => ({ + x: 0, + y: 0, + opacity: 0, + cellIdx: -1, + cellLayer: '', + config: config.stiff + })) const [mask, setMask] = useSpring(() => ({ clipPath: [width, 0, width, height], x:0, y:0, config: config.slow @@ -56,40 +69,172 @@ const ArticleLayer = ({ console.warn('Not found! celleElment with given id:', selectedCellIdx) return } - console.debug('[ArticleLayer] useRefWithCallback:', selectedCellIdx, layer, 'selectedCellTop', selectedCellTop, cellElement.offsetTop, getCellAnchorFromIdx(selectedCellIdx, layer)) - layerDiv.scrollTo({ top: cellElement.offsetTop + layerDiv.offsetTop - selectedCellTop }) + // if the current layer height is greater than the height ref in the URL params, + // it means we can safely scroll to the selectedcellTop position displayed in the URL. + const cellElementRefTop = height >= selectedLayerHeight + ? selectedCellTop + : selectedLayerHeight/2 + const top = cellElement.offsetTop + layerDiv.offsetTop - cellElementRefTop + console.debug( + '[ArticleLayer] useRefWithCallback', + '\n selectedCellIdx:', selectedCellIdx, + '\n layer', layer, + '\n scrollTo:', top + ) + layerDiv.scrollTo({ top, behavior: previousLayer === selectedLayer ? 'smooth': 'instant' }) }) const onCellPlaceholderClickHandler = (e, cell) => { if (typeof onCellPlaceholderClick === 'function') { - onCellPlaceholderClick(e, { + const wrapper = e.currentTarget.closest('.ArticleLayer_placeholderWrapper') + onAnchorClick(e, { layer: cell.layer, idx: cell.idx, + previousIdx: cell.idx, + previousLayer: layer, height, // ref height - y: e.currentTarget.parentNode.parentNode.getBoundingClientRect().y + y: wrapper.offsetTop - wrapper.parentNode.scrollTop - 15 }) } else { console.warn('[ArticleLayer] misses a onCellPlaceholderClick listener') } } + /** + * method onSelectedCellClickHandler + * This method send user back to the placeholder who generated the link, if any. + */ const onSelectedCellClickHandler = (e, cell) => { if (typeof onCellPlaceholderClick === 'function') { onCellPlaceholderClick(e, { layer: previousLayer, - idx: cell.idx, + idx: previousCellIdx > -1 ? previousCellIdx : cell.idx, height, // ref height - y: e.currentTarget.parentNode.getBoundingClientRect().y + y: previousCellIdx > -1 ? selectedCellTop : e.currentTarget.parentNode.parentNode.offsetTop - e.currentTarget.parentNode.parentNode.parentNode.scrollTop - 15 + }) + } + } + + /** + * method onNumClickHandler + * update the Animatable properties of ArticleCellPopup component. We add there also + * the current cell idx and cell layer (yes, it should be better placed in a ref @todo) + */ + const onNumClickHandler = (e, cell) => { + const wrapper = e.currentTarget.closest('.ArticleLayer_paragraphWrapper') + setPopupProps.start({ + from: { + x: e.currentTarget.parentNode.offsetLeft, + y: wrapper.offsetTop, + opacity:.6, + cellIdx: cell.idx, + cellLayer: cell.layer, + }, + to:{ + x: e.currentTarget.parentNode.offsetLeft, + y: wrapper.offsetTop - 10, + opacity:1, + cellIdx: cell.idx, + cellLayer: cell.layer + } + }) + onAnchorClick(e, { + layer: cell.layer, + idx: cell.idx, + previousLayer: cell.layer, + previousIdx: cell.idx, + height, // ref height + y: wrapper.offsetTop - wrapper.parentNode.scrollTop - 15 + }) + } + + const onCellPlaceholderNumClickHandler = (e, cell) => { + const wrapper = e.currentTarget.closest('.ArticleLayer_placeholderWrapper') + + console.debug('[ArticleLayer] @onCellPlaceholderNumClickHandler', cell) + onAnchorClick(e, { + idx: cell.idx, + layer: cell.layer, + height, // ref height + y: wrapper.offsetTop - wrapper.parentNode.scrollTop - 15, + previousLayer: selectedLayer, + previousIdx: cell.idx + }) + } + + const onCellPopupClickHandler = (e, cell) => { + console.debug('[ArticleLayer] @onCellPopupClickHandler', cell) + onCellPlaceholderClick(e, { + layer: cell.layer, + idx: cell.idx, + height, // ref height + y: 100 + }) + } + + const onLayerClickHandler = (e) => { + // console.debug('[ArticleLayer] @onLayerClickHandler', e) + // // eslint-disable-next-line + // debugger + // check if the user clicked on an anchor element (figure, table of simple anchor) + // in the markdown cell content. + if (e.target.nodeName === 'A') { + if (e.target.hasAttribute('data-href')) { + // link to bibliography :) + const dataHref = e.target.getAttribute('data-href') + onDataHrefClick({ dataHref }) + } else if (e.target.hasAttribute('data-idx')) { + e.preventDefault() + const targetCellIdx = parseInt(e.target.getAttribute('data-idx'), 10) + const targetCell = paragraphs.find(p => p.idx === targetCellIdx) + const cellElement = document.getElementById(getCellAnchorFromIdx(targetCellIdx, targetCell.layer)) + if (!cellElement) { + console.warn('Not found! celleElment with given id:', selectedCellIdx) + return + } + // get cell idx where the event was generated. + const wrapper = e.target.closest('.ArticleLayer_paragraphWrapper') + if (!wrapper){ + // nothing to do :( + console.warn('ArticleLayer_paragraphWrapper Not found! Element is maybe a placeholder.', selectedCellIdx) + return + } + const sourceCellidx = parseInt(wrapper.getAttribute('data-cell-idx'), 10) + const sourceCellLayer = wrapper.getAttribute('data-cell-layer') + console.debug( + '[ArticleLayer] @onLayerClickHandler:', + '\n - target:', targetCellIdx, targetCell.layer, + '\n - source:', sourceCellidx, sourceCellLayer + ) + onAnchorClick(e, { + layer: targetCell.layer, + idx: targetCell.idx, + previousIdx: sourceCellidx, + previousLayer: sourceCellLayer, + height, // ref height + y: wrapper.offsetTop - wrapper.parentNode.scrollTop - 15 + }) + } + } else { + onDataHrefClick({}) + } + + if (!e.target.classList.contains('ArticleCellContent_num')) { + setPopupProps.start({ + opacity: 0 }) } } useEffect(() => { - if (layers.indexOf(layer) <= layers.indexOf(selectedLayer)) { - console.info('open', layer, layers.indexOf(selectedLayer), layers.indexOf(layer)) + const layerLevel = layers.indexOf(layer) + if (layerLevel === 0) { + setMask.set({ clipPath: [0, 0, width, height], x:-width, y:0 }) + } else if (layerLevel <= layers.indexOf(selectedLayer)) { + console.info('open', layer, layers.indexOf(selectedLayer), layerLevel) setMask.start({ clipPath: [0, 0, width, height], x:-width, y:0 }) - } else if (layers.indexOf(layer) > layers.indexOf(selectedLayer)) { - console.info('close', layer, layers.indexOf(selectedLayer), layers.indexOf(layer)) + } else if (layerLevel > layers.indexOf(selectedLayer)) { + console.info('close', layer, layers.indexOf(selectedLayer), layerLevel) setMask.start({ clipPath: [width, 0, width, height], x:0, y:0 }) } }, [isSelected, layer, layers]) @@ -100,10 +245,12 @@ const ArticleLayer = ({ + }} onClick={onLayerClickHandler}>
+ {children} + {paragraphsGroups.map((paragraphsIndices, i) => { const firstCellInGroup = paragraphs[paragraphsIndices[0]] const isPlaceholder = firstCellInGroup.layer !== layer @@ -113,25 +260,26 @@ const ArticleLayer = ({ {paragraphsIndices.map((k) => (
))} -
+
+
{paragraphsIndices.slice(0,2).map((j) => ( paragraphs[d].num)} /> ))}
-
- -
+
) : null} - {/* debug && selectedCellIdx === cell.idx && previousLayer ? ( -
- -
- ):null */} - {/* debug && selectedCellIdx === cell.idx && nextLayer ? ( -
- -
- ):null */} +
) })} diff --git a/src/components/ArticleV2/ArticleLayer.module.css b/src/components/ArticleV2/ArticleLayer.module.css index f5424f09..9a6cc39b 100644 --- a/src/components/ArticleV2/ArticleLayer.module.css +++ b/src/components/ArticleV2/ArticleLayer.module.css @@ -43,10 +43,13 @@ .anchor{ position: relative; + height: 0; + overflow: hidden; } .placeholder{ max-height: 200px; overflow: hidden; + position: relative; } .placeholderGradient{ position: absolute; @@ -91,7 +94,7 @@ } .placeholderGradient_hermeneutics_narrative{ composes: placeholderGradient; - background: linear-gradient(180deg, #B1FBEC99 0%, #B1FBECff 90%); + background: linear-gradient(180deg, #B1FBEC00 0%, #B1FBECff 50%); } .placeholderGradient_data_narrative{ composes: placeholderGradient; @@ -105,31 +108,34 @@ .placeholderActive { position: absolute; - left: var(--spacer-2); + left: 0; background: transparent; height: 100%; - right: 20%; + right: 16%; z-index: -1; top: 0; } -/* .placeholderActive:after{ +.placeholderActive:after{ content: ''; position: absolute; - top: 5px; - left: 5px; - width: 2px; + top: 0px; + left: 10px; + width: 3px; background: var(--dark); - bottom: 5px; -} */ + bottom: 0px; + z-index: 1000; +} .placeholderActive_narrative_on, .placeholderActive_hermeneutics_on, -.placeholderActive_data_on, .cellActive_on{ - border-top: 1px solid; - /* border-bottom: 1px double; */ +.placeholderActive_data_on, +.cellActive_on{ + /* border-top: 1px solid; */ composes: placeholderActive; /* background: #ffffff77; */ } -.placeholderActive_off, .cellActive_off{ +.placeholderActive_narrative_off, +.placeholderActive_hermeneutics_off, +.placeholderActive_data_off, .cellActive_off{ composes: placeholderActive; background: transparent; opacity: 0; @@ -138,5 +144,11 @@ position: absolute; left: 25px; top: var(--spacer-2); + /* border-radius: 20px; */ /* border-radius: 100%; */ } + +.paragraphWrapper{ + position: relative; + overflow: auto; +} diff --git a/src/components/ArticleV2/ArticleLayers.js b/src/components/ArticleV2/ArticleLayers.js index 70fb335f..4eb17e26 100644 --- a/src/components/ArticleV2/ArticleLayers.js +++ b/src/components/ArticleV2/ArticleLayers.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import ArticleLayer from './ArticleLayer' import { useQueryParams, StringParam, NumberParam, withDefault, } from 'use-query-params' import { @@ -6,9 +6,11 @@ import { LayerNarrative, DisplayLayerCellIdxQueryParam, DisplayLayerCellTopQueryParam, - DisplayPreviousLayerQueryParam + DisplayPreviousLayerQueryParam, + DisplayPreviousCellIdxQueryParam, + DisplayLayerHeightQueryParam } from '../../constants' - +import { useArticleToCStore } from '../../store' const ArticleLayers = ({ memoid='', @@ -19,8 +21,10 @@ const ArticleLayers = ({ height=0, onCellPlaceholderClick, onCellIntersectionChange, + onDataHrefClick, children, }) => { + const clearVisibleCellsIdx = useArticleToCStore(store => store.clearVisibleCellsIdx) // Store indicies as a local ref, this represents the item order [0,1,2] // const order = React.useRef(layers.map((_, index) => index)) const [{ @@ -28,16 +32,20 @@ const ArticleLayers = ({ [DisplayLayerCellIdxQueryParam]:selectedCellIdx, [DisplayLayerCellTopQueryParam]: selectedCellTop, [DisplayPreviousLayerQueryParam]: previousLayer, + [DisplayPreviousCellIdxQueryParam]: previousCellIdx, + [DisplayLayerHeightQueryParam]: layerHeight, }, setQuery] = useQueryParams({ [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1), [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative), [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 0), [DisplayPreviousLayerQueryParam]: StringParam, + [DisplayPreviousCellIdxQueryParam]: withDefault(NumberParam, -1), + [DisplayLayerHeightQueryParam]: withDefault(NumberParam, -1), }) // const [springs, api] = useSprings(layers.length, fn(order.current)) // Create springs, each corresponds to an item, controlling its transform, scale, etc. - const onCellPlaceholderClickHandler = (e, { layer, idx, y }) => { + const onCellPlaceholderClickHandler = (e, { layer, idx, y, height:h }) => { console.debug('[ArticleLayers] @onCellPlaceholderClickHandler:', layer, idx, y) // replaceIn not to trgger the changes. This is helpful whenever the user // hits the back Button in the browser (or uses the swipe left on mobile) @@ -46,6 +54,7 @@ const ArticleLayers = ({ [DisplayLayerCellIdxQueryParam]: idx, [DisplayLayerCellTopQueryParam]: y, [DisplayPreviousLayerQueryParam]: previousLayer, + [DisplayLayerHeightQueryParam]: layerHeight }, 'replaceIn') // this query setQuery({ @@ -53,11 +62,34 @@ const ArticleLayers = ({ [DisplayLayerCellIdxQueryParam]: idx, [DisplayLayerCellTopQueryParam]: y, [DisplayPreviousLayerQueryParam]: selectedLayer, + [DisplayLayerHeightQueryParam]: h }) if (typeof onCellPlaceholderClick === 'function') { onCellPlaceholderClick(e, { layer, idx, y }) } } + + const onAnchorClickHandler = (e, { layer, idx, previousIdx, previousLayer, y, height:h }) => { + console.debug( + '[ArticleLayers] @onAnchorClickHandler:', + '\n - target:', idx, layer, + '\n - source:', previousIdx, previousLayer + ) + // this query + setQuery({ + [DisplayLayerQueryParam]: layer, + [DisplayLayerCellIdxQueryParam]: idx, + [DisplayLayerCellTopQueryParam]: y, + [DisplayPreviousLayerQueryParam]: previousLayer, + [DisplayPreviousCellIdxQueryParam]: previousIdx, + [DisplayLayerHeightQueryParam]: h + }) + } + + useEffect(() => { + console.debug('[ArticleLayers] @useEffect layer changed to:', selectedLayer) + clearVisibleCellsIdx() + }, [selectedLayer]) console.debug('[ArticleLayers] rendered, selected:', selectedLayer) return ( <> @@ -71,12 +103,16 @@ const ArticleLayers = ({ previousLayerIdx={i > 0 ? layers[i-1] : null} nextLayer={i < layers.length - 1 ? layers[i+1]: null} onCellPlaceholderClick={onCellPlaceholderClickHandler} + onDataHrefClick={onDataHrefClick} + onAnchorClick={onAnchorClickHandler} onCellIntersectionChange={onCellIntersectionChange} selectedCellIdx={selectedCellIdx} selectedCellTop={selectedCellTop} + selectedLayerHeight={layerHeight} isSelected={selectedLayer === layer} selectedLayer={selectedLayer} previousLayer={previousLayer} + previousCellIdx={previousCellIdx} height={height} width={width} layers={layers} diff --git a/src/components/ArticleV2/ArticleToC.js b/src/components/ArticleV2/ArticleToC.js index 30e0cfb5..56ad10f5 100644 --- a/src/components/ArticleV2/ArticleToC.js +++ b/src/components/ArticleV2/ArticleToC.js @@ -1,7 +1,6 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { useQueryParams, withDefault, NumberParam, StringParam } from 'use-query-params' -import { Bookmark } from 'react-feather' -import { Button } from 'react-bootstrap' import { useArticleToCStore } from '../../store' import { LayerHermeneutics, @@ -12,15 +11,17 @@ import { DisplayPreviousLayerQueryParam, } from '../../constants' import ArticleToCStep from './ArticleToCStep' - +import ArticleToCBookmark from './ArticleToCBookmark' const ArticleToC = ({ memoid='', layers=[], paragraphs=[], headingsPositions=[], + binderUrl=null, width=100 }) => { + const { t } = useTranslation() const visibleCellsIdx = useArticleToCStore(state=>state.visibleCellsIdx) const [{ @@ -96,7 +97,7 @@ const ArticleToC = ({ previousHeadingIdx, nextHeadingIdx } - }, [ firstVisibleCellIdx ]) + }, [ firstVisibleCellIdx, lastVisibleCellIdx ]) const onStepClickHandler = (step) => { console.debug('[ArticleToC] @onClickHandler step:', step, selectedLayer) @@ -110,19 +111,25 @@ const ArticleToC = ({ const onBookmarkClickHandler = () => { setQuery({ [DisplayLayerCellIdxQueryParam]: selectedCellIdx, - [DisplayPreviousLayerQueryParam]: undefined + [DisplayPreviousLayerQueryParam]: undefined, + [DisplayLayerCellTopQueryParam]: 100 }) } return ( <>
{layers.map((d,i) => ( -
setQuery({ - [DisplayLayerQueryParam]: d - })}>{selectedLayer === d ? {d}: d}
+
setQuery({[DisplayLayerQueryParam]: d})} + >{d}
))} +

-
+
{steps.map((step, i) => { const showBookmark = i < steps.length - 1 ? selectedCellIdx >= step.cell.idx && selectedCellIdx < steps[i+1].cell.idx @@ -130,19 +137,14 @@ const ArticleToC = ({ return (
{showBookmark ? ( - + /> ): null} { + return( +
+
{children ? children : }
+
+ ) +} + +export default ArticleToCBookmark diff --git a/src/components/Forms/FormNotebookUrl.js b/src/components/Forms/FormNotebookUrl.js index e8dfee62..9eec6a0b 100644 --- a/src/components/Forms/FormNotebookUrl.js +++ b/src/components/Forms/FormNotebookUrl.js @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Form , Button} from 'react-bootstrap' -import { Link2 } from 'react-feather' +import { Cpu } from 'react-feather' const FormNotebookUrl = ({initialValue = '', onSubmit}) => { const { t } = useTranslation() @@ -58,15 +58,15 @@ const FormNotebookUrl = ({initialValue = '', onSubmit}) => { diff --git a/src/constants.js b/src/constants.js index de430a26..414bd6fe 100644 --- a/src/constants.js +++ b/src/constants.js @@ -121,6 +121,7 @@ export const FigureRefPrefix = 'figure-' export const CoverRefPrefix = 'cover' export const TableRefPrefix = 'table-' export const QuoteRefPrefix = 'quote-' +export const AnchorRefPrefix = 'anchor-' // display Layer to enable switch between layers export const DisplayLayerHermeneutics = 'h' export const DisplayLayerNarrative = 'n' @@ -128,7 +129,9 @@ export const DisplayLayerAll = 'all' export const DisplayLayerQueryParam = 'layer' export const DisplayPreviousLayerQueryParam = 'pl' export const DisplayLayerCellIdxQueryParam = 'idx' +export const DisplayPreviousCellIdxQueryParam = 'pidx' export const DisplayLayerCellTopQueryParam = 'y' +export const DisplayLayerHeightQueryParam = 'lh' // article status export const ArticleStatusPublished = 'PUBLISHED' export const ArticleStatusDraft = 'Draft' diff --git a/src/logic/ipynb.js b/src/logic/ipynb.js index 38db4d0e..3f429975 100644 --- a/src/logic/ipynb.js +++ b/src/logic/ipynb.js @@ -9,6 +9,7 @@ import ArticleCell from '../models/ArticleCell' // import ArticleCellGroup from '../models/ArticleCellGroup' import ArticleReference from '../models/ArticleReference' import ArticleFigure from '../models/ArticleFigure' +import ArticleAnchor from '../models/ArticleAnchor' import { SectionChoices, SectionDefault, LayerChoices, LayerNarrative, // LayerHermeneuticsStep, @@ -17,7 +18,8 @@ import { FigureRefPrefix, TableRefPrefix, CoverRefPrefix, - QuoteRefPrefix + QuoteRefPrefix, + AnchorRefPrefix } from '../constants' const encodeNotebookURL = (url) => btoa(encodeURIComponent(url)) @@ -41,16 +43,20 @@ const renderMarkdownWithReferences = ({ sources = '', referenceIndex = {}, citationsFromMetadata = {}, - figures + figures = [], + anchors = [] }) => { const references = [] // console.info('markdownParser.render', markdownParser.render(sources)) const content = markdownParser.render(sources) + .replace(/<a[^&]*>(.*)<\/a>/g, '') // replace links "figure-" add data-idx attribute containing a figure id - .replace(//g, (m, anchorRef) => { - const ref = figures.find(d => d.ref === anchorRef) + .replace(//g, (m, anchorRef) => { + const ref = anchorRef.indexOf('anchor-') !== -1 + ? anchors.find(d => d.ref === anchorRef) + : figures.find(d => d.ref === anchorRef) if (ref) { - return `` + return `` } return `` }) @@ -150,6 +156,7 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { const figures = [] const articleCells = [] const articleParagraphs = [] + const anchors = [] const sectionsIndex = {} let citationsFromMetadata = metadata?.cite2c?.citations @@ -194,6 +201,7 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { const figureRef = cell.metadata.tags?.find(d => d.indexOf(FigureRefPrefix) === 0) const tableRef = cell.metadata.tags?.find(d => d.indexOf(TableRefPrefix) === 0) const quoteRef = cell.metadata.tags?.find(d => d.indexOf(QuoteRefPrefix) === 0) + const anchorRef = cell.metadata.tags?.find(d => d.indexOf(AnchorRefPrefix) === 0) // get section and layer from metadata cell.section = getSectionFromCellMetadata(cell.metadata) cell.layer = getLayerFromCellMetadata(cell.metadata) @@ -248,6 +256,9 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { } else if (cell.section !== SectionDefault) { cell.role = RoleMetadata } + if (anchorRef) { + anchors.push(new ArticleAnchor({ ref: anchorRef, idx })) + } cell.source = Array.isArray(cell.source) ? cell.source : [cell.source] @@ -270,6 +281,7 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { referenceIndex, citationsFromMetadata, figures, + anchors }) // get tokens 'heading_open' to get all h1,h2,h3 etc... const headerIdx = tokens.findIndex(t => t.type === 'heading_open'); @@ -372,7 +384,7 @@ const getArticleTreeFromIpynb = ({ id, cells=[], metadata={} }) => { paragraphs: articleParagraphs, paragraphsPositions: articleParagraphs.map(d => d.idx), sections: sectionsIndex, - bibliography, figures, + bibliography, figures, anchors, citationsFromMetadata }) } diff --git a/src/models/ArticleAnchor.js b/src/models/ArticleAnchor.js new file mode 100644 index 00000000..3808cc02 --- /dev/null +++ b/src/models/ArticleAnchor.js @@ -0,0 +1,11 @@ +export default class ArticleAnchor { + constructor({ + ref = 'anchor-', // 'anchor-something nice' identifier. it must start with constants/AnchorRefPrefix + idx = -1, + type = 'anchor' + }) { + this.ref = ref + this.idx = idx + this.type = type + } +} diff --git a/src/models/ArticleTree.js b/src/models/ArticleTree.js index e0ba789d..1b428cc9 100644 --- a/src/models/ArticleTree.js +++ b/src/models/ArticleTree.js @@ -5,6 +5,7 @@ export default class ArticleTree { paragraphs = [], sections = {}, figures = [], + anchors = [], cells = [], headingsPositions = [], paragraphsPositions = [], @@ -15,6 +16,7 @@ export default class ArticleTree { this.headings= headings this.paragraphs = paragraphs this.figures = figures + this.anchors = anchors this.cells = cells this.sections = sections this.headingsPositions = headingsPositions diff --git a/src/pages/NotebookViewer.js b/src/pages/NotebookViewer.js index de724916..d3fd3e86 100644 --- a/src/pages/NotebookViewer.js +++ b/src/pages/NotebookViewer.js @@ -30,7 +30,7 @@ const NotebookViewer = ({ pid, }) => { const [{[ArticleVersionQueryParam]: version}] = useQueryParams({ - [ArticleVersionQueryParam]: withDefault(NumberParam, 0) + [ArticleVersionQueryParam]: withDefault(NumberParam, 2) }) const ArticleComponent = version === 2 ? ArticleV2 : Article const [animatedProps, api] = useSpring(() => ({ width : 0, opacity:1, config: config.slow })) diff --git a/src/store.js b/src/store.js index 132bb619..57db362f 100644 --- a/src/store.js +++ b/src/store.js @@ -10,6 +10,7 @@ export const useIssueStore = create((set) => ({ export const useArticleToCStore = create((set) => ({ visibleCellsIdx: [], + clearVisibleCellsIdx: () => set(() => ({ visibleCellsIdx: [] })), setVisibleCell: (cellIdx, isVisible) => set((state) => { const copy = [...state.visibleCellsIdx] const idx = copy.indexOf(cellIdx) @@ -19,7 +20,7 @@ export const useArticleToCStore = create((set) => ({ copy.splice(idx, 1) } copy.sort((a,b)=> a - b) - console.debug('[useArticleToCStore] visibleCellsIdx:', copy) + // console.debug('[useArticleToCStore] visibleCellsIdx:', copy) return { visibleCellsIdx: copy } }) })) diff --git a/src/styles/article.scss b/src/styles/article.scss index 730d38a9..14b2ba0c 100644 --- a/src/styles/article.scss +++ b/src/styles/article.scss @@ -35,6 +35,10 @@ $breakpoint-lg: 992px; &.level_H3, &.level_H4, &.level_H5, &.level_H6{ top: 20px; } + &.selectable{ + cursor: pointer; + text-decoration: underline; + } } .ArticleCellContent_idx { @@ -645,6 +649,17 @@ svg.ArticleFingerprint{ color: var(--primary-dark); } +.ArticleToC_layerSelector{ + cursor: pointer; + display: inline-block; + padding: 0 var(--spacer-2); + border-radius: var(--spacer-1); + &.active{ + background: var(--dark); + color: var(--white); + } +} + .ArticleCellAccordion{ // border-top: 1px solid var(--dark); color: var(--primary-dark); diff --git a/src/styles/components/Article/ArticleCellPopup.scss b/src/styles/components/Article/ArticleCellPopup.scss new file mode 100644 index 00000000..f7c90135 --- /dev/null +++ b/src/styles/components/Article/ArticleCellPopup.scss @@ -0,0 +1,33 @@ +.ArticleCellPopup { + position: absolute; + left: var(--negative-spacer-5); + background-color: var(--dark); + color: var(--white); + padding: 0 var(--spacer-2); + font-size: 12px; + line-height: 25px; + font-weight: bold; + top: 0px; + z-index: 1000; + + label:hover{ + cursor: pointer; + text-decoration: underline; + } + &::after{ + content: ''; + position: absolute; + left: var(--spacer-3); + top: 100%; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid var(--dark); + clear: both; + } + + line, path{ + stroke-width: 2.5px; + } +} From bbe7d3ca09b000ba2b517d7b6feb21a0a30a65c8 Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Wed, 5 Jan 2022 10:45:42 +0100 Subject: [PATCH 03/13] Feature/fingerprint for all (#293) * add new route * Create FingerprintViewer.js * Update translations.json * add mouseMove on articleFingerprint which selects closest "Datum" * move IssueArticlesGrid in its own file * forward onMouseMove event * add hover line * update Fingerprint viewer page * add DisplayLayerCellRefHeight to control the height of the available window * add onclick method on ArticleFingerprint * add mouseout and mouseclick event on the IssuArticlesGrid to link to the exact cell * set initial opacity to 0 for our tooltip * lock body scrolling before loading api in ArticleViewer page * add initial animatable props for issue tooltip in IssueArticlesGrid component * fix safari scrolling (pointerEvents to none whn layer is not selected) --- src/App.js | 3 + src/components/Article/ArticleFingerprint.js | 69 ++++++- src/components/ArticleV2/Article.js | 9 +- src/components/ArticleV2/ArticleLayer.js | 4 + src/components/ArticleV2/ArticleLayers.js | 1 + src/components/Issue/IssueArticleGridItem.js | 32 ++- src/components/Issue/IssueArticlesGrid.js | 113 +++++++++++ src/logic/fingerprint.js | 49 +++++ src/pages/ArticleViewer.js | 9 +- src/pages/ErrorViewer.js | 6 +- src/pages/FingerprintViewer.js | 183 ++++++++++++++++++ src/pages/Issue.js | 39 +--- src/pages/NotFound.js | 4 +- src/styles/article.scss | 19 ++ .../components/IssueArticleGridItem.scss | 4 +- src/translations.json | 7 + 16 files changed, 485 insertions(+), 66 deletions(-) create mode 100644 src/components/Issue/IssueArticlesGrid.js create mode 100644 src/logic/fingerprint.js create mode 100644 src/pages/FingerprintViewer.js diff --git a/src/App.js b/src/App.js index 316cd081..bfd1c7bf 100644 --- a/src/App.js +++ b/src/App.js @@ -67,6 +67,8 @@ const Guidelines = lazy(() => import('./pages/Guidelines')) const NotFound = lazy(() => import('./pages/NotFound')) const ArticleViewer = lazy(() => import('./pages/ArticleViewer')) const Fingerprint = lazy(() => import('./pages/Fingerprint')) +const FingerprintViewer = lazy(() => import('./pages/FingerprintViewer')) + const { startLangShort, lang } = getStartLang() console.info('start language:', lang, startLangShort) i18n @@ -123,6 +125,7 @@ function LangRoutes() { + diff --git a/src/components/Article/ArticleFingerprint.js b/src/components/Article/ArticleFingerprint.js index 0f7faa41..6564f380 100644 --- a/src/components/Article/ArticleFingerprint.js +++ b/src/components/Article/ArticleFingerprint.js @@ -1,8 +1,8 @@ -import React from 'react' +import React, { useRef } from 'react' import { scalePow, } from 'd3-scale' import { extent } from 'd3-array' import ArticleFingerprintCellGraphics from './ArticleFingerprintCellGraphics' - +import { animated, useSpring, config} from 'react-spring' const ArticleFingerprint = ({ stats={}, @@ -13,8 +13,15 @@ const ArticleFingerprint = ({ // this space is needed for the text elements margin=20, // - variant='' + variant='', + onMouseMove, + onClick, + onMouseOut, }) => { + // reference value for the clicked idx + const cached = useRef({ idx: -1}) + // animated properties for current selected "datum" + const [pointer, api] = useSpring(() => ({ theta : 0, idx:0, opacity: 0, config: config.stiff })) const radius = (size/2 - margin) * 2 / 3 // value radius, this give us extra safety margin. const maxNumCharsRadius = radius / 2 @@ -36,6 +43,44 @@ const ArticleFingerprint = ({ .domain(stats?.extentRefs || [0,1]) .range([1.5, maxNumRefsRadius]) + const scaleTheta = scalePow().exponent(1).domain([0, 360]).range([0, cells.length + 1]) + + + + const mouseMoveHander = (e) => { + if (typeof onMouseMove !== 'function') { + return + } + // get cell based on mouse position + const x = e.nativeEvent.offsetX - size/2 + const y = e.nativeEvent.offsetY - size/2 + // get angle and relative cell. + const radians = Math.atan2(y, x) + Math.PI / 2 // correction + const absRadians = radians < 0 ? radians + Math.PI * 2 : radians + const theta = absRadians * 180 / Math.PI + const datumIdx = Math.round(scaleTheta(theta)) + const idx = cells[datumIdx] ? datumIdx : 0 + const datum = cells[idx] + // save idx + cached.current.idx = idx + api.start({ theta: theta - 90, idx, opacity: 1 }) + onMouseMove(e, datum, cells[datumIdx] ? datumIdx : 0 ) + } + + const mouseOutHandler = () => { + api.start({ opacity: 0 }) + if (typeof onMouseOut === 'function') { + onMouseOut() + } + } + + const onClickHandler = (e) => { + if (typeof onClick === 'function' && cached.current.idx !== -1) { + const datum = cells[cached.current.idx] + onClick(e, datum, cached.current.idx) + } + } + if (cells.length===0) { return null } @@ -52,6 +97,8 @@ const ArticleFingerprint = ({ xmlns="http://www.w3.org/2000/svg" width={size} height={size} + onMouseMove={mouseMoveHander} + onClick={onClickHandler} > {debug // outer circle (narrative layer) @@ -97,6 +144,22 @@ const ArticleFingerprint = ({ ) })} + + + {/* + o)} + transform={pointer.theta.to((t) => `rotate(${t})`)} + /> + */} + o)} + transform={pointer.idx.to((idx) => { + // const t = parseInt(idx, 10) + return `rotate(${scaleTheta.invert(idx) - 90})` + })} + /> + ) } diff --git a/src/components/ArticleV2/Article.js b/src/components/ArticleV2/Article.js index 86a7c0e4..f5a6795d 100644 --- a/src/components/ArticleV2/Article.js +++ b/src/components/ArticleV2/Article.js @@ -1,11 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useIpynbNotebookParagraphs } from '../../hooks/ipynb' import { useCurrentWindowDimensions } from '../../hooks/graphics' import ArticleHeader from '../Article/ArticleHeader' import ArticleNote from '../Article/ArticleNote' import ArticleFlow from './ArticleFlow' -import { setBodyNoScroll } from '../../logic/viewport' const Article = ({ // pid, @@ -46,12 +45,6 @@ const Article = ({ console.debug('DataHref click handler') setSelectedDataHref(d) } - useEffect(() => { - setBodyNoScroll(true) - return function() { - setBodyNoScroll(false) - } - },[]) return ( <> diff --git a/src/components/ArticleV2/ArticleLayer.js b/src/components/ArticleV2/ArticleLayer.js index 39d39aa7..af873827 100644 --- a/src/components/ArticleV2/ArticleLayer.js +++ b/src/components/ArticleV2/ArticleLayer.js @@ -105,6 +105,10 @@ const ArticleLayer = ({ * This method send user back to the placeholder who generated the link, if any. */ const onSelectedCellClickHandler = (e, cell) => { + // console.info('@onCellPlaceholderClickHandler', e, cell) + // // eslint-disable-next-line + // debugger + if (typeof onCellPlaceholderClick === 'function') { onCellPlaceholderClick(e, { layer: previousLayer, diff --git a/src/components/ArticleV2/ArticleLayers.js b/src/components/ArticleV2/ArticleLayers.js index 4eb17e26..20c622a9 100644 --- a/src/components/ArticleV2/ArticleLayers.js +++ b/src/components/ArticleV2/ArticleLayers.js @@ -122,6 +122,7 @@ const ArticleLayers = ({ top: 0, overflow: selectedLayer === layer ? "scroll": "hidden", zIndex: i, + pointerEvents: selectedLayer === layer ? "auto": "none", // left: i * width }} > diff --git a/src/components/Issue/IssueArticleGridItem.js b/src/components/Issue/IssueArticleGridItem.js index db451757..c6eecf08 100644 --- a/src/components/Issue/IssueArticleGridItem.js +++ b/src/components/Issue/IssueArticleGridItem.js @@ -11,21 +11,33 @@ import { IsMobile } from '../../constants' import '../../styles/components/IssueArticleGridItem.scss' -const IssueArticleGridItem = ({ article={}, isFake=false, num=0, isEditorial }) => { +const IssueArticleGridItem = ({ + article={}, isFake=false, num=0, isEditorial, + onMouseMove, + onMouseOut, + onClick, + }) => { const [{width: size }, ref] = useBoundingClientRect() const { title, keywords, excerpt, contributor } = extractMetadataFromArticle(article) const { t } = useTranslation() return ( -
- -
- +
+
+
- {isEditorial ? {t('editorial')}: num}

diff --git a/src/components/Issue/IssueArticlesGrid.js b/src/components/Issue/IssueArticlesGrid.js new file mode 100644 index 00000000..a4a49476 --- /dev/null +++ b/src/components/Issue/IssueArticlesGrid.js @@ -0,0 +1,113 @@ +import React, { useRef } from 'react' +import { Row, Col } from 'react-bootstrap' +import { useHistory } from 'react-router' +import { useGetJSON } from '../../logic/api/fetchData' +import IssueArticleGridItem from './IssueArticleGridItem' +import { StatusSuccess, DisplayLayerCellIdxQueryParam } from '../../constants' +import { a, useSpring, config} from 'react-spring' +import {useBoundingClientRect} from '../../hooks/graphics' + + +const IssueArticlesGrid = ({ issue, onError }) => { + const [{ left }, ref] = useBoundingClientRect() + const history = useHistory() + const [animatedTooltipProps, tooltipApi] = useSpring(() => ({ + x : 0, y: 0, opacity:0, + color: 'var(--white)', + backgroundColor: 'var(--secondary)', + config: config.stiff + })) + const tooltipText = useRef({ idx: '', text: '', heading: ''}); + // console.info('IssueArticlesGrid', articles) + const { data, error, status, errorCode } = useGetJSON({ + url: `/api/articles/?pid=${issue.pid}` + }) + if (typeof onError === 'function' && error) { + console.error('IssueArticlesGrid loading error:', errorCode, error) + } + if (status !== StatusSuccess ) { + return null + } + + const editorials = [] + const articles = [] + + for (let i=0,j=data.length; i < j; i++) { + if (data[i].tags.some((t) => t.name === process.env.REACT_APP_TAG_EDITORIAL)) { + editorials.push(data[i]) + } else { + articles.push(data[i]) + } + } + const onMouseMoveHandler = (e, datum, idx) => { + if( tooltipText.current.idx !== idx) { + tooltipText.current.text = datum.firstWords + tooltipText.current.idx = idx + tooltipText.current.heading = datum.firstWordsHeading || '?' + console.info('datum', datum) + } + tooltipApi.start({ + x: e.clientX - left +ref.current.parentNode.offsetLeft, + y: e.clientY - 50, + color: datum.type === 'code' + ? 'var(--white)' + : datum.isHermeneutic + ? 'var(--secondary)' + : 'var(--white)', + backgroundColor: datum.type === 'code' + ? 'var(--accent)' + : datum.isHermeneutic + ? 'var(--primary)' + : 'var(--secondary)', + opacity: 1 + }) + } + + const onClickHandler = (e, datum, idx, article) => { + e.stopPropagation() + console.info('clicked on me!', tooltipText.current?.idx, idx, article) + // link to specific cell in article + const url = `/en/article/${article.abstract.pid}?${DisplayLayerCellIdxQueryParam}=${idx}` + history.push(url); + } + const onMouseOutHandler = () => { + tooltipApi.start({ opacity: 0 }) + } + + return ( + <> + + {animatedTooltipProps.x.to(() => String(tooltipText.current.text))} + + + {editorials.map((article, i) => ( + + onClickHandler(e, datum, idx, article)} + onMouseOut={onMouseOutHandler} + onMouseMove={onMouseMoveHandler} + article={article} + isEditorial + /> + + ))} + {articles.map((article, i) => ( + + onClickHandler(e, datum, idx, article)} + onMouseOut={onMouseOutHandler} + onMouseMove={onMouseMoveHandler} + article={article} + num={i+1} + total={articles.length} + /> + + ))} + + + ) +} + +export default IssueArticlesGrid diff --git a/src/logic/fingerprint.js b/src/logic/fingerprint.js new file mode 100644 index 00000000..e5fa7cfe --- /dev/null +++ b/src/logic/fingerprint.js @@ -0,0 +1,49 @@ +import { markdownParser} from './ipynb' + + +export function parseNotebook({ cells=[] }={}) { + const dataciteRegexp = new RegExp('data-cite=[\'"][^\'"]+[\'"]','g'); + const stats = { + extentChars: [Infinity, -Infinity], + extentRefs: [Infinity, -Infinity] + } + const parsedCells = [] + cells.forEach((cell, i) => { + const c = {} + const tags = cell.metadata.tags ?? [] + const sources = cell.source.join('') + c.type = cell.cell_type + c.idx = i + c.countChars = sources.length + // does it contains a cite2c marker? + c.countRefs = [...sources.matchAll(dataciteRegexp)].length + + stats.extentChars = [ + Math.min(stats.extentChars[0], c.countChars), + Math.max(stats.extentChars[1], c.countChars) + ] + stats.extentRefs = [ + Math.min(stats.extentRefs[0], c.countRefs), + Math.max(stats.extentRefs[1], c.countRefs) + ] + c.isHeading = c.type === 'markdown' && !!sources.match(/^\s*#+\s/) + if (tags.includes('hidden')) { + // this should be hidden + return + } + c.isHermeneutic = tags.some(t => ['hermeneutics', 'hermeneutics-step'].indexOf(t) !== 0) + c.isFigure = tags.some(t => t.indexOf('figure-') !== -1) + c.isTable = tags.some(t => t.indexOf('table-') !== -1) + c.firstWords = c.type === 'markdown' + ? markdownParser.render(sources).replace(/<[^>]*>/g, '').split(/[\s\n,.]+/).slice(0, 10).concat(['...']).join(' ') + : cell.source.slice(0, 1).join(' ') + c.firstWordsHeading = [ + c.isHermeneutic ? 'Hermeneutics': 'Narrative', + c.type=="code" ? 'CODE' : null, + tags.find(t => t.indexOf('table-') !== -1 || t.indexOf('figure-') !== -1) + ].filter(d => d).join(' / ') + + parsedCells.push(c) + }) + return { stats, cells:parsedCells } +} diff --git a/src/pages/ArticleViewer.js b/src/pages/ArticleViewer.js index a70f006b..31c2d948 100644 --- a/src/pages/ArticleViewer.js +++ b/src/pages/ArticleViewer.js @@ -8,7 +8,7 @@ import ErrorViewer from './ErrorViewer' import NotebookViewer from './NotebookViewer' import { extractMetadataFromArticle } from '../logic/api/metadata' import { useIssueStore } from '../store' - +import { setBodyNoScroll } from '../logic/viewport' const ArticleViewer = ({ match: { params: { pid }}}) => { const { t } = useTranslation() @@ -18,6 +18,13 @@ const ArticleViewer = ({ match: { params: { pid }}}) => { delay: 1000 }) + useEffect(() => { + setBodyNoScroll(true) + return function() { + setBodyNoScroll(false) + } + },[]) + useEffect(() => { if (article && article.issue) { setIssue(article.issue) diff --git a/src/pages/ErrorViewer.js b/src/pages/ErrorViewer.js index 158e155a..3d6e09ed 100644 --- a/src/pages/ErrorViewer.js +++ b/src/pages/ErrorViewer.js @@ -6,7 +6,7 @@ import { BootstrapColumLayout } from '../constants' import hljs from 'highlight.js' // import hljs library -const ErrorViewer = ({ error={}, errorCode=404, language="python", children }) => { +const ErrorViewer = ({ error={}, errorCode=404, language="python", className='page', children }) => { const { t } = useTranslation() if (errorCode === 404) { return @@ -16,7 +16,7 @@ const ErrorViewer = ({ error={}, errorCode=404, language="python", children }) = ? hljs.highlight(language, cleanedError) : hljs.highlightAuto(cleanedError); return ( - +

{t('pages.errorViewer.title')} @@ -25,7 +25,7 @@ const ErrorViewer = ({ error={}, errorCode=404, language="python", children }) = {error.code === 'ECONNABORTED' ?

{error.message}

:

{t('pages.errorViewer.subheading')}

- + }

{ + const { t } = useTranslation() + const tooltipText = useRef({ idx: '', text: '', heading: ''}); + const [animatedProps, api] = useSpring(() => ({ width : 0, opacity:1, config: config.slow })) + const [animatedTooltipProps, tooltipApi] = useSpring(() => ({ + x : 0, y: 0, opacity:1, + backgroundColor: 'var(--secondary)', + color: 'var(--white)', + config: config.stiff + })) + const { data, error, status } = useGetJSON({ + url, + delay, + onDownloadProgress: (e) => { + console.debug('onDownloadProgress', e.total, e.loaded) + if (e.total && e.loaded) { + if (e.loaded < e.total) { + api.start({ width: 100 * e.loaded / e.total }) + } + } + } + }) + const [{ left, width:size }, ref] = useBoundingClientRect() + + const fingerprintData = status === StatusSuccess ? parseNotebook(data): null + + + + const onMouseMoveHandler = (e, datum) => { + if( tooltipText.current.idx !== datum.idx ) { + tooltipText.current.text = datum.firstWords + tooltipText.current.heading = datum.firstWordsHeading + } + tooltipApi.start({ + x: e.clientX - left +ref.current.parentNode.offsetLeft, + y: e.clientY - 50, + color: datum.type === 'code' + ? 'var(--white)' + : datum.isHermeneutic + ? 'var(--secondary)' + : 'var(--white)', + backgroundColor: datum.type === 'code' + ? 'var(--accent)' + : datum.isHermeneutic + ? 'var(--primary)' + : 'var(--secondary)', + opacity: 1 + }) + } + const onMouseOutHandler = () => { + tooltipApi.start({ opacity: 0 }) + } + + useEffect(() => { + if (status === StatusFetching) { + api.start({ width: 10, opacity: 1 }) + } else if(status === StatusSuccess) { + api.start({ width: 100, opacity: 0 }) + } + }, [status]) + + return ( + <> + + {animatedTooltipProps.x.to(() => String(tooltipText.current.heading))} +
+ {animatedTooltipProps.x.to(() => String(tooltipText.current.text))} +
+

+ `${x}%`), + opacity: animatedProps.opacity + }}/> + {animatedProps.width.to(x => `${Math.round(x * 10000) / 10000}%`)} +
+ {error ? : ( + + + +

{t('pages.fingerprintViewerForm.title')}

+ +
+ + +
+ {fingerprintData ? + + : null} +
+ +
+
+ )} + + ) +} +/** + * THis component displays the form where we can add the notebook + * url. On submit, it checks the validity of the url + * then forward the user to the fingerprint-viewer?ipynb= page + * (which is handled by another component `FingerprintViewer` + */ +const FingerprintViewer = () => { + const { t } = useTranslation() + const [value, setValue] = useState('') + const [ipynbUrl, setIpynbUrl] = useQueryParam('url', withDefault(StringParam, '')) + console.info('initial url', ipynbUrl) + const handleSubmit = () => { + console.info('@submit', value) + // is a vaild, full url + setIpynbUrl(value) + // eslint-disable-next-line + // debugger + // history.push({ + // pathname: generatePath("/:lang/fingerprint-viewer", { + // lang: i18n.language.split('-')[0] + // }), + // search: `?ipynb=${value}` + // }) + } + const isValidUrl = ipynbUrl.match(/^https?:\/\/[^ "]+$/) + + if (isValidUrl) { + return + } + return ( + + + +

{t('pages.fingerprintViewerForm.title')}

+
+ + {t('forms.fingerprintViewerForm.notebookUrl')} + setValue(e.target.value)} + type="url" + placeholder="https://" + /> + + + +
+ +
+
+ ) +} + +export default FingerprintViewer diff --git a/src/pages/Issue.js b/src/pages/Issue.js index 9fa84237..6177f5c3 100644 --- a/src/pages/Issue.js +++ b/src/pages/Issue.js @@ -3,48 +3,11 @@ import { useTranslation } from 'react-i18next' import { Container, Row, Col } from 'react-bootstrap' import { useGetJSON } from '../logic/api/fetchData' import { BootstrapColumLayout } from '../constants' -import IssueArticleGridItem from '../components/Issue/IssueArticleGridItem' +import IssueArticlesGrid from '../components/Issue/IssueArticlesGrid' import ErrorViewer from './ErrorViewer' import Loading from './Loading' import { StatusSuccess } from '../constants' -const IssueArticlesGrid = ({ issue, onError }) => { - // console.info('IssueArticlesGrid', articles) - const { data, error, status, errorCode } = useGetJSON({ - url: `/api/articles/?pid=${issue.pid}` - }) - if (typeof onError === 'function' && error) { - console.error('IssueArticlesGrid loading error:', errorCode, error) - } - if (status !== StatusSuccess ) { - return null - } - - const editorials = [] - const articles = [] - - for (let i=0,j=data.length; i < j; i++) { - if (data[i].tags.some((t) => t.name === process.env.REACT_APP_TAG_EDITORIAL)) { - editorials.push(data[i]) - } else { - articles.push(data[i]) - } - } - return ( - - {editorials.map((article, i) => ( - - - - ))} - {articles.map((article, i) => ( - - - - ))} - - ) -} const Issue = ({ match: { params: { id:issueId }}}) => { const { t } = useTranslation() diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js index a5f31981..f0b83882 100644 --- a/src/pages/NotFound.js +++ b/src/pages/NotFound.js @@ -2,10 +2,10 @@ import React from 'react' import { Container, Row, Col } from 'react-bootstrap' import { useTranslation } from 'react-i18next' -const NotFound = () => { +const NotFound = ({ className='page', ...rest }) => { const { t } = useTranslation() return ( - +

{t('pages.notFound.title')}

diff --git a/src/styles/article.scss b/src/styles/article.scss index 14b2ba0c..01874d91 100644 --- a/src/styles/article.scss +++ b/src/styles/article.scss @@ -730,3 +730,22 @@ svg.ArticleFingerprint{ transform: translate(0, 0); } } + + +.ArticleFingerprintTooltip { + z-index: 1000; + font-size: 12px; + background-color: var(--secondary); + color: white; + border-radius: 2px; + padding: 9px 15px; + pointer-events: none; + width: 200px; + left: var(--spacer-5); +} + +.ArticleFingerprintTooltip_heading{ + font-weight: bold; + text-transform: uppercase; + font-family: var(--font-family-monospace); +} diff --git a/src/styles/components/IssueArticleGridItem.scss b/src/styles/components/IssueArticleGridItem.scss index 3d5e35cb..295e4d61 100644 --- a/src/styles/components/IssueArticleGridItem.scss +++ b/src/styles/components/IssueArticleGridItem.scss @@ -2,7 +2,9 @@ .IssueArticleGridItem{ margin: var(--spacer-3) 0; border-top: 1px solid var(--secondary); - + svg{ + cursor: pointer; + } } .IssueArticleGridItem h3{ diff --git a/src/translations.json b/src/translations.json index d58c2586..e0a5f03c 100644 --- a/src/translations.json +++ b/src/translations.json @@ -80,6 +80,9 @@ }, "mobileDisclaimer": "While we work hard on the mobile experience, please explore all three layers - the narrative, the hermeneutics and the data layers on desktop" }, + "fingerprintViewerForm": { + "title": "Preview Fingerprint" + }, "issue": { "title": "Issue {{ id }}" }, @@ -118,6 +121,10 @@ "FormNotebookUrl": { "notebookUrl": "Public notebook url", "notebookUrlDescription": "Use a well formed URL pointing to the .ipynb notebook file." + }, + "fingerprintViewerForm": { + "notebookUrl": "Absolute URL of the ipynb file", + "notebookUrlDescription": "Use a well formed URL pointing to the .ipynb notebook file. For instance use to the raw url of the ipynb file for notebook hosted on Github." } }, "actions": { From 3aa5e619da92b5f71f1710ab7366d2527e670caf Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Wed, 5 Jan 2022 16:28:07 +0100 Subject: [PATCH 04/13] Fix/issue 296 js injection (#306) * default cellTop of 100 to consider the header * Create ArticleCellOutputs.js * move scripts to ArticleCellOutputs the new component that rassemble the cell outputs * forward isJavascriptTrusted if the request comes from a saved article * add isTrusted check at the level of hooks too --- src/components/Article/ArticleCell.js | 15 ++++--- src/components/Article/ArticleCellFigure.js | 41 +++++++----------- src/components/Article/ArticleCellOutput.js | 35 ++-------------- .../Article/ArticleCellOutputPlugin.js | 2 +- src/components/Article/ArticleCellOutputs.js | 42 +++++++++++++++++++ src/components/ArticleV2/Article.js | 4 +- src/components/ArticleV2/ArticleFlow.js | 2 + src/components/ArticleV2/ArticleLayer.js | 2 + src/components/ArticleV2/ArticleLayers.js | 4 +- src/hooks/graphics.js | 6 +-- src/pages/ArticleViewer.js | 1 + src/pages/NotebookViewer.js | 2 + 12 files changed, 87 insertions(+), 69 deletions(-) create mode 100644 src/components/Article/ArticleCellOutputs.js diff --git a/src/components/Article/ArticleCell.js b/src/components/Article/ArticleCell.js index e00ba568..7c340e18 100644 --- a/src/components/Article/ArticleCell.js +++ b/src/components/Article/ArticleCell.js @@ -1,6 +1,6 @@ import React, { lazy } from 'react'; import { Container, Row, Col} from 'react-bootstrap' -import ArticleCellOutput from './ArticleCellOutput' +import ArticleCellOutputs from './ArticleCellOutputs' import ArticleCellContent from './ArticleCellContent' import ArticleCellSourceCode from './ArticleCellSourceCode' import ArticleCellFigure from './ArticleCellFigure' @@ -25,6 +25,7 @@ const ArticleCell = ({ isNarrativeStep, figure, // ArticleFigure instance headingLevel=0, // if isHeading, set this to its ArticleHeading.level value + isJavascriptTrusted = false, onNumClick, }) => { let cellBootstrapColumnLayout = metadata.jdh?.text?.bootstrapColumLayout || BootstrapColumLayout; @@ -93,6 +94,7 @@ const ArticleCell = ({ metadata={metadata} figure={figure} figureColumnLayout={cellObjectBootstrapColumnLayout} + isJavascriptTrusted={isJavascriptTrusted} isNarrativeStep={isNarrativeStep} > } > ) } + return ( @@ -143,10 +147,11 @@ const ArticleCell = ({
- {outputs.length - ? outputs.map((output,i) => ) - :
no output
- } +
diff --git a/src/components/Article/ArticleCellFigure.js b/src/components/Article/ArticleCellFigure.js index e150dcb6..72e67afe 100644 --- a/src/components/Article/ArticleCellFigure.js +++ b/src/components/Article/ArticleCellFigure.js @@ -1,12 +1,16 @@ import React from 'react' -import ArticleCellOutput from './ArticleCellOutput' +import ArticleCellOutputs from './ArticleCellOutputs' import ArticleFigure from './ArticleFigure' import { markdownParser } from '../../logic/ipynb' -import { useInjectTrustedJavascript } from '../../hooks/graphics' import {BootstrapColumLayout} from '../../constants' import { Container, Row, Col} from 'react-bootstrap' -const ArticleCellFigure = ({ figure, metadata={}, outputs=[], sourceCode, figureColumnLayout, children }) => { +const ArticleCellFigure = ({ + figure, metadata={}, outputs=[], sourceCode, + isJavascriptTrusted, + figureColumnLayout, + children +}) => { const isFluidContainer = figure.isCover || (metadata.tags && metadata.tags.includes('full-width')) const captions = outputs.reduce((acc, output) => { if (output.metadata && Array.isArray(output.metadata?.jdh?.object?.source)) { @@ -30,35 +34,20 @@ const ArticleCellFigure = ({ figure, metadata={}, outputs=[], sourceCode, figure figureColumnLayout = { ...figureColumnLayout, ...metadata.jdh?.object?.bootstrapColumLayout } } - // use scripts if there areany - const trustedScripts = outputs.reduce((acc, output) => { - if (typeof output.data === 'object') { - if (Array.isArray(output.data['application/javascript'])) { - return acc.concat(output.data['application/javascript']) - } - } - return acc - }, []) - - const refTrustedJavascript = useInjectTrustedJavascript({ - id: `trusted-script-for-${figure.ref}`, - contents: trustedScripts - }) return ( -
+
-
+
- {!outputs.length ? ( -
-
- ): null} - {outputs.map((output,i) => ( - - ))} +
{children} diff --git a/src/components/Article/ArticleCellOutput.js b/src/components/Article/ArticleCellOutput.js index 4ee5f816..41d12da3 100644 --- a/src/components/Article/ArticleCellOutput.js +++ b/src/components/Article/ArticleCellOutput.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import {markdownParser} from '../../logic/ipynb' import ArticleCellOutputPlugin from './ArticleCellOutputPlugin' @@ -9,7 +9,7 @@ const getOutput = (output) => { : output } -const ArticleCellOutput = ({ output, height, width, hideLabel=false, isTrusted=true, cellIdx=-1 }) => { +const ArticleCellOutput = ({ output, height, width, hideLabel=false, isJavascriptTrusted=false, cellIdx=-1 }) => { const outputTypeClassName= `ArticleCellOutput_${output.output_type}` const { t } = useTranslation() const style = !isNaN(width) && !isNaN(height) ? { @@ -17,34 +17,6 @@ const ArticleCellOutput = ({ output, height, width, hideLabel=false, isTrusted=t width, height, } : {} - const trustedScripts = !!output.data && isTrusted && Array.isArray(output.data['application/javascript']) - ? output.data['application/javascript'] - : [] - // apply scripts if found on data. - useEffect(() => { - if (!trustedScripts.length || isNaN(cellIdx)) { - return - } - console.debug('[ArticleCellOutput] @useEffect found javscript trusted scripts at cellIdx:', cellIdx) - const scriptDomElementId = 'trusted-article-cell-output-' + String(cellIdx) - let scriptDomElement = document.getElementById(scriptDomElementId) - if (scriptDomElement === null) { - const script = document.createElement('script'); - script.setAttribute('id', scriptDomElementId) - script.appendChild(document.createTextNode(trustedScripts.join('\n'))); - document.body.appendChild(script) - } else { - // replace contents of the script - scriptDomElement.appendChild(document.createTextNode(trustedScripts.join('\n'))); - } - return () => { - try { - document.body.removeChild(scriptDomElement) - } catch(e) { - console.warn('document.body.removeChild failed for ', cellIdx, e.message) - } - } - }, [trustedScripts, cellIdx]) if(output.output_type === 'display_data' && output.data['text/markdown']) { return ( @@ -54,8 +26,7 @@ const ArticleCellOutput = ({ output, height, width, hideLabel=false, isTrusted=t ) } if (['execute_result', 'display_data'].includes(output.output_type) && output.data['text/html']) { - if (trustedScripts.length) { - // use DOM to handle this + if (isJavascriptTrusted) { // use DOM directly to handle this return ( this.el = el} />; + return
this.el = el} />; } } diff --git a/src/components/Article/ArticleCellOutputs.js b/src/components/Article/ArticleCellOutputs.js new file mode 100644 index 00000000..22cf0d0d --- /dev/null +++ b/src/components/Article/ArticleCellOutputs.js @@ -0,0 +1,42 @@ +import React from 'react' +import ArticleCellOutput from './ArticleCellOutput' +import { useInjectTrustedJavascript } from '../../hooks/graphics' + +const ArticleCellOutputs = ({ + isJavascriptTrusted, + outputs=[], + cellIdx, + hideLabel, +}) => { + // use scripts if there areany + const trustedScripts = isJavascriptTrusted ? outputs.reduce((acc, output) => { + if (typeof output.data === 'object') { + if (Array.isArray(output.data['application/javascript'])) { + return acc.concat(output.data['application/javascript']) + } + } + return acc + }, []): [] + + const refTrustedJavascript = useInjectTrustedJavascript({ + id: `trusted-script-for-${cellIdx}`, + contents: trustedScripts, + isTrusted: isJavascriptTrusted + }) + + return ( +
+ {outputs.map((output,i) => ( + + ))} +
+ ) +} + +export default ArticleCellOutputs diff --git a/src/components/ArticleV2/Article.js b/src/components/ArticleV2/Article.js index f5a6795d..320b2b19 100644 --- a/src/components/ArticleV2/Article.js +++ b/src/components/ArticleV2/Article.js @@ -21,7 +21,8 @@ const Article = ({ doi, binderUrl, bibjson, - emailAddress + emailAddress, + isJavascriptTrusted=false }) => { const [selectedDataHref, setSelectedDataHref] = useState(null) const { height, width } = useCurrentWindowDimensions() @@ -60,6 +61,7 @@ const Article = ({ binderUrl={binderUrl} emailAddress={emailAddress} onDataHrefClick={onDataHrefClickHandler} + isJavascriptTrusted={isJavascriptTrusted} > { const setVisibleCell = useArticleToCStore(store => store.setVisibleCell) @@ -89,6 +90,7 @@ const ArticleFlow = ({ paragraphs={paragraphs} height={height} width={width} + isJavascriptTrusted={isJavascriptTrusted} > {children} diff --git a/src/components/ArticleV2/ArticleLayer.js b/src/components/ArticleV2/ArticleLayer.js index af873827..def9e1bb 100644 --- a/src/components/ArticleV2/ArticleLayer.js +++ b/src/components/ArticleV2/ArticleLayer.js @@ -44,6 +44,7 @@ const ArticleLayer = ({ layers=[], children, width=0, height=0, + isJavascriptTrusted=false, style, FooterComponent = function({ width, height }) { return
}, }) => { @@ -329,6 +330,7 @@ const ArticleLayer = ({ ) : null} { const clearVisibleCellsIdx = useArticleToCStore(store => store.clearVisibleCellsIdx) @@ -37,7 +38,7 @@ const ArticleLayers = ({ }, setQuery] = useQueryParams({ [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1), [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative), - [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 0), + [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 100), [DisplayPreviousLayerQueryParam]: StringParam, [DisplayPreviousCellIdxQueryParam]: withDefault(NumberParam, -1), [DisplayLayerHeightQueryParam]: withDefault(NumberParam, -1), @@ -116,6 +117,7 @@ const ArticleLayers = ({ height={height} width={width} layers={layers} + isJavascriptTrusted={isJavascriptTrusted} style={{ width, height, diff --git a/src/hooks/graphics.js b/src/hooks/graphics.js index b378f91e..72fa0295 100644 --- a/src/hooks/graphics.js +++ b/src/hooks/graphics.js @@ -183,9 +183,9 @@ export function useRefWithCallback(onMount, onUnmount) { } -export function useInjectTrustedJavascript({ id='', contents=[], onMount, onUnmount }) { +export function useInjectTrustedJavascript({ id='', isTrusted=false, contents=[], onMount, onUnmount }) { const setRefWithCallback = useRefWithCallback((node) => { - if (contents.length) { + if (isTrusted && contents.length) { console.debug('useInjectTrustedJavascript', id, contents.length) let scriptDomElement = document.getElementById(id) if (scriptDomElement === null) { @@ -199,7 +199,7 @@ export function useInjectTrustedJavascript({ id='', contents=[], onMount, onUnmo onMount(node) } }, (node) => { - if (contents.length) { + if (isTrusted && contents.length) { let scriptDomElement = document.getElementById(id) try { node.removeChild(scriptDomElement) diff --git a/src/pages/ArticleViewer.js b/src/pages/ArticleViewer.js index 31c2d948..4ffaa774 100644 --- a/src/pages/ArticleViewer.js +++ b/src/pages/ArticleViewer.js @@ -85,6 +85,7 @@ const ArticleViewer = ({ match: { params: { pid }}}) => { doi={article.doi} issue={article.issue} bibjson={article.citation} + isJavascriptTrusted match={{ params: { encodedUrl: article.notebook_url diff --git a/src/pages/NotebookViewer.js b/src/pages/NotebookViewer.js index d3fd3e86..ec94f300 100644 --- a/src/pages/NotebookViewer.js +++ b/src/pages/NotebookViewer.js @@ -28,6 +28,7 @@ const NotebookViewer = ({ doi, bibjson, pid, + isJavascriptTrusted }) => { const [{[ArticleVersionQueryParam]: version}] = useQueryParams({ [ArticleVersionQueryParam]: withDefault(NumberParam, 2) @@ -100,6 +101,7 @@ const NotebookViewer = ({ excerpt={excerpt} plainKeywords={keywords} plainContributor={plainContributor} + isJavascriptTrusted={isJavascriptTrusted} /> ) : ( From 98864f7e25c3851d77c406d661cce3246c6ee2b1 Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Wed, 5 Jan 2022 16:38:19 +0100 Subject: [PATCH 05/13] Fix/issue 305 citation style (#307) * change btn close see #249 * remove MLA :P and add harvard --- src/components/Article/ArticleCitationModal.js | 12 +++++------- src/styles/index.scss | 9 +++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/Article/ArticleCitationModal.js b/src/components/Article/ArticleCitationModal.js index 03d72392..837bbf81 100644 --- a/src/components/Article/ArticleCitationModal.js +++ b/src/components/Article/ArticleCitationModal.js @@ -1,5 +1,5 @@ import React, {useState} from 'react' -import { Modal, Button, ToggleButtonGroup, ToggleButton } from 'react-bootstrap' +import { Modal, ToggleButtonGroup, ToggleButton } from 'react-bootstrap' import Citation from '../Citation' // import CopyToClipboardTrigger from '../CopyToClipboardTrigger' // import { CopyToClipboard } from 'react-copy-to-clipboard' @@ -9,7 +9,9 @@ const ArticleCitationModal = (props) => { const choices = [ { format: 'bibtex', label: 'bibtex' }, { format: 'html', template: 'apa', label: 'APA' }, - { format: 'html', template: 'mla', label: 'MLA' } + // { format: 'html', template: 'mla', label: 'MLA' }, + // { format: 'html', template: 'vancouver', label: 'vancouver' }, + { format: 'html', template: 'harvard1', label: 'harvard' } ] const [value, setValue] = useState(1); const handleChange = (val) => { @@ -24,7 +26,7 @@ const ArticleCitationModal = (props) => { className="shadow" centered > - + Cite as ... @@ -44,10 +46,6 @@ const ArticleCitationModal = (props) => { }} />
- - - - ) } diff --git a/src/styles/index.scss b/src/styles/index.scss index c73b9808..62e3a0dc 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -462,6 +462,15 @@ blockquote.code{ padding: var(--spacer-3); padding-bottom: 0; } + .btn-close{ + padding: var(--spacer-3); + width: 30px; + height: 30px; + background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%231E152A'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + } + .btn-close:hover{ + opacity: 1; + } } .left-0{ From fc55ddc3277c9dd1585c90c12e7fddbd89cb30f0 Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Thu, 6 Jan 2022 15:35:30 +0100 Subject: [PATCH 06/13] Misc/clean v2 (#311) * hide tooltip (removing onMousemove handler is sufficient to silence the tooltip) Fix #308 * hide Footer component on specific routes (e.g. having fixed height, lik ArticleViewer) * visualise loading bar in NotebookViewer on top of ArticleLayers * hide console debug when in production * in md screen reduce the left offset * render Bilbiography and Footer in each ArticleLayer * adapt ArticleBibliography component to meet the new properties when pre rendered in Article V2 component fix #310 * fix #309 --- src/App.js | 2 +- src/components/Article/ArticleBibliography.js | 19 +++++++++---- src/components/Article/ArticleFingerprint.js | 12 ++++++--- src/components/ArticleV2/Article.js | 11 ++++++-- src/components/ArticleV2/ArticleFlow.js | 4 +++ src/components/ArticleV2/ArticleLayer.js | 7 +++-- src/components/ArticleV2/ArticleLayers.js | 4 +++ src/components/Footer/Footer.js | 9 +++++-- src/components/Issue/IssueArticlesGrid.js | 10 ++++--- src/constants.js | 2 +- src/index.js | 5 ++++ src/models/ArticleReference.js | 27 ++++++++++++++----- src/pages/NotebookViewer.js | 6 +---- src/styles/graphics.scss | 5 ++++ 14 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/App.js b/src/App.js index bfd1c7bf..9320b1d7 100644 --- a/src/App.js +++ b/src/App.js @@ -209,7 +209,7 @@ export default function App() { -
+