diff --git a/.storybook/addons.js b/.storybook/addons.js new file mode 100644 index 0000000..6aed412 --- /dev/null +++ b/.storybook/addons.js @@ -0,0 +1,2 @@ +import '@storybook/addon-actions/register'; +import '@storybook/addon-links/register'; diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 0000000..639b168 --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,9 @@ +import { configure } from '@storybook/react'; + +// automatically import all files ending in *.stories.js +const req = require.context('../stories', true, /\.stories\.js$/); +function loadStories() { + req.keys().forEach(filename => req(filename)); +} + +configure(loadStories, module); diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..433721a --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,320 @@ + + + + + + \ No newline at end of file diff --git a/stories/ContextProvider.js b/stories/ContextProvider.js new file mode 100644 index 0000000..5535561 --- /dev/null +++ b/stories/ContextProvider.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react';/* eslint no-unused-vars : 0 */ +import PropTypes from 'prop-types'; + +export default class ContextProvider extends Component { + + static childContextTypes = { + renderingMode: PropTypes.string, + } + + getChildContext = () => ( { + renderingMode: this.props.renderingMode, + } ) + render = () => { + return this.props.children; + } +} diff --git a/stories/PagedPreviewer/PagedPreviewer.js b/stories/PagedPreviewer/PagedPreviewer.js new file mode 100755 index 0000000..c5fce4a --- /dev/null +++ b/stories/PagedPreviewer/PagedPreviewer.js @@ -0,0 +1,127 @@ +/** + * This module provides a preview for paged content, thanks to the awesome pagedjs polyfill + * @module ovide/components/PagedPreviewer + * @todo find a way to do the same thing while having better manners + */ +/** + * Imports Libraries + */ +import React, { Component } from 'react'; +import Frame, { FrameContextConsumer } from 'react-frame-component'; +import fetch from 'axios'; + +/** + * Imports Dependencies + */ +import addons from 'raw-loader!./addons.paged.jx'; +import previewStyleData from 'raw-loader!./previewStyle.paged.csx'; + +class PreviewWrapper extends Component { + + constructor( props ) { + super( props ); + this.state = { + }; + } + + componentDidMount = () => { + fetch( 'https://unpkg.com/pagedjs@0.1.30/dist/paged.polyfill.js' ) + .then( ( { data } ) => { + this.setState( { + pagedScript: data + } ); + } ); + } + + shouldComponentUpdate = ( { updateTrigger } ) => { + return updateTrigger !== this.props.updateTrigger; + } + + render = () => { + + const { + props: { + style, + Component: RenderingComponent, + additionalHTML = '', + } + } = this; + + const injectRenderer = ( thatDocument ) => { + // 1. swap body content to have sections as direct children (paged js requirement) + let htmlContent = thatDocument.body.children[0].querySelector( '.frame-content > div' ).innerHTML; + thatDocument.body.innerHTML = htmlContent; + // 2. extract styles from content + const stylesRegexp = /([\w\W\n]*)<\/style>/gm; + let additionalStyles = ''; + let match; + while ( ( match = stylesRegexp.exec( htmlContent ) ) !== null ) { + additionalStyles += match[1]; + htmlContent = htmlContent.slice( 0, match.index ) + htmlContent.slice( match.index + match[0].length ); + match.index = -1; + } + // add toaster lib + const toasterScript = thatDocument.createElement( 'script' ); + toasterScript.type = 'text/javascript'; + toasterScript.src = 'https://cdn.jsdelivr.net/npm/toastify-js'; + thatDocument.getElementsByTagName( 'head' )[0].appendChild( toasterScript ); + const toasterStyle = thatDocument.createElement( 'link' ); + toasterStyle.rel = 'stylesheet'; + toasterStyle.type = 'text/css'; + toasterStyle.href = 'https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css'; + thatDocument.getElementsByTagName( 'head' )[0].appendChild( toasterStyle ); + + // load paged js lib + const pagedLibScript = thatDocument.createElement( 'script' ); + pagedLibScript.type = 'text/javascript'; + pagedLibScript.innerHTML = this.state.pagedScript || ''; + // pagedLibScript.src = 'https://unpkg.com/pagedjs@0.1.30/dist/paged.polyfill.js'; + thatDocument.getElementsByTagName( 'head' )[0].appendChild( pagedLibScript ); + // load addons script + const addonsScript = thatDocument.createElement( 'script' ); + addonsScript.type = 'text/javascript'; + addonsScript.innerHTML = addons; + thatDocument.getElementsByTagName( 'head' )[0].appendChild( addonsScript ); + // load paged js preview + const previewStyle = thatDocument.createElement( 'style' ); + previewStyle.innerHTML = `${previewStyleData} + ${additionalStyles}`; + thatDocument.getElementsByTagName( 'head' )[0].appendChild( previewStyle ); + + const additionalHTMLElement = thatDocument.createElement( 'div' ); + additionalHTMLElement.innerHTML = additionalHTML; + thatDocument.getElementsByTagName( 'body' )[0].appendChild( additionalHTMLElement ); + }; + + return ( +
+ {/* */} + + + + {( { document, window } ) => ( +
+ + { + setTimeout( () => injectRenderer( document ) ) + } +
+ )} +
+ +
+ ); + } +} + +export default PreviewWrapper; + diff --git a/stories/PagedPreviewer/addons.paged.jx b/stories/PagedPreviewer/addons.paged.jx new file mode 100644 index 0000000..182de35 --- /dev/null +++ b/stories/PagedPreviewer/addons.paged.jx @@ -0,0 +1,214 @@ +/* eslint-disable */ + +/** + * DOM observer use to retrigger the resize script when changing content in the page + */ +const observeDOM = ( function() { + const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; + + return function( obj, callback ) { + if ( !obj || !obj.nodeType === 1 ) return; // validation + + if ( MutationObserver ) { + // define a new observer + const obs = new MutationObserver( function( mutations, observer ) { + callback( mutations ); + } ); + // have the observer observe foo for changes in children + obs.observe( obj, { childList: true, subtree: true } ); + } + + else if ( window.addEventListener ) { + obj.addEventListener( 'DOMNodeInserted', callback, false ); + obj.addEventListener( 'DOMNodeRemoved', callback, false ); + } + }; + } )(); + +if ( window.Paged ) { + + /* + * Hooks for paged.js + * footnotes support courtesy of RLesur https://github.com/rstudio/pagedown/blob/master/inst/resources/js/hooks.js + * Footnotes support + */ + Paged.registerHandlers( class extends Paged.Handler { + constructor( chunker, polisher, caller ) { + super( chunker, polisher, caller ); + + this.splittedParagraphRefs = []; + console.log( 'in constructor' ); + } + + beforeParsed( content ) { + console.info( 'spotting footnotes' ); + const footnotes = content.querySelectorAll( '.footnote' ); + + for ( const footnote of footnotes ) { + const parentElement = footnote.parentElement; + const footnoteCall = document.createElement( 'a' ); + const footnoteNumber = footnote.dataset.notenumber; + + footnoteCall.className = 'footnote-ref'; // same class as Pandoc + footnoteCall.setAttribute( 'id', `fnref${ footnoteNumber}` ); // same notation as Pandoc + footnoteCall.setAttribute( 'href', `#${ footnote.id}` ); + footnoteCall.innerHTML = `${ footnoteNumber }`; + parentElement.insertBefore( footnoteCall, footnote ); + + // Here comes a hack. Fortunately, it works with Chrome and FF. + const handler = document.createElement( 'p' ); + handler.className = 'footnoteHandler'; + parentElement.insertBefore( handler, footnote ); + handler.appendChild( footnote ); + handler.style.display = 'inline-block'; + handler.style.width = '100%'; + handler.style.float = 'right'; + handler.style.pageBreakInside = 'avoid'; + } + } + + afterParsed() { + console.info( 'parsing finished, rendering the pages' ); + console.group( 'rendering pages' ); + Toastify( { + text: 'Rendering pages', + duration: 2000 + } ).showToast(); + } + + afterPageLayout( pageFragment, page, breakToken ) { + console.info( 'page %s is rendered', page.position + 1 ); + if (page.position%20 === 0 && page.position) { + Toastify( { + text: 'Rendering pages : ' + (page.position) + '/?', + duration: 1000 + } ).showToast(); + } + + function hasItemParent( node ) { + if ( node.parentElement === null ) { + return false; + } + else { + if ( node.parentElement.tagName === 'LI' ) { + return true; + } + else { + return hasItemParent( node.parentElement ); + } + } + } + + /* + * If a li item is broken, we store the reference of the p child element + * see https://github.com/rstudio/pagedown/issues/23#issue-376548000 + */ + if ( breakToken !== undefined ) { + if ( breakToken.node.nodeName === '#text' && hasItemParent( breakToken.node ) ) { + this.splittedParagraphRefs.push( breakToken.node.parentElement.dataset.ref ); + } + } + } + + afterRendered( pages ) { + console.groupEnd( 'rendering pages' ); + Toastify( { + text: `Attaching footnotes to ${ pages.length } pages`, + duration: 1000 + } ).showToast(); + console.info( 'rendering done, attaching footnotes to %s pages', pages.length ); + for ( const page of pages ) { + const footnotes = page.element.querySelectorAll( '.footnote' ); + if ( footnotes.length === 0 ) { + continue; + } + + const pageContent = page.element.querySelector( '.pagedjs_page_content' ); + const hr = document.createElement( 'hr' ); + const footnoteArea = document.createElement( 'div' ); + + pageContent.style.display = 'flex'; + pageContent.style.flexDirection = 'column'; + + hr.className = 'footnote-break'; + hr.style.marginTop = 'auto'; + hr.style.marginBottom = 0; + hr.style.marginLeft = 0; + hr.style.marginRight = 'auto'; + pageContent.appendChild( hr ); + + footnoteArea.className = 'footnote-area'; + pageContent.appendChild( footnoteArea ); + let footnoteIndex = 0; + for ( const footnote of footnotes ) { + footnoteIndex++; + const handler = footnote.parentElement; + + footnoteArea.appendChild( footnote ); + handler.parentNode.removeChild( handler ); + + const footnoteCall = document.getElementById( `note-content-pointer-${ footnote.id}` ); + if ( footnoteCall ) { + footnoteCall.innerHTML = `${ footnoteIndex }`; + } + + footnote.innerHTML = `${ footnoteIndex }${ footnote.innerHTML}`; + // footnote.style.fontSize = 'x-small'; + footnote.style.marginTop = 0; + footnote.style.marginBottom = 0; + footnote.style.paddingTop = 0; + footnote.style.paddingBottom = 0; + footnote.style.display = 'block'; + } + } + + for ( const ref of this.splittedParagraphRefs ) { + const paragraphFirstPage = document.querySelector( `[data-split-to="${ ref }"]` ); + + /* + * We test whether the paragraph is empty + * see https://github.com/rstudio/pagedown/issues/23#issue-376548000 + */ + if ( paragraphFirstPage.innerText === '' ) { + paragraphFirstPage.parentElement.style.display = 'none'; + const paragraphSecondPage = document.querySelector( `[data-split-from="${ ref }"]` ); + paragraphSecondPage.parentElement.style.setProperty( 'list-style', 'inherit', 'important' ); + } + } + console.info( 'footnotes positionning done' ); + Toastify( { + text: 'Rendering finished !', + duration: 3000 + } ).showToast(); + + } + } );/* end register handlers */ + // resize logic + const paged = new window.Paged.Previewer(); + const resizer = () => { + const pages = document.querySelector( '.pagedjs_pages' ); + + if ( pages ) { + const scale = ( ( window.innerWidth * 0.9 ) / pages.offsetWidth ); + if ( scale < 1 ) { + const translateVal = ( window.innerWidth / 2 ) - ( ( pages.offsetWidth * scale / 2 ) ); + const style = `scale(${ scale }) translate(${ translateVal }px, 0)`; + pages.style.transform = style; + } + else { + pages.style.transform = 'none'; + } + } + }; + resizer(); + + window.addEventListener( 'resize', resizer, false ); + + paged.on( 'rendering', () => { + console.log( 'paged is rendering' ); + resizer(); + } ); + + observeDOM( document.body, resizer ); + + } diff --git a/stories/PagedPreviewer/index.js b/stories/PagedPreviewer/index.js new file mode 100755 index 0000000..a0cfe97 --- /dev/null +++ b/stories/PagedPreviewer/index.js @@ -0,0 +1,3 @@ +import PagedPreviewer from './PagedPreviewer'; + +export default PagedPreviewer; diff --git a/stories/PagedPreviewer/previewStyle.paged.csx b/stories/PagedPreviewer/previewStyle.paged.csx new file mode 100644 index 0000000..c90da90 --- /dev/null +++ b/stories/PagedPreviewer/previewStyle.paged.csx @@ -0,0 +1,79 @@ +:root { + --color-mbox : rgba(0,0,0,0.2); + --margin: 4px; + } + + [contenteditable]:focus { + outline: 0px solid transparent; + } + + #controls { + display: none; + } + + @media screen { + + body { + background-color: whitesmoke; + } + + .pagedjs_pages{ + transform: translate(0.5); + } + + .pagedjs_page { + background-color: #fdfdfd; + margin: calc(var(--margin) * 4) var(--margin); + flex: none; + box-shadow: 0 0 0 1px var(--color-mbox); + pointer-events: none; + } + + .pagedjs_pages { + width: calc((var(--width) * 2) + (var(--margin) * 4)); + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + transform-origin: 0 0; + margin: 0 auto; + } + + #controls { + margin: 20px 0; + text-align: center; + display: block; + } + + .pagedjs_first_page { + margin-left: calc(50% + var(--margin)); + } + } + + @media screen { + .debug .pagedjs_margin-top .pagedjs_margin-top-left-corner, + .debug .pagedjs_margin-top .pagedjs_margin-top-right-corner { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + + .debug .pagedjs_margin-top > div { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + + .debug .pagedjs_margin-right > div { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + + .debug .pagedjs_margin-bottom .pagedjs_margin-bottom-left-corner, + .debug .pagedjs_margin-bottom .pagedjs_margin-bottom-right-corner { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + + .debug .pagedjs_margin-bottom > div { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + + .debug .pagedjs_margin-left > div { + box-shadow: 0 0 0 1px inset var(--color-mbox); + } + } \ No newline at end of file diff --git a/stories/assets/mocks.js b/stories/assets/mocks.js new file mode 100644 index 0000000..187326b --- /dev/null +++ b/stories/assets/mocks.js @@ -0,0 +1,118 @@ +export const printEdition = { + data: { + additionalHTML: '', + bibType: 'book', + citationLocale: { + data: ' Grégoire Colly This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License 2012-07-04T23:31:02+00:00 consulté le et et autres anonyme anon. sur disponible sur par vers v. cité édition éditions éd. et al. à paraître à l\'adresse ibid. in sous presse Internet entretien lettre sans date s. d. en ligne présenté à référence références réf. réf. consulté échelle version apr. J.-C. av. J.-C. «   » ʳᵉ ᵉʳ premier deuxième troisième quatrième cinquième sixième septième huitième neuvième dixième livre livres chapitre chapitres colonne colonnes figure figures folio folios numéro numéros ligne lignes note notes opus opus page pages page pages paragraphe paragraphes partie parties section sections sub verbo sub verbis verset versets volume volumes liv. chap. col. fig. fᵒ fᵒˢ nᵒ nᵒˢ l. n. op. p. p. p. p. paragr. part. sect. s. v. s. vv. v. v. vol. vol. § § § § réalisateur réalisateurs éditeur éditeurs directeur directeurs illustrateur illustrateurs traducteur traducteurs éditeur et traducteur éditeurs et traducteurs réal. réal. éd. éd. dir. dir. ill. ill. trad. trad. éd. et trad. éd. et trad. par réalisé par édité par sous la direction de illustré par entretien réalisé par à par traduit par édité et traduit par réal. par éd. par ss la dir. de ill. par trad. par éd. et trad. par janvier février mars avril mai juin juillet août septembre octobre novembre décembre janv. févr. mars avr. mai juin juill. août sept. oct. nov. déc. printemps été automne hiver ', + id: 'fr-FR', + names: [ + 'Français (France)', + 'French (France)' + ] + }, + citationStyle: { + data: ' ', + id: 'apa-5th-edition', + title: 'American Psychological Association 5th edition' + }, + plan: { + summary: [ + { + data: { + animatedBackground: 'none', + backgroundColor: '#466CA6' + }, + id: 'a6ca685b-b16e-4232-b882-447b4788401e', + type: 'frontCover' + }, + { + id: 'bca30c0e-1dc3-446f-ba48-c3dca72349aa', + type: 'titlePage' + }, + { + data: { + notesPosition: 'footnotes' + }, + id: 'e9ed4a17-2691-4c3a-97fc-ac5bffdd5aff', + type: 'sections' + }, + { + data: { + resourceTypes: [ + 'bib' + ], + showMentions: false, + showUncitedReferences: true, + sortingAscending: true, + sortingKey: 'date' + }, + id: 'c0400061-4bd3-4de7-a3d9-7db519235f79', + type: 'references' + }, + { + data: { + backgroundColor: '#D6CFC4', + useAbstract: true + }, + id: 'dbf00f43-72d6-4966-ad05-6eaa8176627b', + type: 'backCover' + } + ], + type: 'linear' + }, + style: { + css: '.image{\n\tpage-break-before: always;\n\tpage-break-after: always;\n width: 100%;\n max-height: 100%;\n}', + mode: 'merge' + } + }, + id: '37b8b4b9-9bb9-4021-8e08-903b4c6e4fa3', + lastUpdateAt: 1555609018359, + metadata: { + templateId: 'pyrrah', + title: 'paginé', + type: 'paged' + } +}; + +export const editionTypes = { + 'frontCover': { + data: { + animatedBackground: 'none', + backgroundColor: '#466CA6' + }, + id: 'a6ca685b-b16e-4232-b882-447b4788401e', + type: 'frontCover' + }, + 'titlePage': { + id: 'bca30c0e-1dc3-446f-ba48-c3dca72349aa', + type: 'titlePage' + }, + 'sections': { + data: { + notesPosition: 'footnotes' + }, + id: 'e9ed4a17-2691-4c3a-97fc-ac5bffdd5aff', + type: 'sections' + }, + 'references': { + data: { + resourceTypes: [ + 'bib' + ], + showMentions: false, + showUncitedReferences: true, + sortingAscending: true, + sortingKey: 'date' + }, + id: 'c0400061-4bd3-4de7-a3d9-7db519235f79', + type: 'references' + }, + 'backCover': { + data: { + backgroundColor: '#D6CFC4', + useAbstract: true + }, + id: 'dbf00f43-72d6-4966-ad05-6eaa8176627b', + type: 'backCover' + } +}; diff --git a/stories/index.stories.js b/stories/index.stories.js new file mode 100644 index 0000000..c31ef36 --- /dev/null +++ b/stories/index.stories.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import {preprocessEditionData} from 'peritext-utils' + +import ContextProvider from './ContextProvider'; + +import template from '../src'; +import production from './assets/production.json'; +import { printEdition, editionTypes } from './assets/mocks'; + +const { + components: { + Edition + } +} = template; + +const contextualizers = { + bib: require( 'peritext-contextualizer-bib' ), + webpage: require( 'peritext-contextualizer-webpage' ), + glossary: require( 'peritext-contextualizer-glossary' ), + embed: require( 'peritext-contextualizer-embed' ), + video: require( 'peritext-contextualizer-video' ), + image: require( 'peritext-contextualizer-image' ), + sourceCode: require( 'peritext-contextualizer-source-code' ), + vegaLite: require( 'peritext-contextualizer-vegalite' ), + table: require( 'peritext-contextualizer-table' ), +}; + +const extractSpecificView = ( viewType ) => { + + const newEdition = { + ...printEdition, + data: { + ...printEdition.data, + plan: { + ...printEdition.data.plan, + summary: [ editionTypes[viewType] ] + } + } + }; + + const thatPrepro = preprocessEditionData( { + production, + edition: newEdition + } ); + return { + edition: newEdition, + preprocessedData: thatPrepro + }; +}; + +const renderWithEdition = ( { edition: thatEdition, preprocessedData: thatPrepro } ) => ( + <> +
+ + + +
+
+ + +); + +storiesOf( 'Template', module ) + .add( 'complete edition', () => renderWithEdition( printEdition ) ) + .add( 'sections', () => renderWithEdition( extractSpecificView( 'sections' ) ) ) + .add( 'front page', () => renderWithEdition( extractSpecificView( 'frontCover' ) ) ) + .add( 'title page', () => renderWithEdition( extractSpecificView( 'titlePage' ) ) ) + .add( 'references', () => renderWithEdition( extractSpecificView( 'references' ) ) ) + // .add( 'glossary', () => renderWithEdition( extractSpecificView( 'glossary' ) ) ) + .add( 'back cover', () => renderWithEdition( extractSpecificView( 'backCover' ) ) )