├── .babelrc ├── README.md ├── server.js ├── index.html ├── scripts └── deploy ├── .gitignore ├── src ├── scratchpad │ └── view.js └── scratchpad.js ├── styles.css ├── LICENSE ├── index.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tricycle 2 | A scratchpad for trying out Cycle.js. 3 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import budo from 'budo'; 2 | import babelify from 'babelify'; 3 | 4 | budo('./index.js', { 5 | live: '*.{css,html}', 6 | port: 8000, 7 | stream: process.stdout, 8 | browserify: { 9 | transform: babelify 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Try Cycle.js 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git fetch cyclejs 4 | 5 | git checkout gh-pages 6 | 7 | git merge cyclejs/master --no-commit 8 | 9 | npm install 10 | 11 | npm run bundle 12 | 13 | git commit -am "Update bundle" 14 | 15 | git push cyclejs gh-pages 16 | 17 | git checkout - 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | lib/ 30 | -------------------------------------------------------------------------------- /src/scratchpad/view.js: -------------------------------------------------------------------------------- 1 | import {div, input, textarea, span} from '@cycle/dom'; 2 | 3 | export default function scratchpadView ([props, error]) { 4 | return ( 5 | div('.scratchpad', [ 6 | div('.vim-support', [ 7 | div('vim mode'), 8 | input('.vim-checkbox', {props: {type: 'checkbox'}}), 9 | // div('enable cycle-restart'), 10 | // input('.instant-checkbox', {props: {type: 'checkbox', checked: true}}) 11 | ]), 12 | 13 | div('#editor.code', {props: { 14 | value: props.code, 15 | style: {width: props.codeWidth} 16 | }}), 17 | div('.handler'), 18 | div('.result-container', {style: {width: props.resultWidth}}, [ 19 | div('.app'), 20 | div('.error', error.toString()) 21 | ]) 22 | ]) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | 4 | height: 100%; 5 | 6 | font-family: 'Arial' 7 | } 8 | 9 | .scratchpad { 10 | display: flex; 11 | 12 | height: 100%; 13 | } 14 | 15 | .tricycle { 16 | height: 100%; 17 | } 18 | 19 | .code { 20 | width: 70%; 21 | 22 | padding: 0px 5px; 23 | 24 | margin: 0px; 25 | border: 0px; 26 | border-right: 1px solid gray; 27 | } 28 | 29 | .handler { 30 | z-index: 7; 31 | height: 100%; 32 | width: 10px; 33 | cursor: col-resize; 34 | position: absolute; 35 | margin-left: -5px; 36 | } 37 | 38 | .result-container { 39 | width: 30%; 40 | 41 | display: flex; 42 | 43 | flex-direction: column; 44 | } 45 | 46 | .app { 47 | padding: 5px; 48 | 49 | flex-grow: 1; 50 | } 51 | 52 | .error { 53 | border-top: 1px solid gray; 54 | 55 | padding: 5px; 56 | 57 | background: #BBB; 58 | } 59 | 60 | .vim-support { 61 | position: absolute; 62 | bottom: 2px; 63 | left: 53px; 64 | 65 | z-index: 100; 66 | 67 | color: lightgray; 68 | 69 | opacity: 0.5; 70 | 71 | display: flex; 72 | } 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nick Johnstone 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 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {run} from '@cycle/run'; 2 | import {makeDOMDriver, div} from '@cycle/dom'; 3 | import xs from 'xstream'; 4 | 5 | import Scratchpad from './src/scratchpad'; 6 | 7 | const startingCode = ` 8 | import {run} from '@cycle/run'; 9 | import {makeDOMDriver, div, button} from '@cycle/dom'; 10 | import _ from 'lodash'; 11 | import xs from 'xstream'; 12 | 13 | function main (sources) { 14 | const add$ = sources.DOM 15 | .select('.add') 16 | .events('click') 17 | .map(ev => 1); 18 | 19 | const count$ = add$.fold((total, change) => total + change, 0); 20 | 21 | return { 22 | DOM: count$.map(count => 23 | div('.counter', [ 24 | 'Count: ' + count, 25 | button('.add', 'Add') 26 | ]) 27 | ) 28 | }; 29 | } 30 | 31 | const drivers = { 32 | DOM: makeDOMDriver('.app') 33 | } 34 | 35 | // Normally you need to call run, but Tricycle handles that for you! 36 | // If you want to try this out locally, just uncomment this code. 37 | // 38 | // run(main, drivers); 39 | `; 40 | 41 | function main ({DOM}) { 42 | const props = xs.of({code: startingCode}); 43 | const scratchpad = Scratchpad(DOM, props); 44 | 45 | return { 46 | DOM: scratchpad.DOM 47 | }; 48 | } 49 | 50 | run(main, { 51 | DOM: makeDOMDriver('.tricycle') 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tricycle", 3 | "version": "0.0.1", 4 | "description": "A scratchpad for trying out Cycle.js", 5 | "main": "lib/scratchpad.js", 6 | "files": [ 7 | "lib/" 8 | ], 9 | "scripts": { 10 | "start": "babel-node server.js", 11 | "test": "mocha --compilers js:babel-core/register", 12 | "precompile-lib": "rm -rf lib/ && mkdir -p lib", 13 | "compile-lib": "babel src -d lib", 14 | "prepublish": "npm run compile-lib" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Widdershin/trycycle.git" 19 | }, 20 | "keywords": [ 21 | "cycle", 22 | "cycle.js", 23 | "FRP", 24 | "happiness" 25 | ], 26 | "author": "Nick Johnstone", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Widdershin/trycycle/issues" 30 | }, 31 | "homepage": "https://github.com/Widdershin/trycycle", 32 | "dependencies": { 33 | "@cycle/dom": "^17.4.0", 34 | "@cycle/isolate": "^1.4.0", 35 | "@cycle/run": "^3.1.0", 36 | "babel-core": "^6.2.1", 37 | "babel-preset-es2015": "^6.1.18", 38 | "brace": "^0.5.1", 39 | "cycle-restart": "0.0.12", 40 | "lodash": "^3.10.1", 41 | "xstream": "^10.8.0" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.2.0", 45 | "babelify": "^7.2.0", 46 | "browserify": "^12.0.1", 47 | "budo": "^6.0.4", 48 | "garnish": "^4.1.1", 49 | "mocha": "^2.3.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scratchpad.js: -------------------------------------------------------------------------------- 1 | import {run} from '@cycle/run'; 2 | import {makeDOMDriver, h, div} from '@cycle/dom'; 3 | import isolate from '@cycle/isolate'; 4 | import xs from 'xstream'; 5 | import delay from 'xstream/extra/delay'; 6 | import debounce from 'xstream/extra/debounce'; 7 | import sampleCombine from 'xstream/extra/sampleCombine'; 8 | // import {restart, restartable} from 'cycle-restart'; 9 | 10 | const babel = require('babel-core'); 11 | import ace from 'brace'; 12 | import 'brace/mode/javascript'; 13 | import 'brace/theme/monokai'; 14 | import 'brace/keybinding/vim'; 15 | 16 | import _ from 'lodash'; 17 | 18 | import view from './scratchpad/view'; 19 | 20 | import es2015 from 'babel-preset-es2015'; 21 | 22 | import vm from 'vm'; 23 | 24 | function transformES6 (error$) { 25 | return ({code}) => { 26 | try { 27 | return babel.transform(code, {presets: [es2015]}); 28 | } catch (e) { 29 | error$.shamefullySendNext(e); 30 | return {code: ''}; 31 | } 32 | }; 33 | } 34 | 35 | function startAceEditor (code$) { 36 | function updateCode (editor) { 37 | return (_, ev) => { 38 | code$.shamefullySendNext({code: editor.getSession().getValue()}); 39 | }; 40 | } 41 | 42 | return ({code}) => { 43 | window.editor = ace.edit('editor'); 44 | editor.getSession().setMode('ace/mode/javascript'); 45 | editor.setTheme('ace/theme/monokai'); 46 | editor.getSession().setOptions({ 47 | tabSize: 2 48 | }); 49 | 50 | editor.setValue(code); 51 | editor.clearSelection(); 52 | editor.on('input', updateCode(editor)); 53 | }; 54 | } 55 | 56 | export default function Scratchpad (DOM, props) { 57 | let sources, sinks, drivers; 58 | 59 | const code$ = xs.create(); 60 | 61 | const error$ = xs.create(); 62 | 63 | error$.addListener({ 64 | next (err) { 65 | console.log(err); 66 | }, 67 | error (err) { 68 | console.error(err); 69 | }, 70 | complete () {} 71 | }); 72 | 73 | props.compose(delay(100)).addListener({ 74 | next (v) { 75 | startAceEditor(code$)(v) 76 | }, 77 | error (err) { 78 | console.error(err); 79 | }, 80 | complete () {} 81 | }); 82 | 83 | DOM.select('.vim-checkbox').events('change') 84 | .map(ev => ev.target.checked ? 'ace/keyboard/vim' : null) 85 | .startWith(null) 86 | .addListener({ 87 | next (keyHandler) { 88 | if (window.editor) { 89 | window.editor.setKeyboardHandler(keyHandler); 90 | } 91 | }, 92 | error (err) { 93 | console.error(err); 94 | }, 95 | complete () {} 96 | }); 97 | 98 | const restartEnabled$ = DOM.select('.instant-checkbox').events('change') 99 | .map(ev => ev.target.checked) 100 | .startWith(true); 101 | 102 | xs.merge(props, code$) 103 | .compose(debounce(300)) 104 | .map(transformES6(error$)) 105 | .compose(sampleCombine(restartEnabled$)) 106 | .addListener({ 107 | next ([{code}, restartEnabled]) { 108 | runOrRestart(code, restartEnabled) 109 | }, 110 | error (err) { 111 | console.error(err); 112 | }, 113 | complete () {} 114 | }) 115 | 116 | function runOrRestart(code, restartEnabled) { 117 | if (sources) { 118 | sources.dispose(); 119 | } 120 | 121 | if (sinks) { 122 | sinks.dispose(); 123 | } 124 | 125 | const context = {error$, require, console}; 126 | 127 | const wrappedCode = ` 128 | try { 129 | ${code} 130 | 131 | error$.shamefullySendNext(''); 132 | } catch (e) { 133 | error$.shamefullySendNext(e); 134 | } 135 | `; 136 | 137 | try { 138 | vm.runInNewContext(wrappedCode, context); 139 | } catch (e) { 140 | error$.shamefullySendNext(e); 141 | } 142 | 143 | if (typeof context.main !== 'function' || typeof context.drivers !== 'object') { 144 | return; 145 | } 146 | 147 | let userApp; 148 | 149 | if (!drivers) { 150 | drivers = context.drivers; 151 | } 152 | 153 | try { 154 | if (sources && restartEnabled) { 155 | // userApp = restart(context.main, drivers, {sources, sinks}) 156 | } else { 157 | userApp = run(context.main, context.drivers); 158 | } 159 | } catch (e) { 160 | error$.shamefullySendNext(e); 161 | } 162 | 163 | if (userApp) { 164 | sources = userApp.sources; 165 | sinks = userApp.sinks; 166 | } 167 | }; 168 | 169 | const clientWidth$ = DOM.select(':root').elements().map(target => target.clientWidth); 170 | const mouseDown$ = DOM.select('.handler').events('mousedown'); 171 | const mouseUp$ = DOM.select('.tricycle').events('mouseup'); 172 | const mouseMove$ = DOM.select('.tricycle').events('mousemove'); 173 | const mouseLeave$ = DOM.select('.tricycle').events('mouseleave'); 174 | 175 | const MAX_RESULT_WIDTH = 0.9; 176 | const MIN_RESULT_WIDTH = 0.1; 177 | 178 | const windowSize$ = xs.combine( 179 | mouseDown$ 180 | .map(mouseDown => mouseMove$.takeUntil(mouseUp$.merge(mouseLeave$))) 181 | .flatten(), 182 | // TODO: This debounce should be throttle 183 | clientWidth$.compose(debounce(100)) 184 | ) 185 | .map((mouseDrag, clientWidth) => 186 | (clientWidth - mouseDrag.clientX) / clientWidth 187 | ) 188 | .filter(fraction => 189 | fraction < MAX_RESULT_WIDTH && fraction > MIN_RESULT_WIDTH 190 | ) 191 | .map(fraction => ({ 192 | codeWidth: `${100*(1 - fraction)}%`, 193 | resultWidth: `${100*fraction}%` 194 | })); 195 | 196 | return { 197 | DOM: xs.combine( 198 | xs.merge(props, windowSize$), 199 | error$.startWith('') 200 | ).map(view) 201 | }; 202 | } 203 | --------------------------------------------------------------------------------