+ {num !== -1 ? (
+
+ {num}
+
+ ):null}
{toggleVisibility
? (
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) => {
}} />
-
-
- Close
-
)
}
diff --git a/src/components/Article/ArticleFingerprint.js b/src/components/Article/ArticleFingerprint.js
index 0f7faa41..710d2942 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,48 @@ 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') {
+ if (cached.current.idx !== -1) {
+ const datum = cells[cached.current.idx]
+ onClick(e, datum, cached.current.idx)
+ } else {
+ onClick(e)
+ }
+ }
+ }
+
if (cells.length===0) {
return null
}
@@ -52,6 +101,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 +148,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
new file mode 100644
index 00000000..77fee6ca
--- /dev/null
+++ b/src/components/ArticleV2/Article.js
@@ -0,0 +1,117 @@
+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 ArticleHelmet from './ArticleHelmet'
+import ArticleBibliography from '../Article/ArticleBibliography'
+import Footer from '../Footer'
+
+const Article = ({
+ // pid,
+ // Notebook instance, an object containing {cells:[], metadata:{}}
+ ipynb,
+ url,
+ imageUrl,
+ publicationDate = new Date(),
+ publicationStatus,
+ issue,
+ plainTitle,
+ plainContributor = '',
+ plainKeywords = [],
+ excerpt,
+ doi,
+ binderUrl,
+ bibjson,
+ emailAddress,
+ isJavascriptTrusted=false
+}) => {
+ const [selectedDataHref, setSelectedDataHref] = useState(null)
+ 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`)
+ console.debug('[Article] loading articleTree anchors:', articleTree.anchors)
+
+ const onDataHrefClickHandler = (d) => {
+ console.debug('DataHref click handler')
+ setSelectedDataHref(d)
+ }
+ const renderedBibliographyComponent = (
+
+ )
+ const renderedFooterComponent = (
)
+
+
+
+ return (
+ <>
+
+
+
+
+
+
+ {articleTree.citationsFromMetadata
+ ?
+ : null
+ }
+
+
+ >
+ )
+}
+
+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..04a15378
--- /dev/null
+++ b/src/components/ArticleV2/ArticleCellPlaceholder.js
@@ -0,0 +1,67 @@
+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'
+import {ArrowDown} from 'react-feather'
+
+
+const ArticleCellPlaceholder = ({
+ type='code',
+ layer,
+ // 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 (
+
+
+
+ {type === 'markdown'
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
+
+
+ )
+}
+
+export default ArticleCellPlaceholder
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 (
+
+ onClick(e, {
+ idx: style.cellIdx.get(),
+ layer: style.cellLayer.get(),
+ })}>add reading point ...
+
+
+
+
+ )
+}
+export default ArticleCellPopup
diff --git a/src/components/ArticleV2/ArticleFlow.js b/src/components/ArticleV2/ArticleFlow.js
new file mode 100644
index 00000000..929bfc5d
--- /dev/null
+++ b/src/components/ArticleV2/ArticleFlow.js
@@ -0,0 +1,112 @@
+import React from 'react'
+import { LayerNarrative, LayerHermeneutics, 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,
+ onDataHrefClick,
+ binderUrl=null,
+ // emailAddress,
+ headingsPositions=[],
+ tocOffset=99,
+ layers=[LayerNarrative, LayerHermeneutics],
+ isJavascriptTrusted = false,
+ renderedBibliographyComponent=null,
+ renderedFooterComponent=null,
+ 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 || cell.isFigure || cell.isTable )) {
+ 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 (
+ <>
+
+
+
+
+ >
+ )
+}
+
+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/ArticleHelmet.js b/src/components/ArticleV2/ArticleHelmet.js
new file mode 100644
index 00000000..a797463f
--- /dev/null
+++ b/src/components/ArticleV2/ArticleHelmet.js
@@ -0,0 +1,71 @@
+import React, { useLayoutEffect } from 'react'
+import { Helmet } from 'react-helmet'
+
+const ArticleHelmet = ({
+ url='',
+ imageUrl='',
+ plainTitle='',
+ excerpt='',
+ plainContributor='',
+ plainKeywords=[],
+ issue,
+ publicationDate = new Date()
+}) => {
+ // apply zotero when the DOM is ready
+ useLayoutEffect(() => {
+ console.debug('[ArticleHelmet] @useLayoutEffect')
+ document.dispatchEvent(new Event('ZoteroItemUpdated', {
+ bubbles: true,
+ cancelable: true
+ }))
+ }, [url])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {plainKeywords.map((k,i) => (
+
+ ))}
+
+
+
+
+
+ {plainContributor.split(', ').map((d,i) => (
+
+ ))}
+ {/*
+ dc:title Studying E-Journal User Behavior Using Log Files
+ dc:creator Yu, L
+ dc:creator Apps, A
+ dc:subject http://purl.org/dc/terms/DDC 020
+ dc:subject http://purl.org/dc/terms/LCC Z671
+ dc:publisher Elsevier
+ dc:type http://purl.org/dc/terms/DCMIType Text
+ dcterms:issued http://purl.org/dc/terms/W3CDTF 2000
+ dcterms:isPartOf urn:ISSN:0740-8188
+ dcterms:bibliographicCitation
+ */}
+ {plainKeywords.map((k,i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ArticleHelmet
diff --git a/src/components/ArticleV2/ArticleLayer.js b/src/components/ArticleV2/ArticleLayer.js
new file mode 100644
index 00000000..ce94432a
--- /dev/null
+++ b/src/components/ArticleV2/ArticleLayer.js
@@ -0,0 +1,387 @@
+import React, { useEffect } from 'react'
+import { LayerNarrative } from '../../constants'
+import ArticleCell from '../Article/ArticleCell'
+import ArticleCellObserver from './ArticleCellObserver'
+import ArticleCellPlaceholder from './ArticleCellPlaceholder'
+import ArticleCellPopup from './ArticleCellPopup'
+import {a, useSpring, config} from 'react-spring'
+import { useRefWithCallback } from '../../hooks/graphics'
+import { Button } from 'react-bootstrap'
+import { ArrowRight, ArrowLeft } from 'react-feather'
+import {
+ DisplayLayerSectionBibliography,
+ DisplayLayerSectionFooter
+} from '../../constants'
+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,
+ selectedLayerHeight=-1,
+ onDataHrefClick,
+ onCellPlaceholderClick,
+ onCellIntersectionChange,
+ onAnchorClick,
+ isSelected=false,
+ selectedLayer='',
+ previousLayer='',
+ selectedSection=null,
+ previousCellIdx=-1,
+ layers=[],
+ children,
+ width=0, height=0,
+ isJavascriptTrusted=false,
+ style,
+ renderedBibliographyComponent=null,
+ renderedFooterComponent=null,
+}) => {
+ 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
+ }))
+ const layerRef = useRefWithCallback((layerDiv) => {
+ if (selectedSection) {
+ console.info('[ArticleLayer] @useRefWithCallback on selectedSection selected:', selectedSection)
+ // get section offset
+ const sectionElement = document.getElementById(getCellAnchorFromIdx(selectedSection, layer))
+ if (!sectionElement) {
+ console.warn('[ArticleLayer] @useRefWithCallback could not find any sectionElement with given id:', selectedSection)
+ return
+ }
+ layerDiv.scrollTo({
+ top: sectionElement.offsetTop + layerDiv.offsetTop - 150,
+ behavior: previousLayer === selectedLayer
+ ? 'smooth'
+ : 'instant'
+ })
+ return
+ } else 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
+ }
+ // 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 || previousLayer === selectedLayer
+ ? 'smooth'
+ : 'instant'
+ })
+ })
+
+ const onCellPlaceholderClickHandler = (e, cell) => {
+ if (typeof onCellPlaceholderClick === 'function') {
+ const wrapper = e.currentTarget.closest('.ArticleLayer_placeholderWrapper')
+ onAnchorClick(e, {
+ layer: cell.layer,
+ idx: cell.idx,
+ previousIdx: cell.idx,
+ previousLayer: layer,
+ height, // ref height
+ 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) => {
+ // console.info('@onCellPlaceholderClickHandler', e, cell)
+ // // eslint-disable-next-line
+ // debugger
+
+ if (typeof onCellPlaceholderClick === 'function') {
+ onCellPlaceholderClick(e, {
+ layer: previousLayer,
+ idx: previousCellIdx > -1 ? previousCellIdx : cell.idx,
+ height, // ref height
+ 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(() => {
+ 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 (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])
+
+ 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) => (
+
+ paragraphs[d].num)}
+ />
+
+ ))}
+
+
+
+
onCellPlaceholderClickHandler(e, firstCellInGroup)}>
+ read in {firstCellInGroup.layer} layer
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {paragraphsIndices.map((j) => {
+ const cell = paragraphs[j]
+ if(!cell) {
+ // eslint-disable-next-line
+ debugger
+ }
+ return (
+
+
+
+
+
+ { cell.idx === selectedCellIdx && previousLayer !== '' && previousLayer !== layer ? (
+ onSelectedCellClickHandler(e, cell)}
+ >
+ back
+
+ ) : null}
+
+
+
+
+ )
+ })}
+
+ )
+ })}
+
+
+ {renderedBibliographyComponent}
+
+
+ {renderedFooterComponent}
+
+ )
+}
+
+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..9a6cc39b
--- /dev/null
+++ b/src/components/ArticleV2/ArticleLayer.module.css
@@ -0,0 +1,154 @@
+.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;
+ height: 0;
+ overflow: hidden;
+}
+.placeholder{
+ max-height: 200px;
+ overflow: hidden;
+ position: relative;
+}
+.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, #B1FBEC00 0%, #B1FBECff 50%);
+}
+.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: 0;
+ background: transparent;
+ height: 100%;
+ right: 16%;
+ z-index: -1;
+ top: 0;
+}
+.placeholderActive:after{
+ content: '';
+ position: absolute;
+ top: 0px;
+ left: 10px;
+ width: 3px;
+ background: var(--dark);
+ bottom: 0px;
+ z-index: 1000;
+}
+.placeholderActive_narrative_on,
+.placeholderActive_hermeneutics_on,
+.placeholderActive_data_on,
+.cellActive_on{
+ /* border-top: 1px solid; */
+ composes: placeholderActive;
+ /* background: #ffffff77; */
+}
+.placeholderActive_narrative_off,
+.placeholderActive_hermeneutics_off,
+.placeholderActive_data_off, .cellActive_off{
+ composes: placeholderActive;
+ background: transparent;
+ opacity: 0;
+}
+.cellActiveBackButton{
+ 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/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..c316ab2f
--- /dev/null
+++ b/src/components/ArticleV2/ArticleLayers.js
@@ -0,0 +1,149 @@
+import React, { useEffect } from 'react'
+import ArticleLayer from './ArticleLayer'
+import { useQueryParams, StringParam, NumberParam, withDefault, } from 'use-query-params'
+import {
+ DisplayLayerQueryParam,
+ LayerNarrative,
+ DisplayLayerCellIdxQueryParam,
+ DisplayLayerCellTopQueryParam,
+ DisplayPreviousLayerQueryParam,
+ DisplayPreviousCellIdxQueryParam,
+ DisplayLayerHeightQueryParam,
+ DisplayLayerSectionParam
+} from '../../constants'
+import { useArticleToCStore } from '../../store'
+
+const ArticleLayers = ({
+ memoid='',
+ layers=[],
+ paragraphsGroups=[],
+ paragraphs=[],
+ width=0,
+ height=0,
+ onCellPlaceholderClick,
+ onCellIntersectionChange,
+ onDataHrefClick,
+ isJavascriptTrusted=false,
+ children,
+ renderedBibliographyComponent,
+ renderedFooterComponent
+}) => {
+ 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 [{
+ [DisplayLayerQueryParam]:selectedLayer,
+ [DisplayLayerCellIdxQueryParam]:selectedCellIdx,
+ [DisplayLayerCellTopQueryParam]: selectedCellTop,
+ [DisplayPreviousLayerQueryParam]: previousLayer,
+ [DisplayPreviousCellIdxQueryParam]: previousCellIdx,
+ [DisplayLayerHeightQueryParam]: layerHeight,
+ [DisplayLayerSectionParam]: layerSection,
+ }, setQuery] = useQueryParams({
+ [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1),
+ [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative),
+ [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 100),
+ [DisplayPreviousLayerQueryParam]: StringParam,
+ [DisplayPreviousCellIdxQueryParam]: withDefault(NumberParam, -1),
+ [DisplayLayerHeightQueryParam]: withDefault(NumberParam, -1),
+ [DisplayLayerSectionParam]: 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, 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)
+ setQuery({
+ [DisplayLayerQueryParam]: selectedLayer,
+ [DisplayLayerCellIdxQueryParam]: idx,
+ [DisplayLayerCellTopQueryParam]: y,
+ [DisplayPreviousLayerQueryParam]: previousLayer,
+ [DisplayLayerHeightQueryParam]: layerHeight,
+ [DisplayLayerSectionParam]: layerSection
+ }, 'replaceIn')
+ // this query
+ setQuery({
+ [DisplayLayerQueryParam]: layer,
+ [DisplayLayerCellIdxQueryParam]: idx,
+ [DisplayLayerCellTopQueryParam]: y,
+ [DisplayPreviousLayerQueryParam]: selectedLayer,
+ [DisplayLayerHeightQueryParam]: h,
+ // section is Bibliography, later annexes etc...
+ [DisplayLayerSectionParam]: undefined
+ })
+ 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 (
+ <>
+ {layers.map((layer, 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}
+ selectedSection={layerSection}
+ previousLayer={previousLayer}
+ previousCellIdx={previousCellIdx}
+ height={height}
+ width={width}
+ layers={layers}
+ isJavascriptTrusted={isJavascriptTrusted}
+ style={{
+ width,
+ height,
+ top: 0,
+ overflow: selectedLayer === layer ? "scroll": "hidden",
+ zIndex: i,
+ pointerEvents: selectedLayer === layer ? "auto": "none",
+ // left: i * width
+ }}
+ renderedBibliographyComponent={renderedBibliographyComponent}
+ renderedFooterComponent={renderedFooterComponent}
+ >
+ {children}
+
+ ))}
+ >
+ )
+}
+
+export default ArticleLayers
diff --git a/src/components/ArticleV2/ArticleToC.js b/src/components/ArticleV2/ArticleToC.js
new file mode 100644
index 00000000..c388d1f7
--- /dev/null
+++ b/src/components/ArticleV2/ArticleToC.js
@@ -0,0 +1,247 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useQueryParams, withDefault, NumberParam, StringParam } from 'use-query-params'
+import { useArticleToCStore } from '../../store'
+import {
+ LayerHermeneutics,
+ LayerNarrative,
+ DisplayLayerQueryParam,
+ DisplayLayerCellIdxQueryParam,
+ DisplayLayerCellTopQueryParam,
+ DisplayPreviousLayerQueryParam,
+ DisplayLayerSectionParam,
+ DisplayLayerSectionBibliography
+} from '../../constants'
+import ArticleToCStep from './ArticleToCStep'
+import ArticleToCBookmark from './ArticleToCBookmark'
+
+const ArticleToC = ({
+ memoid='',
+ layers=[],
+ paragraphs=[],
+ headingsPositions=[],
+ binderUrl=null,
+ width=100,
+ height=100
+}) => {
+ const { t } = useTranslation()
+ const visibleCellsIdx = useArticleToCStore(state => state.visibleCellsIdx)
+ const setVisibleCellsIdx = useArticleToCStore(state => state.setVisibleCellsIdx)
+ const [{
+ [DisplayLayerQueryParam]:selectedLayer,
+ [DisplayLayerCellIdxQueryParam]:selectedCellIdx,
+ }, setQuery] = useQueryParams({
+ [DisplayLayerCellIdxQueryParam]: withDefault(NumberParam, -1),
+ [DisplayLayerQueryParam]: withDefault(StringParam, LayerNarrative),
+ [DisplayLayerCellTopQueryParam]: withDefault(NumberParam, 100),
+ [DisplayLayerSectionParam]: StringParam,
+ })
+
+ 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
+ }
+ if (cell.hidden) {
+
+ // is possible that there are headingPositions within hidden cells.
+ // 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, lastVisibleCellIdx ])
+
+ const onStepClickHandler = (step) => {
+ console.debug('[ArticleToC] @onClickHandler step:', step, selectedLayer)
+ // go to the cell
+ setQuery({
+ [DisplayLayerCellIdxQueryParam]: step.cell.idx,
+ [DisplayPreviousLayerQueryParam]: undefined,
+ [DisplayLayerSectionParam]: undefined
+ })
+ }
+
+ const onBookmarkClickHandler = () => {
+ setQuery({
+ [DisplayLayerCellIdxQueryParam]: selectedCellIdx,
+ [DisplayPreviousLayerQueryParam]: undefined,
+ [DisplayLayerCellTopQueryParam]: 100,
+ [DisplayLayerSectionParam]: undefined
+ })
+ }
+
+ // this listens to click on Bibliography or other extra section (author bio?)
+ const onSectionClickHandler = (e, section) => {
+ setQuery({
+ [DisplayLayerCellIdxQueryParam]: undefined,
+ [DisplayPreviousLayerQueryParam]: undefined,
+ [DisplayLayerSectionParam]: section
+ })
+ }
+
+ React.useEffect(() => {
+ console.debug('[ArticleToC] @useEffect', visibleCellsIdx)
+ let timer = null
+
+ if (timer) {
+ clearTimeout(timer)
+ }
+ timer = setTimeout(() => {
+ console.debug('[ArticleToC] @useEffect @timeout!')
+ let visibilityChanged = false
+ const updatedVisibleCellsIdx = []
+ for (let i=0, l=visibleCellsIdx.length; i < l; i++) {
+ const el = document.getElementById(selectedLayer + visibleCellsIdx[i])
+ if (el) {
+ const rect = el.getBoundingClientRect()
+ if ( rect.top < 0 || rect.top > height) {
+ console.debug('[ArticleToC]', visibleCellsIdx[i], 'is not visible anymore', rect.top, height)
+ visibilityChanged = true
+ } else {
+ updatedVisibleCellsIdx.push(visibleCellsIdx[i])
+ }
+ }
+ }
+ if(visibilityChanged) {
+ console.debug('[ArticleToC] visibilityChanged from', visibleCellsIdx, 'to', updatedVisibleCellsIdx)
+ setVisibleCellsIdx(updatedVisibleCellsIdx)
+ }
+ }, 100)
+ // delay and double check if cell idx are still visible.
+ return function cleanup() {
+ clearTimeout(timer)
+ }
+ }, [visibleCellsIdx, selectedLayer])
+ return (
+ <>
+
+ {layers.map((d,i) => (
+
setQuery({[DisplayLayerQueryParam]: 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 ? (
+
step.cell.idx
+ ? '100%'
+ : (selectedCellIdx < step.cell.idx ? 0 : '50%')
+ }}
+ />
+ ): 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)'
+ }
+
+
+ )})}
+
+
+
onSectionClickHandler(e, DisplayLayerSectionBibliography)}
+ >{t('bibliography')}
+
+ >
+ )
+}
+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/ArticleToCBookmark.js b/src/components/ArticleV2/ArticleToCBookmark.js
new file mode 100644
index 00000000..79d6b6f8
--- /dev/null
+++ b/src/components/ArticleV2/ArticleToCBookmark.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import {Link} from 'react-feather'
+
+const ArticleToCBookmark = ({ onClick, style, children }) => {
+ return(
+
+
{children ? children : }
+
+ )
+}
+
+export default ArticleToCBookmark
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 (
+
+
+ {children}
+
+
+ {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/Footer/Footer.js b/src/components/Footer/Footer.js
index cb2838ef..792eb1a3 100644
--- a/src/components/Footer/Footer.js
+++ b/src/components/Footer/Footer.js
@@ -1,5 +1,6 @@
import React from 'react'
import LangNavLink from '../LangNavLink'
+import { useLocation } from 'react-router'
import { useTranslation } from 'react-i18next'
import { Container, Row, Col, Nav } from 'react-bootstrap'
import {
@@ -18,9 +19,13 @@ import styles from './Footer.module.scss'
const now = new Date()
-const Footer = () => {
+const Footer = ({ hideOnRoutes=[]}) => {
const { t } = useTranslation()
-
+ const { pathname } = useLocation()
+ if (hideOnRoutes.some((d) => pathname.indexOf(d) !== -1)) {
+ console.debug('[Footer] hidden following hideOnRoutes:', hideOnRoutes, 'with pathname:', pathname)
+ return null
+ }
return (
<>
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}) => {
- {t('FormNotebookUrl_GenerateLink')}
-
+ {t('FormNotebookUrl_GenerateLink')}
+
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..9b872f49
--- /dev/null
+++ b/src/components/Issue/IssueArticlesGrid.js
@@ -0,0 +1,115 @@
+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])
+ }
+ }
+ // eslint-disable-next-line no-unused-vars
+ 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()
+ // link to specific cell in article
+ const url = idx
+ ? `/en/article/${article.abstract.pid}?${DisplayLayerCellIdxQueryParam}=${idx}`
+ : `/en/article/${article.abstract.pid}`
+ history.push(url);
+ }
+ const onMouseOutHandler = () => {
+ tooltipApi.start({ opacity: 0 })
+ }
+
+ return (
+ <>
+
+ {animatedTooltipProps.x.to(() => String(tooltipText.current.text))}
+
+
+ {editorials.map((article, i) => (
+
+ {/* to rehab tooltip add onMouseMove={onMouseMoveHandler} */}
+ onClickHandler(e, datum, idx, article)}
+ onMouseOut={onMouseOutHandler}
+ article={article}
+ isEditorial
+ />
+
+ ))}
+ {articles.map((article, i) => (
+
+ {/* to rehab tooltip add onMouseMove={onMouseMoveHandler} */}
+ onClickHandler(e, datum, idx, article)}
+ onMouseOut={onMouseOutHandler}
+ article={article}
+ num={i+1}
+ total={articles.length}
+ />
+
+ ))}
+
+ >
+ )
+}
+
+export default IssueArticlesGrid
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..9dcddd43 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -33,7 +33,7 @@ export const ReCaptchaSiteKey = process.env.REACT_APP_RECAPTCHA_SITE_KEY
export const GaTrackingId = process.env.REACT_APP_GA_TRACKING_ID
export const BootstrapColumLayout = Object.freeze({
- md: { span:8, offset:2 },
+ md: { span:8, offset:1 },
lg: { span:8, offset:2 }
})
@@ -121,13 +121,25 @@ 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'
export const DisplayLayerAll = 'all'
+// available sections to link to
+export const DisplayLayerSectionBibliography = 'bib'
+export const DisplayLayerSectionHeader = 'head'
+export const DisplayLayerSectionFooter = 'foo'
+// names of the query parameters available
export const DisplayLayerQueryParam = 'layer'
+export const DisplayPreviousLayerQueryParam = 'pl'
export const DisplayLayerCellIdxQueryParam = 'idx'
-
+export const DisplayPreviousCellIdxQueryParam = 'pidx'
+export const DisplayLayerCellTopQueryParam = 'y'
+export const DisplayLayerHeightQueryParam = 'lh'
+export const DisplayLayerSectionParam = 's'
// 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..72fa0295 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='', isTrusted=false, contents=[], onMount, onUnmount }) {
+ const setRefWithCallback = useRefWithCallback((node) => {
+ if (isTrusted && 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 (isTrusted && 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/index.js b/src/index.js
index e3373032..debb1450 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,11 @@ import App from './App';
import * as serviceWorker from './serviceWorker';
import WebFontLoader from 'webfontloader'
+// replace console.* for disable log debug on production
+if (process.env.NODE_ENV === 'production') {
+ console.debug = () => {}
+}
+
WebFontLoader.load({
google: {
families: [
diff --git a/src/logic/api/fetchData.js b/src/logic/api/fetchData.js
index 5f6fb431..8bf5cc69 100644
--- a/src/logic/api/fetchData.js
+++ b/src/logic/api/fetchData.js
@@ -85,7 +85,8 @@ export const useGetJSON = ({
allowCached=true,
delay=0,
onDownloadProgress,
- timeout=process.env.REACT_APP_API_TIMEOUT || 0
+ timeout=process.env.REACT_APP_API_TIMEOUT || 0,
+ raw=false
}) => {
const cache = useRef({});
const [response, setResponse] = useState({
@@ -116,7 +117,9 @@ export const useGetJSON = ({
if (cache.current[url] && allowCached=== true) {
console.debug('useGetDataset allowCached url:', url)
const data = cache.current[url];
- data.cached = true;
+ if (!raw) {
+ data.cached = true;
+ }
if (cancelRequest) return;
setResponse({
data: data,
@@ -162,3 +165,7 @@ export const useGetJSON = ({
}, [url, allowCached, delay])
return response
}
+
+export const useGetRawContents = (opts) => {
+ return useGetJSON({... opts, raw:true })
+}
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/logic/ipynb.js b/src/logic/ipynb.js
index 3033abe2..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
@@ -182,7 +189,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)
@@ -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,12 +256,17 @@ 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]
+ 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
@@ -268,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');
@@ -370,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/ArticleReference.js b/src/models/ArticleReference.js
index 8c69a3c8..bc83367d 100644
--- a/src/models/ArticleReference.js
+++ b/src/models/ArticleReference.js
@@ -9,6 +9,8 @@ export default class ArticleReference {
this.ref = ref
let year = ""
let reference = ""
+ const authorNames = this.getAuthorNames(ref)
+
if (ref) {
if (ref.issued && ref.issued.year) {
year = ref.issued.year
@@ -19,23 +21,36 @@ export default class ArticleReference {
}
// set shortRef accodring to different condition: use elseif
if (!ref.author && Array.isArray(ref.editor)) {
- this.shortRef = ` ${ref.editor.map(d => d.family).join(', ').trim()} (Ed.) ${year} `
+ this.shortRef = ` ${authorNames} (Ed.) ${year} `
} else if (ref.type ==='webpage') {
- if (ref["container-title"]) {
- reference = ref["container-title"]
+ if (ref['container-title']) {
+ reference = ref['container-title']
this.shortRef = ` ${reference} ${year} `
} else {
- this.shortRef = ` ${ref.author?.map(d => d.family).join(', ').trim()} ${year} `
+ this.shortRef = ` ${authorNames} ${year} `
}
} else if (ref.type ==='article-magazine' || ref.type ==='article-newspaper') {
if(!ref.title){
- this.shortRef = ` ${ref.author?.map(d => d.family).join(', ').trim()} ${year} `
+ this.shortRef = ` ${authorNames} ${year} `
} else {
this.shortRef = ` ${ref.title} ${year} `
}
} else {
- this.shortRef = ` ${ref.author?.map(d => d.family).join(', ').trim()} ${year} `
+ this.shortRef = ` ${authorNames} ${year} `
}
}
}
+
+ getAuthorNames(ref) {
+ // add editors as authors when there are no authors
+ let authorNames = !ref.author && Array.isArray(ref.editor)
+ ? ref.editor
+ : ref.author
+ // remap authors to get nice stuff, like Turchin, Currie, Whitehouse, et al. 2018
+ authorNames = (authorNames ?? []).map(d => d.family.trim())
+ if(authorNames.length > 3) {
+ authorNames = authorNames.slice(0, 3).concat(['et al.'])
+ }
+ return authorNames.join(', ')
+ }
}
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/ArticleViewer.js b/src/pages/ArticleViewer.js
index a70f006b..6e3084c1 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)
@@ -72,12 +79,13 @@ const ArticleViewer = ({ match: { params: { pid }}}) => {
collaborators={collaborators}
keywords={keywords}
publicationStatus={article.status}
- publicationDate={article.publication_date}
+ publicationDate={new Date(article.issue?.publication_date)}
binderUrl={article.binder_url}
emailAddress={article.abstract?.contact_email}
doi={article.doi}
issue={article.issue}
bibjson={article.citation}
+ isJavascriptTrusted
match={{
params: {
encodedUrl: article.notebook_url
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')}
-
+
}
{
+ console.debug('[Faq] REACT_APP_GITHUB_WIKI_FAQ', process.env.REACT_APP_GITHUB_WIKI_FAQ)
+
+ const { data, error } = useGetRawContents({
+ url: process.env.REACT_APP_GITHUB_WIKI_FAQ,
+ })
+ // get sections, delimited by H2
+ const sections = (data || '')
+ .split(/\n(## [^\n]*)\n/g)
+ .slice(1)
+ .reduce(chunkReducer, [])
+ .map(([title, content]) => {
+ // question+/answer pairs
+ const qa = (content || '').split(/\n(### [^\n]*)\n/g)
+ const introduction = qa.shift().trim()
+
+ return {
+ title,
+ introduction,
+ paragraphs: qa.reduce(chunkReducer, [])
+ }
+ })
+ console.debug('[Faq] useGetRawContents', error, sections)
+
+ return (
+
+
+
+ Faq
+
+
+ {sections.map((section, i) => (
+
+
+
+
+
+
+ {section.paragraphs.map((p,j) => (
+
+
+
+
+ ))}
+
+
+ ))}
+
+ )
+}
+
+export default Faq
diff --git a/src/pages/FingerprintExplained.js b/src/pages/FingerprintExplained.js
new file mode 100644
index 00000000..7a44325e
--- /dev/null
+++ b/src/pages/FingerprintExplained.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import { Container, Row, Col } from 'react-bootstrap'
+import { BootstrapColumLayout } from '../constants'
+
+
+const FingerprintExplained = () => {
+ return (
+
+
+
+ Fingerprint, explained
+
+
+
+ )
+}
+
+export default FingerprintExplained
diff --git a/src/pages/FingerprintViewer.js b/src/pages/FingerprintViewer.js
new file mode 100644
index 00000000..b9c44b03
--- /dev/null
+++ b/src/pages/FingerprintViewer.js
@@ -0,0 +1,183 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Container, Row, Col, Form, Button } from 'react-bootstrap'
+import { BootstrapColumLayout } from '../constants'
+import { useQueryParam, StringParam, withDefault } from 'use-query-params'
+import { useGetJSON } from '../logic/api/fetchData'
+import { StatusSuccess, StatusFetching } from '../constants'
+import { useSpring, a, animated, config } from 'react-spring'
+import { parseNotebook } from '../logic/fingerprint'
+import { useBoundingClientRect } from '../hooks/graphics'
+import ArticleFingerprint from '../components/Article/ArticleFingerprint'
+
+import ErrorViewer from './ErrorViewer'
+
+
+
+const FingerprintLoader = ({ url, delay=0 }) => {
+ 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://"
+ />
+
+
+ Preview Fingerprint
+
+
+
+
+ )
+}
+
+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/pages/NotebookViewer.js b/src/pages/NotebookViewer.js
index 29b8019d..fbc52e7d 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
@@ -30,8 +28,12 @@ const NotebookViewer = ({
doi,
bibjson,
pid,
+ isJavascriptTrusted
}) => {
- // const { t } = useTranslation()
+ const [{[ArticleVersionQueryParam]: version}] = useQueryParams({
+ [ArticleVersionQueryParam]: withDefault(NumberParam, 2)
+ })
+ const ArticleComponent = version === 2 ? ArticleV2 : Article
const [animatedProps, api] = useSpring(() => ({ width : 0, opacity:1, config: config.slow }))
const url = useMemo(() => {
@@ -69,11 +71,7 @@ const NotebookViewer = ({
// console.info('Notebook render:', url ,'from', encodedUrl, status)
return (
<>
-
+
`${x}%`),
opacity: animatedProps.opacity
@@ -84,7 +82,7 @@ const NotebookViewer = ({
{status === StatusSuccess
? (
-
)
: (
diff --git a/src/pages/ReleaseNotes.js b/src/pages/ReleaseNotes.js
new file mode 100644
index 00000000..ab052d4d
--- /dev/null
+++ b/src/pages/ReleaseNotes.js
@@ -0,0 +1,81 @@
+import React from 'react'
+import { Container, Row, Col } from 'react-bootstrap'
+import { BootstrapColumLayout, StatusFetching } from '../constants'
+import { useGetJSON } from '../logic/api/fetchData'
+import { useTranslation } from 'react-i18next'
+
+const FakeReleases = [
+ {
+ name: '*',
+ published_at: new Date(),
+ body: '',
+ html_url: '',
+ tag_name:'v0.0.0'
+ },
+ {
+ name: '*',
+ published_at: new Date(),
+ body: '',
+ html_url: '',
+ tag_name:'v0.0.0'
+ }
+]
+/**
+ * ReleaseNotes page.
+ * @component
+ * @example
+ * return (
+ *
+ * )
+ */
+const ReleaseNotes = () => {
+ const { t } = useTranslation()
+ // load release notes from github
+ const { data:releases, status } = useGetJSON({
+ url: process.env.REACT_APP_GITHUB_RELEASES_API_ENDPOINT,
+ delay: 100,
+ })
+
+ return (
+ <>
+
+
+
+ Release notes
+ {Array.isArray(releases) ? releases.length : '...'} releases so far.
+
+
+ {(Array.isArray(releases) ? releases : FakeReleases).map((release, i) => (
+
+
+ {release.name}
+ {release.tag_name}
+
+ {t('dates.LLLL', { date: new Date(release.published_at)})}
+ —
+ {t('dates.fromNow', { date: new Date(release.published_at)})}
+
+ {release.body}
+
+
+ ))}
+ {status === StatusFetching && (
+
+
+
+
+ loading
+
+ )}
+
+ {/*
+
+ {JSON.stringify(releases, null, 2)}
+ {status} === {StatusSuccess}
+
+ */}
+ >
+ )
+}
+
+export default ReleaseNotes
diff --git a/src/store.js b/src/store.js
index e6b19eac..36e700ba 100644
--- a/src/store.js
+++ b/src/store.js
@@ -8,6 +8,26 @@ export const useIssueStore = create((set) => ({
setIssue: (issue) => set(() => ({ issue }))
}))
+export const useArticleToCStore = create((set) => ({
+ visibleCellsIdx: [],
+ clearVisibleCellsIdx: () => 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 }
+ }),
+ setVisibleCellsIdx: (visibleCellsIdx=[]) => set(() => ({
+ visibleCellsIdx: [...visibleCellsIdx]
+ }))
+}))
+
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..01874d91 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,11 @@ $breakpoint-lg: 992px;
top: 14px;
}
&.level_H3, &.level_H4, &.level_H5, &.level_H6{
- top: 21px;
+ top: 20px;
+ }
+ &.selectable{
+ cursor: pointer;
+ text-decoration: underline;
}
}
@@ -641,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);
@@ -711,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/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;
+ }
+}
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/styles/graphics.scss b/src/styles/graphics.scss
index c4e00c63..ebdc371b 100644
--- a/src/styles/graphics.scss
+++ b/src/styles/graphics.scss
@@ -20,6 +20,11 @@
transition: opacity 2s ease-in-out;
}
+.NotebookViewer_loadingWrapper{
+ z-index: 10;
+
+}
+
.NotebookViewer_loadingPercentage {
transition: opacity 2s ease-in-out;
margin-left: 0px;
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 123ab58a..62e3a0dc 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);
@@ -456,4 +462,21 @@ 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{
+ left: 0
+}
+
+.right-0{
+ right: 0
}
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": {
diff --git a/yarn.lock b/yarn.lock
index 6b3c5f85..d99c54f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8148,10 +8148,10 @@ markdown-it-replace-link@^1.1.0:
resolved "https://registry.yarnpkg.com/markdown-it-replace-link/-/markdown-it-replace-link-1.1.0.tgz#cab2343eb27928db1c836e10cd518aaf60a0bd50"
integrity sha512-P+4D/Z16utgQVjMumA5W3lMbxWYk4iwg1Fk59wShFm7MRgzbjJm++tvI18LdYwiG8CKNA1TFWvHcbtehvMuViA==
-markdown-it@^12.0.0:
- version "12.0.6"
- resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.6.tgz#adcc8e5fe020af292ccbdf161fe84f1961516138"
- integrity sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==
+markdown-it@^12.3.2:
+ version "12.3.2"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
+ integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
dependencies:
argparse "^2.0.1"
entities "~2.1.0"