├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── demo.gif ├── package.json └── src ├── Terminal.js ├── TerminalStyle.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-2", "react" ] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "leankit", "leankit/es6", "leankit/react" ] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | lib 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | src 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Elijah Manor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spectacle-terminal 2 | 3 | Terminal component that can be used in a spectacle slide deck 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install --save spectacle 9 | $ npm install --save spectacle-terminal 10 | ``` 11 | 12 | ## Demo 13 | 14 | ![](./demo.gif) 15 | 16 | [Interactive Demo](http://elijahmanor.github.com/spectacle-terminal) 17 | 18 | ## Usage 19 | 20 | ```jsx 21 | import React from "react"; 22 | import { Spectacle, Deck, Slide, Heading } from "spectacle"; 23 | import createTheme from "spectacle/lib/themes/default"; 24 | import Terminal from "spectacle-terminal"; 25 | 26 | const theme = createTheme({ primary: "#ff4081" }); 27 | 28 | export default class Presentation extends React.Component { 29 | render() { 30 | return ( 31 | 32 | 33 | 34 | Terminal 35 | TOTAL: 174 SUCCESS, 38 |
39 |
=============================== Coverage summary ===============================
40 |
Statements : 51.29% ( 278/542 )
41 |
Branches : 38.78% ( 95/245 )
42 |
Functions : 46.21% ( 61/132 )
43 |
Lines : 52.69% ( 274/520 )
44 |
================================================================================
45 |
]} 46 | /> 47 |
48 |
49 |
50 | ); 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elijahmanor/spectacle-terminal/7bbeab91db9d706045240c459be68a439ebe5314/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spectacle-terminal", 3 | "version": "0.5.0", 4 | "description": "Terminal component that can be used in a spectacle slide deck", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prebuild": "rm -rf lib/", 8 | "build": "babel src -d lib", 9 | "prepublish": "npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/elijahmanor/spectacle-terminal.git" 14 | }, 15 | "keywords": [ 16 | "spectacle", 17 | "terminal" 18 | ], 19 | "author": "Elijah Manor", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/elijahmanor/spectacle-terminal/issues" 23 | }, 24 | "homepage": "https://github.com/elijahmanor/spectacle-terminal#readme", 25 | "devDependencies": { 26 | "babel-cli": "^6.18.0", 27 | "babel-preset-es2015": "^6.18.0", 28 | "babel-preset-react": "^6.16.0", 29 | "babel-preset-stage-2": "^6.18.0", 30 | "eslint": "^3.12.1", 31 | "eslint-config-leankit": "^3.0.0", 32 | "spectacle": "^1.2.5" 33 | }, 34 | "dependencies": { 35 | "classnames": "^2.2.5", 36 | "prop-types": "^15.6.0", 37 | "react": "^15.4.1", 38 | "react-dom": "^15.4.1" 39 | }, 40 | "publishConfig": { 41 | "registry": "https://registry.npmjs.org/" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Terminal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {array, bool} from "prop-types"; 3 | import classNames from "classnames"; 4 | import style from "./TerminalStyle"; 5 | 6 | export default class Terminal extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | currentLine: { 11 | index: props.showFirstEntry ? 0 : null, 12 | iteration: null 13 | }, 14 | isCollapsed: false, 15 | isMaximized: props.isMaximized, 16 | isAutoScroll: true 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | document.addEventListener("keydown", this.updateCurrentLine); 22 | document.addEventListener("keydown", this.toggleMaximize); 23 | this.terminalRef.closest(".spectacle-content").style.transform = "none"; 24 | } 25 | 26 | componentDidUpdate(prevProps, prevState) { 27 | if (this.mainRef) { 28 | if (this.state.isAutoScroll) { 29 | this.mainRef.scrollTop = this.mainRef.scrollHeight; 30 | } else if (this.state.currentLine.scrollTo) { 31 | this.mainRef.scrollTop = this.state.currentLine.scrollTo; 32 | } 33 | } 34 | this.terminalRef.closest(".spectacle-content").style.transform = "none"; 35 | } 36 | 37 | componentWillUnmount() { 38 | document.removeEventListener("keydown", this.updateCurrentLine); 39 | document.removeEventListener("keydown", this.toggleMaximize); 40 | } 41 | 42 | toggleMaximize = e => { 43 | if (e.altKey && e.keyCode === 77) { 44 | this[`handle${this.state.isMaximized ? "Minimize" : "Expand"}`](); 45 | } 46 | }; 47 | 48 | goDown(line) { 49 | const { output } = this.props; 50 | if (line.index != null) { 51 | let newIndex = line.index < output.length - 1 ? line.index + 1 : line.index; 52 | let nextLine = line.iteration != null ? output[line.index] : output[newIndex]; 53 | if (Array.isArray(nextLine)) { 54 | if (line.iteration != null) { 55 | line.iteration = line.iteration < nextLine.length - 1 ? ++line.iteration : null; 56 | if (line.iteration == null) { 57 | line.index = newIndex; 58 | } 59 | } else { 60 | line.index = newIndex; 61 | line.iteration = 0; 62 | } 63 | nextLine = line.iteration != null ? nextLine[line.iteration] : nextLine; 64 | } else { 65 | line.index = newIndex; 66 | line.iteration = null; 67 | nextLine = output[line.index]; 68 | } 69 | line.isAutoScroll = nextLine.isAutoScroll != null ? nextLine.isAutoScroll : true; 70 | line.isSolo = nextLine.isSolo != null ? nextLine.isSolo : false; 71 | line.scrollTo = nextLine.scrollTo != null ? nextLine.scrollTo : null; 72 | line.note = nextLine.note; 73 | } else { 74 | line = { index: 0 }; 75 | if (Array.isArray(output[line.index])) { 76 | line.iteration = 0; 77 | } 78 | } 79 | return line; 80 | } 81 | 82 | goUp(line) { 83 | const { showFirstEntry, output } = this.props; 84 | 85 | if (line.index > 0) { 86 | line.index--; 87 | } else if (line.index === 0) { 88 | line.index = showFirstEntry ? 0 : null; 89 | } 90 | const previousLine = output[line.index]; 91 | if (previousLine) { 92 | line.isAutoScroll = previousLine.isAutoScroll != null ? previousLine.isAutoScroll : true; 93 | line.isSolo = previousLine.isSolo != null ? previousLine.isSolo : false; 94 | line.scrollTo = previousLine.scrollTo != null ? previousLine.scrollTo : null; 95 | line.note = previousLine.note; 96 | } 97 | 98 | return line; 99 | } 100 | 101 | updateCurrentLine = e => { 102 | let { currentLine } = this.state; 103 | if (e.keyCode === 40) { 104 | currentLine = this.goDown(currentLine); 105 | } else if (e.keyCode === 38) { 106 | currentLine = this.goUp(currentLine); 107 | } 108 | this.setState({ 109 | currentLine, 110 | isAutoScroll: currentLine.isAutoScroll 111 | }); 112 | }; 113 | 114 | handleClose = () => { 115 | this.setState({ isCollapsed: !this.state.isCollapsed }); 116 | }; 117 | 118 | handleMinimize = () => { 119 | this.setState({ isMaximized: false }); 120 | }; 121 | 122 | handleExpand = () => { 123 | this.setState({ isMaximized: true }); 124 | this.terminalRef.closest(".spectacle-content").style.transform = "none"; 125 | document.querySelector("button[data-radium]").closest("div").style.display = "none"; 126 | }; 127 | 128 | renderLines() { 129 | const { output } = this.props; 130 | const { currentLine } = this.state; 131 | 132 | if (!currentLine.isSolo) { 133 | return output.reduce((memo, line, index) => { 134 | if ( 135 | (index <= currentLine.index && !line.isSolo) || 136 | (currentLine.index === 0 && index === 0) 137 | ) { 138 | if (Array.isArray(line)) { 139 | const iteration = 140 | currentLine.iteration != null && currentLine.index === index 141 | ? currentLine.iteration 142 | : line.length - 1; 143 | memo.push( 144 |
145 | {line[iteration].output || line[iteration]} 146 |
147 | ); 148 | } else { 149 | memo.push( 150 |
151 | {line.output || line} 152 |
153 | ); 154 | } 155 | } 156 | return memo; 157 | }, []); 158 | } else { 159 | const line = output[currentLine.index]; 160 | if (currentLine.scrollTo) { 161 | this.mainRef.scrollTop = currentLine.scrollTo; 162 | } else { 163 | this.mainRef.scrollTop = 0; 164 | } 165 | return ( 166 |
167 | {line.output || line} 168 |
169 | ); 170 | } 171 | } 172 | 173 | renderMain() { 174 | const { output } = this.props; 175 | const { isMaximized, currentLine } = this.state; 176 | return ( 177 |
{ 180 | this.mainRef = elem; 181 | }} 182 | style={Object.assign({}, style.main, isMaximized ? style.mainMaximized : {})} 183 | > 184 | {currentLine.index != null && this.renderLines()} 185 |
186 | ); 187 | } 188 | 189 | renderNote() { 190 | const { currentLine } = this.state; 191 | return ( 192 |
208 | {currentLine.note} 209 |
210 | ); 211 | } 212 | 213 | render() { 214 | const { title } = this.props; 215 | const { isMaximized, isCollapsed, currentLine } = this.state; 216 | 217 | return ( 218 |
{ 222 | this.terminalRef = elem; 223 | }} 224 | > 225 |
226 | 237 |
{title}
238 |
239 | {!isCollapsed && this.renderMain()} 240 | {currentLine.note && this.renderNote()} 241 |
242 | ); 243 | } 244 | } 245 | 246 | Terminal.propTypes = { 247 | output: array, 248 | showFirstEntry: bool, 249 | isMaximized: false 250 | }; 251 | -------------------------------------------------------------------------------- /src/TerminalStyle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | container: { 3 | position: "relative", 4 | fontSize: "1.75vw", 5 | minWidth: "75vw", 6 | maxWidth: "75vw", 7 | left: "50%", 8 | transform: "translateX(-50%)" 9 | }, 10 | containerMaximized: { 11 | position: "absolute", 12 | top: "1rem", 13 | right: "1rem", 14 | bottom: "5rem", 15 | left: "1rem", 16 | fontSize: "1.9vw" 17 | }, 18 | header: { 19 | position: "relative", 20 | padding: "10px", 21 | backgroundColor: "#E0E9F0", 22 | borderTopLeftRadius: "10px", 23 | borderTopRightRadius: "10px", 24 | boxShadow: "inset 0px -3px 10px 0px rgba(0, 0, 0, 0.2)" 25 | }, 26 | nav: { 27 | display: "flex", 28 | textAlign: "left", 29 | height: "2vw", 30 | alignItems: "center" 31 | }, 32 | title: { 33 | position: "absolute", 34 | top: "50%", 35 | left: "50%", 36 | transform: "translate(-50%, -50%)", 37 | fontSize: "1.75vw" 38 | }, 39 | button: { 40 | border: "none", 41 | borderRadius: "50%", 42 | height: "1.5vw", 43 | width: "1.5vw", 44 | marginRight: "1vw" 45 | }, 46 | buttonClose: { 47 | backgroundColor: "#EE5057" 48 | }, 49 | buttonMinimize: { 50 | backgroundColor: "#DEC612" 51 | }, 52 | buttonExpand: { 53 | backgroundColor: "#33B969" 54 | }, 55 | main: { 56 | padding: "15px", 57 | backgroundColor: "#303539", 58 | color: "white", 59 | height: "50vh", 60 | fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace", 61 | direction: "ltr", 62 | textAlign: "left", 63 | whiteSpace: "pre", 64 | wordSpacing: "normal", 65 | wordBreak: "normal", 66 | wordWrap: "normal", 67 | lineHeight: "1.5", 68 | tabSize: "4", 69 | hyphens: "none", 70 | overflow: "auto" 71 | }, 72 | mainMaximized: { 73 | height: "100%", 74 | maxHeight: "none" 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require( "./Terminal" ); 2 | --------------------------------------------------------------------------------