diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e737a31 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-1", "react"], + "plugins": ["react-pure-components"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45f7222 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.log +lib diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..cace0d6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +node_modules +*.log +src diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f21f59f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +COPYRIGHT (c) 2016 James Kyle + +MIT License + +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: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1943c3 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# spectacle-code-slide + +Present code with style. + + + +## Install + +``` +$ npm install --save spectacle-code-slide +``` + +## Usage + +```js +import CodeSlide from 'spectacle-code-slide'; + +export default class Presentation extends React.Component { + render() { + return ( + + + // ... + + // ... + + + ); + } +} +``` diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..ac84a5d Binary files /dev/null and b/demo.gif differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..53fd0b3 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "spectacle-code-slide", + "version": "0.0.0", + "description": "Present code with style.", + "main": "lib/index.js", + "scripts": { + "build": "babel src -d lib", + "prepublish": "npm run build" + }, + "author": "James Kyle ", + "license": "MIT", + "dependencies": { + "component-raf": "^1.2.0", + "component-tween": "^1.2.0", + "lodash.clamp": "^4.0.1", + "lodash.memoize": "^4.0.1", + "lodash.padstart": "^4.2.0", + "react": "^0.14.7", + "spectacle": "^1.0.4" + }, + "devDependencies": { + "babel-cli": "^6.6.5", + "babel-plugin-react-pure-components": "^2.2.2", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", + "babel-preset-stage-1": "^6.5.0" + } +} diff --git a/src/CodeSlide.js b/src/CodeSlide.js new file mode 100644 index 0000000..3815be1 --- /dev/null +++ b/src/CodeSlide.js @@ -0,0 +1,103 @@ +const React = require('react'); +const {PropTypes} = React; + +const {Slide} = require('spectacle'); +const CodeSlideTitle = require('./CodeSlideTitle'); +const CodeSlideCode = require('./CodeSlideCode'); +const CodeSlideNote = require('./CodeSlideNote'); + +const clamp = require('lodash.clamp'); +const padStart = require('lodash.padstart'); +const getHighlightedCodeLines = require('./getHighlightedCodeLines'); +const calculateScrollCenter = reuqire('./calculateScrollCenter'); + +function startOrEnd(index, loc) { + if (index === loc[0]) { + return 'start'; + } else if (index - 1 === loc[1]) { + return 'end'; + } else { + return null; + } +} + +function calculateOpacity(index, loc) { + return (loc[0] <= index && loc[1] > index) ? 1 : 0.2; +} + +function getLineNumber(index) { + return '' + padStart(index + 1, 3) + '. '; +} + +class CodeSlide extends React.Component { + static propTypes = { + lang: PropTypes.string.isRequired, + code: PropTypes.string.isRequired, + ranges: PropTypes.arrayOf(PropTypes.shape({ + loc: PropTypes.arrayOf(PropTypes.number).isRequired, + title: PropTypes.string, + note: PropTypes.string + })) + }; + + state = { + active: 0 + }; + + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } + + onKeyDown = e => { + let prev = this.state.active; + let active; + + if (e.which === 38) { + active = prev - 1; + } else if (e.which === 40) { + active = prev + 1; + } + + if (active) { + e.preventDefault(); + active = clamp(active, 0, this.props.ranges.length); + this.setState({ active }, this.scrollIntoView); + } + }; + + scrollIntoView = () => { + const {container, start, end} = this.refs; + const scrollTo = calculateScrollCenter(start, end, container); + scrollToElement(container, 0, scrollTo); + }; + + render() { + const {code, lang, ranges, ...rest} = this.props; + const {active} = this.state; + + const range = ranges[active] || {}; + const loc = range.loc || []; + + const lines = getHighlightedCodeLines(code, lang).map((line, index) => { + return
; + }); + + return ( + + {range.title && {range.title}} + + {range.note && {range.note}} + + ); + } +} + +module.exports = CodeSlide; diff --git a/src/CodeSlideCode.js b/src/CodeSlideCode.js new file mode 100644 index 0000000..ddf30c9 --- /dev/null +++ b/src/CodeSlideCode.js @@ -0,0 +1,23 @@ +const React = require('react'); + +const styles = { + position: 'relative', + textAlign: 'left', + overflow: 'auto', + color: 'white', + height: '646px', + margin: 0, + padding: '40% 0' +}; + +class CodeSlideTitle extends React.Component { + render() { + return ( +
+        {this.props.children}
+      
+ ); + } +} + +module.exports = CodeSlideTitle; diff --git a/src/CodeSlideNote.js b/src/CodeSlideNote.js new file mode 100644 index 0000000..7688cd3 --- /dev/null +++ b/src/CodeSlideNote.js @@ -0,0 +1,24 @@ +const React = require('react'); + +const style = { + position: 'fixed', + bottom: '20px', + width: '100%', + padding: '20px', + background: 'black', + color: 'white', + fontFamily: 'monospace', + textAlign: 'left' +}; + +class CodeSlideNote extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +module.exports = CodeSlideNote; diff --git a/src/CodeSlideTitle.js b/src/CodeSlideTitle.js new file mode 100644 index 0000000..86551cb --- /dev/null +++ b/src/CodeSlideTitle.js @@ -0,0 +1,24 @@ +const React = require('react'); + +const styles = { + position: 'fixed', + left: '50%', + top: '20px', + transform: 'translate(-50%)', + padding: '20px 40px', + border: '10px solid hotpink', + fontSize: '2.5em', + color: 'white', + textTransform: 'uppercase', + whiteSpace: 'nowrap' +}; + +class CodeSlideTitle extends React.Component { + render() { + return ( +

{this.props.children}

+ ); + } +} + +module.exports = CodeSlideTitle; diff --git a/src/caculateScrollCenter.js b/src/caculateScrollCenter.js new file mode 100644 index 0000000..aee4004 --- /dev/null +++ b/src/caculateScrollCenter.js @@ -0,0 +1,16 @@ +function calculateScrollCenter(start, end, container) { + if (!start) return; + + end = end || start; + + const top = start.offsetTop; + const bottom = end.offsetTop + end.offsetHeight; + + const middle = Math.floor((top + bottom) / 2); + const height = container.offsetHeight; + const half = height / 2; + + return middle - half; +} + +module.exports = calculateScrollCenter; diff --git a/src/getHighlightedCodeLines.js b/src/getHighlightedCodeLines.js new file mode 100644 index 0000000..90a2488 --- /dev/null +++ b/src/getHighlightedCodeLines.js @@ -0,0 +1,15 @@ +const memoize = require('lodash.memoize'); + +function highlightCode(code, lang) { + if (window.Prism) { + return window.Prism.highlight(code, window.Prism.languages[lang]) + } else { + return code; + } +} + +function getHighlightedCodeLines(code, lang) { + return highlightCode(code, lang).split('\n'); +} + +module.exports = memoize(getHighlightedCodeLines); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b87cb13 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +module.exports = require('./CodeSlide'); diff --git a/src/scrollToElement.js b/src/scrollToElement.js new file mode 100644 index 0000000..6b61263 --- /dev/null +++ b/src/scrollToElement.js @@ -0,0 +1,43 @@ +/** + * Adapted from scroll-to@0.0.2 (MIT Licensed) https://github.com/component/scroll-to + */ + +const Tween = require('component-tween'); +const raf = require('component-raf'); + +function scroll(element) { + var y = element.scrollTop; + var x = element.scrollLeft; + return { top: y, left: x }; +} + +function scrollToElement(element, x, y, options) { + options = options || {}; + + var start = scroll(element); + + var tween = Tween(start) + .ease(options.ease || 'out-circ') + .to({ top: y, left: x }) + .duration(options.duration || 1000); + + tween.update(function(o){ + element.scrollTop = o.top | 0; + element.scrollLeft = o.left | 0; + }); + + tween.on('end', function(){ + animate = function(){}; + }); + + function animate() { + raf(animate); + tween.update(); + } + + animate(); + + return tween; +} + +module.exports = scrollToElement;