├── .gitignore ├── LICENSE.md ├── README.md ├── _index.html ├── components ├── card.js ├── cardVis.js ├── custom-component.js ├── default │ ├── action.js │ ├── analytics.js │ ├── aside.js │ ├── boolean.js │ ├── button.js │ ├── chart.js │ ├── code-highlight.js │ ├── display.js │ ├── dynamic.js │ ├── equation.js │ ├── feature.js │ ├── fixed.js │ ├── float.js │ ├── full-screen.js │ ├── gist.js │ ├── graphic.js │ ├── header.js │ ├── index.js │ ├── inline.js │ ├── link.js │ ├── panel.js │ ├── preload.js │ ├── radio.js │ ├── range.js │ ├── scroller.js │ ├── select.js │ ├── slide.js │ ├── slideshow.js │ ├── step.js │ ├── stepper-control.js │ ├── stepper.js │ ├── svg.js │ ├── table.js │ ├── text-container.js │ ├── text-input.js │ ├── utils │ │ ├── container.js │ │ └── screen.js │ └── waypoint.js ├── image.js ├── multiRiffle.js └── positionChart.js ├── index.idyll ├── package.json ├── static └── images │ ├── riffle.gif │ ├── share.png │ └── smoosh.gif ├── styles.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2019 Fred Hohman. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Math of Shuffling Cards 2 | *Riffling from factory order to complete randomness.* 3 | 4 | Code for [interactive article on card shuffling](http://fredhohman.com/card-shuffling/). 5 | 6 | Written using [Idyll](https://idyll-lang.org/). 7 | -------------------------------------------------------------------------------- /_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{#title}} 6 | 7 | {{title}} 8 | 9 | 10 | {{/title}} 11 | {{^title}} 12 | Idyll 13 | 14 | {{/title}} 15 | 16 | {{#shareImageUrl}} 17 | 18 | 19 | 20 | {{/shareImageUrl}} 21 | {{#shareImageWidth}} 22 | 23 | {{/shareImageWidth}} 24 | {{#shareImageHeight}} 25 | 26 | {{/shareImageHeight}} 27 | 28 | 29 | {{#description}} 30 | 31 | 32 | {{/description}} 33 | {{#url}} 34 | 35 | {{/url}} 36 | {{#twitterHandle}} 37 | 38 | {{/twitterHandle}} 39 | {{#usesTex}} 40 | 41 | {{/usesTex}} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
{{{idyllContent}}}
50 | 51 | 52 | -------------------------------------------------------------------------------- /components/card.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class card extends React.Component { 4 | render() { 5 | 6 | const { idyll, hasError, updateProps, ...props } = this.props; 7 | let spanStyle = { fontWeight: '700' }; 8 | const suits = { 9 | "S": "♠", 10 | "C": "♣", 11 | "H": "♥", 12 | "D": "♦" 13 | }; 14 | 15 | if ((this.props.suit === "H") || (this.props.suit === "D")) { 16 | spanStyle.color = '#f44336'; 17 | } else { 18 | spanStyle.color = 'black'; 19 | } 20 | 21 | let cardToRender; 22 | if (this.props.number) { 23 | cardToRender = this.props.number + suits[this.props.suit] 24 | } else { 25 | cardToRender = suits[this.props.suit] 26 | } 27 | 28 | return ( 29 | 30 | {cardToRender} 31 | 32 | ); 33 | } 34 | } 35 | 36 | module.exports = card; 37 | -------------------------------------------------------------------------------- /components/cardVis.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const D3Component = require('idyll-d3-component'); 3 | const d3 = require('d3'); 4 | 5 | const size = 650; 6 | const height = 300; 7 | const cardWidthMultiplier = 2.5; 8 | const cardHeightMultiplier = 3.5; 9 | const cardSize = 15; 10 | 11 | const cardPrimitives = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; 12 | const suits = ["♠", "♣", "♥", "♦"]; 13 | let propsUpdated = false; 14 | 15 | function makeCards(suits, cardPrimitives) { 16 | let cards = []; 17 | for (let i = 0; i < suits.length; i++) { 18 | for (let j = 0; j < cardPrimitives.length; j++) { 19 | cards.push(cardPrimitives[j] + suits[i]); 20 | } 21 | } 22 | return cards; 23 | } 24 | 25 | let cards = makeCards(suits, cardPrimitives); 26 | 27 | class cardVis extends D3Component { 28 | 29 | initialize(node, props) { 30 | 31 | const svg = this.svg = d3.select(node).append('svg'); 32 | svg.attr('viewBox', `0 0 ${size} ${height}`) 33 | .style('width', '100%') 34 | .style('height', 'auto'); 35 | 36 | svg.selectAll('rect') 37 | .data(cards) 38 | .enter() 39 | .append('rect') 40 | .attr('class', 'card') 41 | .attr('x', function (d, i) { 42 | return 40 + i % 13 * 45 - 10; 43 | }) 44 | .attr('y', function (d, i) { 45 | if (i < (cards.length - 1) * (1 / 4)) { 46 | return height * (1 / 5) - 16; 47 | } 48 | if (i > (cards.length - 1) * (1 / 4) && i < (cards.length - 1) * (2 / 4)) { 49 | return height * (2 / 5) - 16; 50 | } 51 | if (i > (cards.length - 1) * (2 / 4) && i < (cards.length - 1) * (3 / 4)) { 52 | return height * (3 / 5) - 16; 53 | } 54 | if (i > (cards.length - 1) * (3 / 4) && i <= (cards.length - 1) * (4 / 4)) { 55 | return height * (4 / 5) - 16; 56 | } 57 | }) 58 | .attr('width', cardSize * cardWidthMultiplier) 59 | .attr('height', cardSize * cardHeightMultiplier) 60 | .attr('fill', function (d) { 61 | if (props.static === "True") { 62 | return '#FFFFFF' 63 | } else { 64 | if (d === 'K♦') { 65 | return '#f44336'; 66 | } else { 67 | return '#FFFFFF'; 68 | } 69 | } 70 | }) 71 | .attr('rx', 3) 72 | .attr('ry', 3) 73 | .attr('stroke', '#444444') 74 | .attr('stroke-width', 1) 75 | // .on('mouseover', function(d) { d3.select(this).attr('fill', '#F0F0F0'); }) 76 | // .on('mouseout', function(d) { d3.select(this).attr('fill', '#FFFFFF'); }); 77 | 78 | svg.selectAll('text') 79 | .data(cards) 80 | .enter() 81 | .append('text') 82 | .attr('class', 'card-text') 83 | .attr('x', function (d, i) { return 35 + i % 13 * 45; }) 84 | .attr('y', function (d, i) { 85 | if (i < (cards.length - 1) * (1 / 4)) { 86 | return height * (1 / 5); 87 | } 88 | if (i > (cards.length - 1) * (1 / 4) && i < (cards.length - 1) * (2 / 4)) { 89 | return height * (2 / 5); 90 | } 91 | if (i > (cards.length - 1) * (2 / 4) && i < (cards.length - 1) * (3 / 4)) { 92 | return height * (3 / 5); 93 | } 94 | if (i > (cards.length - 1) * (3 / 4) && i <= (cards.length - 1) * (4 / 4)) { 95 | return height * (4 / 5); 96 | } 97 | }) 98 | // .attr('x', function(d,i) { return 20 + i*20 }) 99 | .text(function (d) { return d; }) 100 | .attr('text-anchor', 'start') 101 | .attr('fill', function (d) { 102 | if (props.static === "True") { 103 | if ((d[d.length - 1] === suits[0]) || (d[d.length - 1] === suits[1])) { 104 | return 'black'; 105 | } 106 | if ((d[d.length - 1] === suits[2]) || (d[d.length - 1] === suits[3])) { 107 | return '#f44336'; 108 | } 109 | } else { 110 | if (d === 'K♦') { 111 | return '#FFFFFF'; 112 | } 113 | if ((d[d.length - 1] === suits[0]) || (d[d.length - 1] === suits[1])) { 114 | return 'black'; 115 | } 116 | if ((d[d.length - 1] === suits[2]) || (d[d.length - 1] === suits[3])) { 117 | return '#f44336'; 118 | } 119 | } 120 | }) 121 | .style('font-size', '14px') 122 | .style('font-weight', 700); 123 | } 124 | 125 | update(props) { 126 | 127 | if (propsUpdated === false) { 128 | 129 | if (props.iterVar === 0) { 130 | cards = makeCards(suits, cardPrimitives); 131 | // console.log(cards) 132 | } 133 | 134 | let lastPoint = props.points[props.points.length - 1]; 135 | 136 | if (lastPoint.y !== 1) { 137 | propsUpdated = true; 138 | 139 | // console.log('riffle', props.iterVar); 140 | 141 | function riffle(cards) { 142 | let randCardIndex = Math.floor(Math.random() * cards.length); 143 | let topCard = cards.shift(); 144 | cards.splice(randCardIndex, 0, topCard); 145 | return cards; 146 | } 147 | cards = riffle(cards); 148 | 149 | this.svg.selectAll('.card') 150 | .data(cards) 151 | .attr('fill', function (d) { 152 | if (d === 'K♦') { 153 | return '#f44336'; 154 | } else { 155 | return '#FFFFFF'; 156 | } 157 | }); 158 | 159 | this.svg.selectAll('.card-text') 160 | .data(cards) 161 | .text(function (d) { return d; }) 162 | .attr('fill', function (d) { 163 | if (d === 'K♦') { 164 | return '#FFFFFF'; 165 | } 166 | if ((d[d.length - 1] === suits[0]) || (d[d.length - 1] === suits[1])) { 167 | return 'black'; 168 | } 169 | if ((d[d.length - 1] === suits[2]) || (d[d.length - 1] === suits[3])) { 170 | return '#f44336'; 171 | } 172 | }); 173 | 174 | const newXValue = props.iterVar; 175 | const newYValue = cards.indexOf('K♦') + 1; 176 | // console.log('updateprops'); 177 | 178 | // Make sure you put this code in a conditional 179 | // so that it doesn't loop infinitely 180 | props.updateProps({ 181 | points: props.points.concat([{ 182 | x: newXValue, 183 | y: newYValue 184 | }]) 185 | }); 186 | } 187 | } else { 188 | propsUpdated = false; 189 | } 190 | } 191 | } 192 | 193 | module.exports = cardVis; 194 | -------------------------------------------------------------------------------- /components/custom-component.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class CustomComponent extends React.Component { 4 | render() { 5 | const { idyll, hasError, updateProps, ...props } = this.props; 6 | return ( 7 |
8 | This is a custom component 9 |
10 | ); 11 | } 12 | } 13 | 14 | module.exports = CustomComponent; 15 | -------------------------------------------------------------------------------- /components/default/action.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Action extends React.PureComponent { 4 | render() { 5 | return ( 6 | {this.props.children} 7 | ); 8 | } 9 | } 10 | 11 | Action._idyll = { 12 | name: "Action", 13 | tagType: "open", 14 | children: [ 15 | "action text" 16 | ], 17 | props: [{ 18 | name: "onClick", 19 | type: 'event', 20 | example: '`x = !x`' 21 | }, { 22 | name: "onMouseEnter", 23 | type: 'event', 24 | example: '`x = true`' 25 | }, { 26 | name: "onMouseLeave", 27 | type: 'event', 28 | example: '`x = false`' 29 | }] 30 | } 31 | 32 | export default Action; 33 | -------------------------------------------------------------------------------- /components/default/analytics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Analytics extends React.PureComponent { 4 | componentDidMount() { 5 | try { 6 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 7 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 8 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 9 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 10 | 11 | ga('create', this.props.google, 'auto'); 12 | 13 | window.ga('send', 'pageview', { 14 | tag: this.props.tag 15 | }); 16 | } catch(e) { console.log('Could not mount Analytics.'); } 17 | } 18 | 19 | render() { 20 | return null; 21 | } 22 | } 23 | 24 | 25 | Analytics._idyll = { 26 | name: "Analytics", 27 | tagType: "closed", 28 | props: [{ 29 | name: "google", 30 | type: 'string', 31 | example: '"UA-XXXXXXX"' 32 | }] 33 | } 34 | 35 | 36 | 37 | export default Analytics; 38 | -------------------------------------------------------------------------------- /components/default/aside.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Aside extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |
8 | {this.props.children} 9 |
10 |
11 | ); 12 | } 13 | } 14 | 15 | 16 | Aside._idyll = { 17 | name: "Aside", 18 | tagType: "open" 19 | } 20 | 21 | export default Aside; 22 | -------------------------------------------------------------------------------- /components/default/boolean.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Boolean extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | toggleCheckbox() { 9 | this.props.updateProps({ 10 | value: !this.props.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Boolean.defaultProps = { 23 | value: false 24 | }; 25 | 26 | 27 | Boolean._idyll = { 28 | name: "Boolean", 29 | tagType: "closed", 30 | props: [{ 31 | name: "value", 32 | type: "boolean", 33 | example: "x" 34 | }] 35 | } 36 | 37 | export default Boolean; 38 | -------------------------------------------------------------------------------- /components/default/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Button extends React.PureComponent { 4 | render() { 5 | const { onClick, hasError, updateProps, ...props } = this.props; 6 | return ( 7 | 10 | ); 11 | } 12 | } 13 | 14 | Button.defaultProps = { 15 | onClick: function() {} 16 | }; 17 | 18 | Button._idyll = { 19 | name: "Button", 20 | tagType: "open", 21 | children: ['Click Me.'], 22 | props: [{ 23 | name: "onClick", 24 | type: "event", 25 | example: "`x += 1`" 26 | }] 27 | } 28 | export default Button; 29 | -------------------------------------------------------------------------------- /components/default/chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const V = require('victory'); 3 | const d3Arr = require('d3-array'); 4 | 5 | const types = { 6 | AREA: V.VictoryArea, 7 | TIME: V.VictoryLine, 8 | LINE: V.VictoryLine, 9 | BAR: V.VictoryBar, 10 | SCATTER: V.VictoryScatter, 11 | PIE: V.VictoryPie 12 | }; 13 | 14 | let chartCount = 0; 15 | 16 | class Chart extends React.PureComponent { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.id = chartCount++; 21 | } 22 | 23 | render() { 24 | const { id, props } = this; 25 | const type = props.type.toUpperCase(); 26 | const INNER_CHART = types[type]; 27 | let { scale, data, domain, animate, ...customProps } = props; 28 | 29 | if (props.equation) { 30 | const d = domain; 31 | data = d3Arr.range(d[0], d[1], (d[1] - d[0]) / props.samplePoints).map((x) => { 32 | try { 33 | return { 34 | x: x, 35 | y: props.equation(x) 36 | }; 37 | } catch(err) { 38 | return { 39 | x: x, 40 | y: 0 41 | } 42 | } 43 | }); 44 | } 45 | 46 | if (type === types.TIME) { 47 | scale = {x: 'time', y: 'linear'}; 48 | data = data.map((d) => { 49 | return Object.assign({}, d, { 50 | x: new Date(d.x) 51 | }); 52 | }); 53 | } 54 | return ( 55 |
56 | {type !== 'PIE' ? ( 57 | 58 | 63 | 64 | 65 | ) : ( 66 | 67 | 68 | ) 69 | } 70 |
71 | ); 72 | } 73 | } 74 | 75 | Chart.defaultProps = { 76 | domain: [-1, 1], 77 | range: [-1, 1], 78 | domainPadding: 0, 79 | samplePoints: 100, 80 | type: 'line' 81 | }; 82 | 83 | 84 | Chart._idyll = { 85 | name: "Chart", 86 | tagType: "closed", 87 | props: [{ 88 | name: "type", 89 | type: "string", 90 | example: '"scatter"' 91 | },{ 92 | name: "data", 93 | type: "array", 94 | example: "`[{x: 1, y: 1}, { x: 2, y: 2 }]`" 95 | }] 96 | } 97 | 98 | export default Chart; 99 | -------------------------------------------------------------------------------- /components/default/code-highlight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SyntaxHighlighter from "react-syntax-highlighter/dist/light"; 3 | import style from 'react-syntax-highlighter/dist/styles/github'; 4 | 5 | class CodeHighlight extends React.PureComponent { 6 | render() { 7 | return {this.props.children.length ? this.props.children[0] : ''}; 8 | } 9 | } 10 | 11 | CodeHighlight.defaultProps = { 12 | children: [] 13 | } 14 | 15 | export default CodeHighlight; 16 | -------------------------------------------------------------------------------- /components/default/display.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Format = require('d3-format'); 3 | 4 | class Display extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.format = Format.format(props.format || '0.2f'); 8 | } 9 | 10 | formatValue(v) { 11 | const t = typeof v; 12 | switch(t) { 13 | case 'object': 14 | return JSON.stringify(v); 15 | case 'number': 16 | return this.format(v); 17 | case 'string': 18 | default: 19 | return v; 20 | } 21 | } 22 | 23 | render() { 24 | const { value } = this.props; 25 | const v = value !== undefined ? value : this.props.var; 26 | return ( 27 | 28 | {this.formatValue(v)} 29 | 30 | ); 31 | } 32 | } 33 | 34 | 35 | Display._idyll = { 36 | name: "Display", 37 | tagType: "closed", 38 | props: [{ 39 | name: "value", 40 | type: "number", 41 | example: "x" 42 | }, { 43 | name: "format", 44 | type: "string", 45 | example: '"0.2f"' 46 | }] 47 | } 48 | 49 | export default Display; 50 | -------------------------------------------------------------------------------- /components/default/dynamic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Format = require('d3-format'); 4 | const Drag = require('d3-drag'); 5 | const Selection = require('d3-selection'); 6 | 7 | class Dynamic extends React.PureComponent { 8 | 9 | componentDidMount() { 10 | let node; 11 | try { 12 | node = ReactDOM.findDOMNode(this); 13 | } catch(e) {}; 14 | if (!node) { 15 | return; 16 | } 17 | this.drag = Drag.drag().on('drag', () => { 18 | const dx = Selection.event.dx; 19 | const { step, value, interval } = this.props; 20 | const newValue = Math.max(Math.min(value + (step || interval) * dx, this.props.max), this.props.min); 21 | this.props.updateProps({ value: newValue }); 22 | }); 23 | this.drag(Selection.select(node)); 24 | } 25 | 26 | render() { 27 | const { format, value } = this.props; 28 | const formatter = Format.format(format); 29 | return ( 30 | 31 | {formatter(value)} 32 | 33 | ); 34 | } 35 | } 36 | 37 | Dynamic.defaultProps = { 38 | format: '.2f', 39 | min: Number.NEGATIVE_INFINITY, 40 | max: Number.POSITIVE_INFINITY, 41 | step: 1 42 | }; 43 | 44 | 45 | Dynamic._idyll = { 46 | name: "Dynamic", 47 | tagType: "closed", 48 | props: [{ 49 | name: "value", 50 | type: "number", 51 | example: "x" 52 | }, { 53 | name: "step", 54 | type: "string", 55 | example: '1' 56 | }, { 57 | name: "min", 58 | type: "number", 59 | example: '-100' 60 | }, { 61 | name: "max", 62 | type: "number", 63 | example: '100' 64 | }] 65 | } 66 | 67 | export default Dynamic; 68 | -------------------------------------------------------------------------------- /components/default/equation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Latex = require('react-latex-patched'); 4 | const select = require('d3-selection').select; 5 | const format = require('d3-format').format; 6 | 7 | const allowedProps = ['domain', 'step', 'children']; 8 | 9 | class Equation extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | showRange: false 14 | }; 15 | } 16 | 17 | handleChange(event) { 18 | this.props.updateProps({ 19 | value: +event.target.value 20 | }); 21 | } 22 | 23 | componentDidMount() { 24 | let dom; 25 | 26 | const cssId = 'idyll-equation-css'; // you could encode the css path itself to generate id.. 27 | const cssURL = '//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css' 28 | if (document && !document.getElementById(cssId) && !this.props.skipCSS && !select(`link[href='${cssURL}']`).size()) { 29 | const heads = document.getElementsByTagName('head') 30 | if (heads.length) { 31 | const head = heads[0]; 32 | const link = document.createElement('link'); 33 | link.id = cssId; 34 | link.href = cssURL; 35 | link.rel = 'stylesheet'; 36 | link.type = 'text/css'; 37 | link.media = 'all'; 38 | head.appendChild(link); 39 | } 40 | } 41 | 42 | try { 43 | dom = ReactDOM.findDOMNode(this); 44 | } catch(e) {}; 45 | if (!dom) { 46 | return; 47 | } 48 | 49 | this.propNodes = {}; 50 | const self = this; 51 | select(dom).selectAll('.mord').each(function (d) { 52 | const $this = select(this); 53 | Object.keys(self.props).filter((prop) => { 54 | return allowedProps.indexOf(prop) === -1 55 | }).forEach((prop) => { 56 | if ($this.text() === prop) { 57 | self.propNodes[prop] = $this; 58 | $this.style('cursor', 'pointer'); 59 | $this.on('mouseover', () => { 60 | $this.style('color', 'red'); 61 | }).on('mouseout', () => { 62 | if (!(self.state.showRange && self.state.var === prop)) { 63 | $this.style('color', 'black'); 64 | } 65 | }).on('click', () => { 66 | 67 | if (!(self.state.showRange && self.state.var === prop)) { 68 | self.setState({ 69 | showRange: true, 70 | var: prop 71 | }); 72 | $this.text(self.props[prop]) 73 | $this.style('color', 'red'); 74 | Object.keys(self.propNodes).filter(d => d !== prop).forEach((d) => { 75 | self.propNodes[d].text(d); 76 | self.propNodes[d].style('color', 'black'); 77 | }) 78 | } else { 79 | self.setState({ 80 | showRange: false, 81 | var: prop 82 | }); 83 | $this.style('color', 'black'); 84 | $this.text(prop) 85 | } 86 | }) 87 | } 88 | }) 89 | }); 90 | 91 | } 92 | 93 | handleRangeUpdate(event) { 94 | const newProps = {}; 95 | const val = +event.target.value; 96 | newProps[this.state.var] = val; 97 | this.props.updateProps(newProps); 98 | this.propNodes[this.state.var].text(val); 99 | } 100 | 101 | renderEditing() { 102 | if (!this.state.showRange) { 103 | return null; 104 | } 105 | 106 | const d = (this.props.domain || {})[this.state.var] || [-10, 10]; 107 | const step = (this.props.step || {})[this.state.var] || 0.1; 108 | return ( 109 |
110 | 111 |
112 | ); 113 | } 114 | 115 | getLatex() { 116 | if (this.props.latex) { 117 | return this.props.latex; 118 | } 119 | return (this.props.children && this.props.children[0]) ? this.props.children[0] : ''; 120 | } 121 | 122 | render() { 123 | const latexChar = '$'; 124 | const latexString = latexChar + this.getLatex() + latexChar; 125 | 126 | let style; 127 | if (this.state.showRange) { 128 | style = this.props.style; 129 | } else { 130 | style = Object.assign({ 131 | display: this.props.display ? "block" : "inline-block" 132 | }, this.props.style); 133 | } 134 | 135 | return ( 136 | 137 | {latexString} 138 | {this.renderEditing()} 139 | 140 | ); 141 | } 142 | } 143 | 144 | Equation._idyll = { 145 | name: "Equation", 146 | tagType: "open", 147 | children: "y = x^2", 148 | props: [{ 149 | name: "display", 150 | type: "boolean", 151 | example: "true" 152 | }] 153 | } 154 | 155 | export default Equation; 156 | -------------------------------------------------------------------------------- /components/default/feature.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | const stateClasses = [ 5 | 'is-top', 6 | 'is-fixed', 7 | 'is-bottom' 8 | ]; 9 | 10 | class Content extends React.PureComponent { 11 | render () { 12 | return
13 | {this.props.children} 14 |
15 | } 16 | } 17 | 18 | class Feature extends React.PureComponent { 19 | constructor (props) { 20 | super(props) 21 | this.setFeature = this.setFeature.bind(this); 22 | this.setRoot = this.setRoot.bind(this); 23 | 24 | this.state = { 25 | scrollState: 0, 26 | featureMarginLeft: 0, 27 | }; 28 | } 29 | 30 | setRoot (c) { 31 | this.rootEl = c; 32 | this.initialize(); 33 | } 34 | 35 | setFeature (c) { 36 | this.featureEl = c; 37 | this.initialize(); 38 | } 39 | 40 | handleResize () { 41 | let rootRect = this.rootEl.getBoundingClientRect() 42 | this.setState({ 43 | featureMarginLeft: -rootRect.left 44 | }); 45 | } 46 | 47 | handleScroll () { 48 | if (!this.rootEl) return; 49 | let rootRect = this.rootEl.getBoundingClientRect(); 50 | let position = rootRect.top / (window.innerHeight - rootRect.height) 51 | // Update this whenever it changes so that the state is correctly adjusted: 52 | this.setState({scrollState: position < 0 ? 0 : (position <= 1 ? 1 : 2)}) 53 | // Only update the value when onscreen: 54 | if (rootRect.top < window.innerHeight && rootRect.bottom > 0) { 55 | this.props.updateProps({value: position}) 56 | } 57 | } 58 | 59 | 60 | 61 | initialize () { 62 | if (!this.rootEl || !this.featureEl) return; 63 | 64 | this.handleResize(); 65 | window.addEventListener('resize', this.handleResize.bind(this)); 66 | window.addEventListener('scroll', this.handleScroll.bind(this)); 67 | } 68 | 69 | unwrapChild(c) { 70 | if (c => c.type.name && c.type.name.toLowerCase() === 'wrapper') { 71 | return c.props.children[0]; 72 | } 73 | return c; 74 | } 75 | 76 | unwrapChildren() { 77 | return this.props.children.map((c) => this.unwrapChild(c)); 78 | } 79 | 80 | splitFeatureChildren() { 81 | const unwrapped = this.unwrapChildren(); 82 | return React.Children.toArray(this.props.children).reduce((memo, child, i) => { 83 | const c = unwrapped[i]; 84 | if (!c.type) { 85 | memo[1] = memo[1].concat([child]); 86 | return memo; 87 | } 88 | if ((c.type.name && c.type.name.toLowerCase() === 'content') || c.type.prototype instanceof Content) { 89 | memo[0] = child; 90 | } else { 91 | memo[1] = memo[1].concat([child]); 92 | } 93 | return memo; 94 | }, [undefined, []]); 95 | } 96 | 97 | render () { 98 | let feature; 99 | let ps = this.state.scrollState; 100 | let featureStyles = { 101 | width: 'calc(100vw - 15px)', 102 | overflowX: 'hidden', 103 | height: '100vh', 104 | marginLeft: ps === 1 ? 0 : (this.state.featureMarginLeft + 'px'), 105 | position: ps >= 1 ? 'fixed' : 'absolute', 106 | bottom: ps === 2 ? 0 : 'auto', 107 | zIndex: -1 108 | }; 109 | 110 | if (ps === 1) { 111 | featureStyles.top = 0; 112 | featureStyles.right = 0; 113 | featureStyles.bottom = 0; 114 | featureStyles.left = 0; 115 | } 116 | 117 | let rootStyles = { 118 | position: 'relative', 119 | marginLeft: 0, 120 | marginRight: 0, 121 | maxWidth: 'none' 122 | }; 123 | 124 | const [ featureChild, nonFeatureChildren ] = this.splitFeatureChildren(); 125 | 126 | if (featureChild) { 127 | const unwrapped = this.unwrapChild(featureChild); 128 | if (featureChild !== unwrapped) { 129 | // React.Children.only(featureChild.props.children); 130 | feature = React.cloneElement(featureChild, { 131 | children: React.cloneElement(React.Children.toArray(featureChild.props.children)[0], { 132 | style: featureStyles, 133 | ref: (ref) => this.setFeature(ref) 134 | }) 135 | }); 136 | } else { 137 | feature = React.cloneElement(featureChild, { 138 | style: featureStyles, 139 | ref: (ref) => this.setFeature(ref) 140 | }); 141 | } 142 | } 143 | 144 | return
{ return this.setRoot(ref) }} 148 | > 149 | {feature} 150 | {nonFeatureChildren} 151 |
152 | } 153 | } 154 | 155 | Feature.defaultProps = { 156 | children: [] 157 | }; 158 | 159 | 160 | 161 | Feature._idyll = { 162 | name: "Feature", 163 | tagType: "open", 164 | props: [{ 165 | name: "value", 166 | type: "number", 167 | example: "x" 168 | }] 169 | } 170 | 171 | export { Content, Feature as default }; 172 | -------------------------------------------------------------------------------- /components/default/fixed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Fixed extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | 14 | Fixed._idyll = { 15 | name: "Fixed", 16 | tagType: "open" 17 | } 18 | 19 | export default Fixed; 20 | -------------------------------------------------------------------------------- /components/default/float.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Float extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | 14 | Float._idyll = { 15 | name: "Float", 16 | tagType: "open", 17 | props: [{ 18 | name: "position", 19 | type: "string", 20 | example: '"left"' 21 | }, { 22 | name: 'width', 23 | type: 'string', 24 | example: '"50%"' 25 | }] 26 | } 27 | 28 | export default Float; 29 | -------------------------------------------------------------------------------- /components/default/full-screen.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Screen from './utils/screen'; 5 | 6 | class FullScreen extends React.PureComponent { 7 | constructor (props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | return ; 13 | } 14 | 15 | } 16 | 17 | export default FullScreen; 18 | -------------------------------------------------------------------------------- /components/default/gist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const PropTypes = require('prop-types'); 3 | 4 | class EmbeddedGist extends React.PureComponent { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.gist = props.gist; 9 | this.file = props.file; 10 | this.stylesheetAdded = false; 11 | this.state = { 12 | loading: true, 13 | src: "" 14 | }; 15 | } 16 | 17 | // The Gist JSON data includes a stylesheet to add to the page 18 | // to make it look correct. `addStylesheet` ensures we only add 19 | // the stylesheet one time. 20 | addStylesheet(href) { 21 | if (!this.stylesheetAdded) { 22 | this.stylesheetAdded = true; 23 | var link = document.createElement('link'); 24 | link.type = "text/css"; 25 | link.rel = "stylesheet"; 26 | link.href = href; 27 | 28 | (document.head || document.body || {appendChild: () => {}}).appendChild(link); 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | // Create a JSONP callback that will set our state 34 | // with the data that comes back from the Gist site 35 | var gistCallback = EmbeddedGist.nextGistCallback(); 36 | window[gistCallback] = function(gist) { 37 | this.setState({ 38 | loading: false, 39 | src: gist.div 40 | }); 41 | this.addStylesheet(gist.stylesheet); 42 | }.bind(this); 43 | 44 | var url = "https://gist.github.com/" + this.props.gist + ".json?callback=" + gistCallback; 45 | if (this.props.file) { 46 | url += "&file=" + this.props.file; 47 | } 48 | 49 | // Add the JSONP script tag to the document. 50 | var script = document.createElement('script'); 51 | script.type = 'text/javascript'; 52 | script.src = url; 53 | (document.head || document.body || {appendChild: () => {}}).appendChild(script); 54 | } 55 | 56 | render() { 57 | if (this.state.loading) { 58 | return
loading...
; 59 | } else { 60 | return
; 61 | } 62 | } 63 | } 64 | 65 | EmbeddedGist.propTypes = { 66 | gist: PropTypes.string.isRequired, // e.g. "username/id" 67 | file: PropTypes.string // to embed a single specific file from the gist 68 | }; 69 | 70 | // Each time we request a Gist, we'll need to generate a new 71 | // global function name to serve as the JSONP callback. 72 | var gistCallbackId = 0; 73 | EmbeddedGist.nextGistCallback = () => { 74 | return "embed_gist_callback_" + gistCallbackId++; 75 | }; 76 | 77 | EmbeddedGist.defaultProps = { 78 | gist: 'mathisonian/689614257cb1af6b15de3344da6cdc7a' 79 | } 80 | 81 | EmbeddedGist._idyll = { 82 | name: "Gist", 83 | tagType: "closed", 84 | props: [{ 85 | name: "gist", 86 | type: "string", 87 | example: '"0f83a12e29b268ffca39f471ecf39e91"' 88 | }, { 89 | name: 'file', 90 | type: 'string', 91 | example: '"particles.idl"' 92 | }] 93 | } 94 | export default EmbeddedGist; 95 | 96 | -------------------------------------------------------------------------------- /components/default/graphic.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Graphic extends React.Component { 4 | render() { 5 | const { idyll, updateProps, hasError, ...props } = this.props; 6 | return ( 7 |
8 | ); 9 | } 10 | } 11 | 12 | module.exports = Graphic; 13 | -------------------------------------------------------------------------------- /components/default/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Header extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |

