├── .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 |
--------------------------------------------------------------------------------