8 | {this.props.title} 9 |

10 | { 11 | this.props.subtitle && ( 12 |

13 | {this.props.subtitle} 14 |

15 | ) 16 | } 17 | { 18 | this.props.author && ( 19 |
20 | By: {this.props.author} 21 |
22 | ) 23 | } 24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | Header._idyll = { 31 | name: "Header", 32 | tagType: "closed", 33 | props: [{ 34 | name: "title", 35 | type: "string", 36 | example: '"Article Title"' 37 | }, { 38 | name: 'subtitle', 39 | type: 'string', 40 | example: '"Article subtitle."' 41 | }, { 42 | name: 'author', 43 | type: 'string', 44 | example: '"Author Name"' 45 | }, { 46 | name: 'authorLink', 47 | type: 'string', 48 | example: '"author.website"' 49 | }] 50 | } 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /components/default/index.js: -------------------------------------------------------------------------------- 1 | export { default as Action } from './action'; 2 | export { default as Analytics } from './analytics'; 3 | export { default as Aside } from './aside'; 4 | export { default as Boolean } from './boolean'; 5 | export { default as Button } from './button'; 6 | export { default as Chart } from './chart'; 7 | export { default as CodeHighlight } from './code-highlight'; 8 | export { default as Display } from './display'; 9 | export { default as Dynamic } from './dynamic'; 10 | export { default as Equation } from './equation'; 11 | export { default as Feature, Content as FeatureContent } from './feature'; 12 | export { default as Fixed } from './fixed'; 13 | export { default as Float } from './float'; 14 | export { default as FullScreen } from './full-screen'; 15 | export { default as Gist } from './gist'; 16 | export { default as Header } from './header'; 17 | export { default as Inline } from './inline'; 18 | export { default as Link } from './link'; 19 | export { default as Panel } from './panel'; 20 | export { default as Preload } from './preload'; 21 | export { default as Radio } from './radio'; 22 | export { default as Range } from './range'; 23 | export { default as Select } from './select'; 24 | export { default as Slide } from './slide'; 25 | export { default as Slideshow } from './slideshow'; 26 | export { default as Step } from './step'; 27 | export { default as Stepper } from './stepper'; 28 | export { default as StepperControl } from './stepper-control'; 29 | export { default as SVG } from './svg'; 30 | export { default as Table } from './table'; 31 | export { default as TextContainer } from './text-container'; 32 | export { default as TextInput } from './text-input'; 33 | export { default as Waypoint } from './waypoint'; 34 | -------------------------------------------------------------------------------- /components/default/inline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Inline extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | Inline._idyll = { 14 | name: "Inline", 15 | tagType: "open" 16 | } 17 | 18 | 19 | export default Inline; 20 | -------------------------------------------------------------------------------- /components/default/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Link extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | let props = {...this.props}; 10 | if (props.url) { 11 | props.href = props.url; 12 | } 13 | return ( 14 | 15 | {this.props.text || this.props.children} 16 | 17 | ); 18 | } 19 | } 20 | 21 | Link._idyll = { 22 | name: "Link", 23 | tagType: "closed", 24 | props: [{ 25 | name: "text", 26 | type: "string", 27 | example: '"Link Text"' 28 | }, { 29 | name: 'url', 30 | type: 'string', 31 | example: '"https://some.url/"' 32 | }] 33 | } 34 | 35 | export default Link; 36 | -------------------------------------------------------------------------------- /components/default/panel.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | const ReactDOM = require('react-dom'); 4 | 5 | class Panel extends React.PureComponent { 6 | constructor (props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const { updateProps, hasError, ...props } = this.props; 12 | return
; 13 | } 14 | 15 | } 16 | 17 | export default Panel; 18 | -------------------------------------------------------------------------------- /components/default/preload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const imageCache = []; 4 | 5 | class Preloader extends React.PureComponent { 6 | componentDidMount() { 7 | const { images } = this.props; 8 | images.forEach((i) => { 9 | const img = new Image(); 10 | img.src = i; 11 | imageCache.push(img); 12 | }); 13 | } 14 | render () { 15 | return null; 16 | } 17 | } 18 | 19 | Preloader.defaultProps = { 20 | images: [] 21 | }; 22 | 23 | export default Preloader; 24 | -------------------------------------------------------------------------------- /components/default/radio.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | let id = 0; 4 | 5 | class Radio extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.onChange = this.onChange.bind(this); 9 | this.id = id++; 10 | } 11 | 12 | onChange(e) { 13 | this.props.updateProps({ value: e.target.value }); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | {this.props.options.map((d) => { 20 | if (typeof d === 'string') { 21 | return ; 22 | } 23 | return ; 24 | })} 25 |
26 | ); 27 | } 28 | } 29 | 30 | Radio.defaultProps = { 31 | options: [] 32 | }; 33 | 34 | 35 | Radio._idyll = { 36 | name: "Radio", 37 | tagType: "closed", 38 | props: [{ 39 | name: "value", 40 | type: "string", 41 | example: "x" 42 | }, { 43 | name: "options", 44 | type: "array", 45 | example: '`["option1", "option2"]`' 46 | }] 47 | } 48 | 49 | export default Radio; 50 | -------------------------------------------------------------------------------- /components/default/range.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Range extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | handleChange(event) { 9 | this.props.updateProps({ 10 | value: +event.target.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value, min, max, step } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Range.defaultProps = { 23 | value: 0, 24 | min: 0, 25 | max: 1, 26 | step: 1 27 | }; 28 | 29 | Range._idyll = { 30 | name: "Range", 31 | tagType: "closed", 32 | props: [{ 33 | name: "value", 34 | type: "number", 35 | example: "x" 36 | }, { 37 | name: "min", 38 | type: "number", 39 | example: '0' 40 | }, { 41 | name: "max", 42 | type: "number", 43 | example: '100' 44 | }, { 45 | name: "step", 46 | type: "number", 47 | example: '1' 48 | }] 49 | } 50 | 51 | export default Range; 52 | -------------------------------------------------------------------------------- /components/default/scroller.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { filterChildren, mapChildren } = require('idyll-component-children'); 3 | import TextContainer from './text-container'; 4 | const d3 = require('d3'); 5 | 6 | 7 | const styles = { 8 | SCROLL_GRAPHIC: { 9 | position: 'absolute', 10 | top: 0, 11 | left: 0, 12 | right: 0, 13 | bottom: 'auto', 14 | height: '100vh', 15 | width: '100%', 16 | transform: `translate3d(0, 0, 0)`, 17 | zIndex: -1 18 | }, 19 | SCROLL_GRAPHIC_FIXED: { 20 | position: 'fixed' 21 | }, 22 | SCROLL_GRAPHIC_BOTTOM: { 23 | bottom: 0, 24 | top: 'auto' 25 | }, 26 | 27 | SCROLL_GRAPHIC_INNER: { 28 | position: 'absolute', 29 | // right: '1rem', 30 | left: 0, 31 | right: 0, 32 | top: '50%', 33 | transform: 'translateY(-50%)' 34 | } 35 | } 36 | 37 | let id = 0; 38 | 39 | class Scroller extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | this.id = id++; 43 | this.state = { 44 | isFixed: false, 45 | isBottom: false, 46 | graphicHeight: 0, 47 | graphicWidth: 0 48 | }; 49 | 50 | this.SCROLL_STEP_MAP = {}; 51 | this.SCROLL_NAME_MAP = {}; 52 | } 53 | 54 | 55 | componentDidMount() { 56 | require('intersection-observer'); 57 | const scrollama = require('scrollama'); 58 | // instantiate the scrollama 59 | const scroller = scrollama(); 60 | this.handleResize(); 61 | 62 | // setup the instance, pass callback functions 63 | scroller 64 | .setup({ 65 | step: '.idyll-scroll-text .idyll-step', // required 66 | container: `#idyll-scroll-${this.id}`, // required (for sticky) 67 | graphic: '.idyll-scroll-graphic' // required (for sticky) 68 | }) 69 | .onStepEnter(this.handleStepEnter.bind(this)) 70 | // .onStepExit(handleStepExit) 71 | .onContainerEnter(this.handleContainerEnter.bind(this)) 72 | .onContainerExit(this.handleContainerExit.bind(this)); 73 | 74 | 75 | // setup resize event 76 | window.addEventListener('resize', this.handleResize.bind(this)); 77 | } 78 | 79 | handleStepEnter({ element, index, direction }) { 80 | this.SCROLL_STEP_MAP[index] && this.SCROLL_STEP_MAP[index](); 81 | let update = { currentStep: index }; 82 | if (this.SCROLL_NAME_MAP[index]) { 83 | update.currentState = this.SCROLL_NAME_MAP[index]; 84 | } 85 | this.props.updateProps && this.props.updateProps(update); 86 | if (index === Object.keys(this.SCROLL_STEP_MAP).length - 1) { 87 | d3.select('body').style('overflow', 'auto'); 88 | } 89 | } 90 | 91 | handleResize() { 92 | this.setState({ 93 | graphicHeight: window.innerHeight + 'px', 94 | graphicWidth: window.innerWidth + 'px', 95 | }); 96 | } 97 | handleContainerEnter(response) { 98 | if (this.props.disableScroll && (!this.props.currentStep || this.props.currentStep < Object.keys(this.SCROLL_STEP_MAP).length - 1)) { 99 | d3.select('body').style('overflow', 'hidden'); 100 | } 101 | this.setState({ isFixed: true, isBottom: false }); 102 | } 103 | 104 | handleContainerExit(response) { 105 | this.setState({ isFixed: false, isBottom: response.direction === 'down'}); 106 | } 107 | 108 | componentWillReceiveProps(nextProps) { 109 | if (this.props.currentStep !== nextProps.currentStep) { 110 | d3.selectAll(`#idyll-scroll-${this.id} .idyll-step`) 111 | .filter(function (d, i) { return i === nextProps.currentStep;}) 112 | .node() 113 | .scrollIntoView({ behavior: 'smooth' }); 114 | } 115 | if (this.props.currentState !== nextProps.currentState) { 116 | d3.selectAll(`#idyll-scroll-${this.id} .idyll-step`) 117 | .filter(function (d, i) { return nextProps.currentState === this.SCROLL_NAME_MAP[i] }) 118 | .node() 119 | .scrollIntoView({ behavior: 'smooth' }); 120 | } 121 | if (nextProps.disableScroll && (!nextProps.currentStep || nextProps.currentStep < Object.keys(this.SCROLL_STEP_MAP).length - 1)) { 122 | d3.select('body').style('overflow', 'hidden'); 123 | } 124 | } 125 | 126 | registerStep(elt, name, val) { 127 | this.SCROLL_STEP_MAP[elt] = val; 128 | this.SCROLL_NAME_MAP[elt] = name; 129 | } 130 | 131 | render() { 132 | const { hasError, updateProps, idyll, children, ...props } = this.props; 133 | const { isFixed, isBottom, graphicHeight, graphicWidth } = this.state; 134 | return ( 135 |
this.ref = ref} className="idyll-scroll" id={`idyll-scroll-${this.id}`} style={{position: 'relative'}}> 136 |
141 | 142 |
143 | {filterChildren( 144 | children, 145 | (c) => { 146 | return c.type.name && c.type.name.toLowerCase() === 'graphic'; 147 | } 148 | )} 149 |
150 |
151 | 152 |
153 | {mapChildren(filterChildren( 154 | children, 155 | (c) => { 156 | return !c.type.name || c.type.name.toLowerCase() === 'step'; 157 | } 158 | ), (c) => { 159 | return React.cloneElement(c, { 160 | registerStep: this.registerStep.bind(this) 161 | }); 162 | })} 163 |
164 |
165 |
166 | ); 167 | } 168 | } 169 | 170 | 171 | module.exports = Scroller; 172 | -------------------------------------------------------------------------------- /components/default/select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class Select extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | const { idyll, hasError, updateProps, ...props } = this.props; 16 | return ( 17 | 25 | ); 26 | } 27 | } 28 | 29 | Select.defaultProps = { 30 | options: [] 31 | } 32 | 33 | Select._idyll = { 34 | name: "Select", 35 | tagType: "closed", 36 | props: [{ 37 | name: "value", 38 | type: "string", 39 | example: "x" 40 | }, { 41 | name: "options", 42 | type: "array", 43 | example: '`["option1", "option2"]`' 44 | }] 45 | } 46 | export default Select; 47 | -------------------------------------------------------------------------------- /components/default/slide.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Slide extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Slide; 14 | -------------------------------------------------------------------------------- /components/default/slideshow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Slide = require('./slide'); 3 | 4 | class Slideshow extends React.PureComponent { 5 | 6 | getChildren(children) { 7 | let processedChildren = []; 8 | React.Children.forEach(children, (child) => { 9 | if (typeof child === 'string') { 10 | return; 11 | } 12 | if ((child.type.name && child.type.name.toLowerCase() === 'slide') || child.type.prototype instanceof Slide) { 13 | processedChildren.push(child); 14 | } else { 15 | processedChildren = processedChildren.concat(this.getChildren(child.props.children)); 16 | } 17 | }) 18 | return processedChildren; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | {this.getChildren(this.props.children)[this.props.currentSlide-1]} 25 |
26 | ); 27 | } 28 | } 29 | 30 | Slideshow.defaultProps = { 31 | currentSlide: 1 32 | }; 33 | 34 | Slideshow._idyll = { 35 | name: "Slideshow", 36 | tagType: "open", 37 | children: [` 38 | [slide]This is the content for slide 1[/slide] 39 | [slide]This is the content for slide 2[/slide] 40 | [slide]This is the content for slide 3[/slide]`], 41 | props: [{ 42 | name: "currentSlide", 43 | type: "number", 44 | example: 'x' 45 | }] 46 | } 47 | export default Slideshow; 48 | -------------------------------------------------------------------------------- /components/default/step.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | let idx = 0; 4 | class Step extends React.Component { 5 | 6 | componentDidMount() { 7 | this.props.registerStep(idx++, this.props.state, (this.props.onEnter || (() => {})).bind(this)); 8 | } 9 | render() { 10 | const { idyll, updateProps, hasError, registerStep, onEnter, state, className, ...props } = this.props; 11 | return ( 12 |
this.ref = ref} className={`idyll-step ${className || ''}`} {...props} /> 13 | ); 14 | } 15 | } 16 | 17 | module.exports = Step; 18 | -------------------------------------------------------------------------------- /components/default/stepper-control.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class StepperControl extends React.Component { 4 | 5 | componentDidMount() { 6 | } 7 | render() { 8 | const { idyll, ...props } = this.props; 9 | return
10 |
11 | ← 12 |
13 |
14 | → 15 |
16 |
; 17 | 18 | 19 | // ( 20 | //
this.ref = ref} className={`idyll-step ${className || ''}`} style={{margin: '10vh 0 60vh 0'}} {...props} /> 21 | // ); 22 | } 23 | } 24 | 25 | module.exports = StepperControl; 26 | -------------------------------------------------------------------------------- /components/default/stepper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getProperty } from 'idyll-ast'; 3 | const { filterChildren, mapChildren } = require('idyll-component-children'); 4 | 5 | const Step = require('./step'); 6 | 7 | class Stepper extends React.PureComponent { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.SCROLL_STEP_MAP = {}; 12 | this.SCROLL_NAME_MAP = {}; 13 | } 14 | 15 | 16 | registerStep(elt, name, val) { 17 | this.SCROLL_STEP_MAP[elt] = val; 18 | this.SCROLL_NAME_MAP[elt] = name; 19 | } 20 | 21 | getSteps() { 22 | return filterChildren( 23 | this.props.children, 24 | (c) => { 25 | return c.type.name && c.type.name.toLowerCase() === 'step'; 26 | } 27 | ) 28 | } 29 | 30 | next() { 31 | this.props.updateProps({ currentStep: (this.props.currentStep + 1) % (this.getSteps().length) }); 32 | } 33 | previous() { 34 | let newStep = this.props.currentStep - 1; 35 | if (newStep < 0) { 36 | newStep = (this.getSteps().length) + newStep; 37 | } 38 | 39 | this.props.updateProps({ currentStep: newStep }); 40 | } 41 | 42 | getSelectedStep() { 43 | const { currentState, currentStep } = this.props; 44 | const steps = this.getSteps(); 45 | if (currentState) { 46 | return filterChildren( 47 | steps, 48 | (c) => { 49 | return c.props.state === currentState 50 | } 51 | )[0]; 52 | } 53 | return steps[currentStep % steps.length]; 54 | } 55 | 56 | render() { 57 | const { children, height, ...props } = this.props; 58 | return ( 59 |
60 |
61 | {filterChildren( 62 | children, 63 | (c) => { 64 | return c.type.name && c.type.name.toLowerCase() === 'graphic'; 65 | } 66 | )} 67 |
68 |
69 | { 70 | mapChildren(this.getSelectedStep(), (c) => { 71 | return React.cloneElement(c, { 72 | registerStep: this.registerStep.bind(this) 73 | }) 74 | }) 75 | } 76 |
77 | {mapChildren(filterChildren( 78 | children, 79 | (c) => { 80 | return c.type.name && c.type.name.toLowerCase() === 'steppercontrol'; 81 | } 82 | ), (c) => { 83 | return React.cloneElement(c, { 84 | next: this.next.bind(this), 85 | previous: this.previous.bind(this) 86 | }) 87 | })} 88 |
89 | ); 90 | } 91 | } 92 | 93 | 94 | Stepper.defaultProps = { 95 | currentStep: 0, 96 | height: 500 97 | }; 98 | 99 | Stepper._idyll = { 100 | name: "Stepper", 101 | tagType: "open", 102 | children: [` 103 | [Step]This is the content for step 1[/Step] 104 | [Step]This is the content for step 2[/Step] 105 | [Step]This is the content for step 3[/Step]`], 106 | props: [{ 107 | name: "currentStep", 108 | type: "number", 109 | example: '0' 110 | }] 111 | } 112 | export default Stepper; 113 | -------------------------------------------------------------------------------- /components/default/svg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InlineSVG from 'react-inlinesvg'; 3 | 4 | class SVG extends React.PureComponent { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | 12 | SVG.defaultProps = { 13 | src: '' 14 | } 15 | 16 | SVG._idyll = { 17 | name: "SVG", 18 | tagType: "closed", 19 | props: [{ 20 | name: "src", 21 | type: "string", 22 | example: '"https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg"' 23 | }] 24 | } 25 | 26 | export default SVG; 27 | 28 | -------------------------------------------------------------------------------- /components/default/table.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const Table = require('react-table').default; 3 | 4 | class TableComponent extends React.PureComponent { 5 | getColumns() { 6 | if (this.props.columns) { 7 | if (this.props.columns.length && typeof this.props.columns[0] === 'string') { 8 | return this.props.columns.map((d) => { 9 | return { 10 | Header: d, 11 | accessor: d 12 | }; 13 | }) 14 | } 15 | 16 | return this.props.columns; 17 | } 18 | if ((this.props.data || []).length) { 19 | return Object.keys(this.props.data[0]).map((d) => { 20 | return { 21 | Header: d, 22 | accessor: d 23 | } 24 | }) 25 | } 26 | 27 | return []; 28 | } 29 | render() { 30 | return ( 31 | 38 | ); 39 | } 40 | } 41 | 42 | TableComponent.defaultProps = { 43 | showPagination: false, 44 | showPageSizeOptions: false, 45 | showPageJump: false 46 | } 47 | 48 | TableComponent._idyll = { 49 | name: "Table", 50 | tagType: "closed", 51 | props: [{ 52 | name: "data", 53 | type: "array", 54 | example: 'x' 55 | }, { 56 | name: "showPagination", 57 | type: "boolean", 58 | example: 'false' 59 | }, { 60 | name: "showPageSizeOptions", 61 | type: "boolean", 62 | example: 'false' 63 | }, { 64 | name: "showPageJump", 65 | type: "boolean", 66 | example: 'false' 67 | }] 68 | } 69 | 70 | module.exports = TableComponent; 71 | -------------------------------------------------------------------------------- /components/default/text-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class TextContainer extends React.PureComponent { 4 | render() { 5 | const { idyll, children, className, hasError, updateProps, ...props } = this.props; 6 | const { styles, ...layout } = idyll.layout; 7 | const { styles: _, ...theme } = idyll.theme; 8 | const style = { ...layout, ...theme }; 9 | const cn = (className || '') + ' idyll-text-container'; 10 | return ( 11 |
{children}
12 | ); 13 | } 14 | } 15 | 16 | export default TextContainer; 17 | -------------------------------------------------------------------------------- /components/default/text-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class TextInput extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | const { idyll, hasError, updateProps, ...props } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | TextInput._idyll = { 23 | name: "TextInput", 24 | tagType: "closed", 25 | props: [{ 26 | name: "value", 27 | type: "string", 28 | example: '"Hello"' 29 | }] 30 | } 31 | 32 | export default TextInput; 33 | -------------------------------------------------------------------------------- /components/default/utils/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class Container extends React.Component { 5 | constructor (props) { 6 | super(props); 7 | 8 | this.state = { 9 | expandLeft: 0, 10 | expandRight: 0 11 | }; 12 | 13 | this.setPosition = this.setPosition.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | window.addEventListener('resize', this.setPosition); 18 | try { 19 | this.node = ReactDOM.findDOMNode(this) 20 | this.setPosition(); 21 | } catch(e) {} 22 | } 23 | 24 | //shouldComponentUpdate (nextProps, nextState) { 25 | //return Math.round(nextState.expandLeft) !== Math.round(this.state.expandLeft) || 26 | //Math.round(nextState.expandRight) !== Math.round(this.state.expandRight); 27 | //} 28 | 29 | setPosition () { 30 | var expandLeft, expandRight; 31 | var rect = this.node.getBoundingClientRect(); 32 | var pageWidth = window.innerWidth; 33 | 34 | if (this.props.fullBleed) { 35 | expandLeft = Infinity; 36 | expandRight = Infinity; 37 | } else { 38 | expandLeft = this.props.expandLeft === undefined ? this.props.expand : this.props.expandLeft; 39 | expandRight = this.props.expandRight === undefined ? this.props.expand : this.props.expandRight; 40 | } 41 | 42 | var left = Math.max(rect.left - expandLeft, this.props.padding); 43 | var right = Math.min(rect.right + expandRight, pageWidth - this.props.padding); 44 | 45 | this.setState({ 46 | expandLeft: left - rect.left, 47 | expandRight: rect.right - right 48 | }); 49 | } 50 | 51 | render () { 52 | var expandStyle = Object.assign({}, this.props.style || {}, { 53 | marginLeft: this.state.expandLeft, 54 | marginRight: this.state.expandRight 55 | }); 56 | 57 | return
61 |
62 | {this.props.children} 63 |
64 |
65 | } 66 | } 67 | 68 | Container.defaultProps = { 69 | padding: 15, 70 | expand: 0, 71 | fullBleed: false 72 | } 73 | 74 | export default Container; 75 | -------------------------------------------------------------------------------- /components/default/utils/screen.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Container from './container'; 5 | 6 | class Screen extends React.PureComponent { 7 | constructor (props) { 8 | super(props); 9 | } 10 | 11 | 12 | render () { 13 | let overlayStyle = { 14 | position: this.props.display ? this.props.display : 'relative', 15 | zIndex: 1, 16 | width: this.props.fullBleed ? '100%' : undefined, 17 | left: this.props.display === 'fixed' ? 0 : undefined, 18 | pointerEvents: 'none', 19 | transition: 'background 0.5s' 20 | }; 21 | 22 | if (this.props.height) { 23 | overlayStyle.minHeight = this.props.height; 24 | } else { 25 | overlayStyle.height = '100vh'; 26 | } 27 | 28 | if (this.props.backgroundImage) { 29 | overlayStyle.backgroundImage = 'url(' + this.props.backgroundImage + ')'; 30 | overlayStyle.backgroundSize = 'cover'; 31 | overlayStyle.backgroundPosition = 'top center'; 32 | } 33 | 34 | let contentContainerStyle = Object.assign({ 35 | flexDirection: this.props.direction || 'column', 36 | display: 'flex', 37 | height: '100%', 38 | justifyContent: { 39 | center: 'center' 40 | }[this.props.justify] || undefined 41 | }, this.props.contentContainerStyle || {}); 42 | 43 | let contentStyle = { 44 | alignSelf: { 45 | left: 'flex-start', 46 | center: 'center', 47 | right: 'flex-end', 48 | stretch: 'stretch' 49 | }[this.props.align] || 'flex-end', 50 | pointerEvents: 'all' 51 | } 52 | 53 | if (this.props.fullBleed) { 54 | return ( 55 |
56 |
57 |
58 |
59 |
60 | {this.props.children} 61 |
62 |
63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | return 78 |
79 |
80 |
81 | {this.props.children} 82 |
83 |
84 |
85 | 86 | } 87 | } 88 | 89 | Screen.defaultProps = { 90 | position: 0.5, 91 | padding: 0, 92 | fullBleed: false, 93 | align: 'left', 94 | }; 95 | 96 | export default Screen; 97 | -------------------------------------------------------------------------------- /components/default/waypoint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Screen from './utils/screen'; 4 | 5 | class Waypoint extends React.PureComponent { 6 | constructor (props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ; 12 | } 13 | 14 | } 15 | 16 | Waypoint._idyll = { 17 | name: "Waypoint", 18 | tagType: "open", 19 | props: [{ 20 | name: "onEnterView", 21 | type: "event", 22 | example: "`x = true`" 23 | }] 24 | } 25 | 26 | export default Waypoint; 27 | -------------------------------------------------------------------------------- /components/image.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class CustomComponent extends React.Component { 4 | 5 | componentDidCatch(e) { 6 | console.log(e); 7 | } 8 | render() { 9 | const { hasError, updateProps, children, ...props } = this.props; 10 | return ( 11 | 12 | ); 13 | } 14 | } 15 | 16 | module.exports = CustomComponent; -------------------------------------------------------------------------------- /components/multiRiffle.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class multiRiffle extends React.PureComponent { 4 | render() { 5 | const { onClick, hasError, updateProps, iter, points, ...props } = this.props; 6 | return ( 7 |