├── .gitignore
├── LICENCE
├── README.md
├── docs
├── index.html
└── index.js
├── img.png
├── jsconfig.json
├── package.json
├── postcss.config.js
├── src
├── actions
│ └── index.jsx
├── components
│ ├── App.jsx
│ ├── App.scss
│ ├── IndentTextarea.jsx
│ ├── Link.jsx
│ ├── Pin.jsx
│ └── PointLink.jsx
├── containers
│ ├── Balloons.jsx
│ ├── Balloons.scss
│ ├── Block.jsx
│ ├── Block.scss
│ ├── BlockCreator.jsx
│ ├── BlockCreator.scss
│ ├── Chain.jsx
│ ├── Chain.scss
│ ├── HTMLEditor.jsx
│ ├── HTMLRenderer.jsx
│ ├── HTMLRenderer.scss
│ └── PinLink.jsx
├── cosine-curve-points.jsx
├── index.jsx
├── index.pug
├── index.scss
├── models
│ └── index.jsx
├── reducers
│ ├── balloons.jsx
│ ├── block-creator.jsx
│ ├── blocks.jsx
│ ├── html-editor.jsx
│ ├── index.jsx
│ ├── pin-links.jsx
│ └── point-link.jsx
└── shared
│ ├── util.scss
│ └── vars.scss
├── webpack.config.babel.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | ### https://raw.github.com/github/gitignore/4caa9bfad9c46254724a3ff3c1f6b1c4c2a687a0/node.gitignore
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20 | .grunt
21 |
22 | # node-waf configuration
23 | .lock-wscript
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directories
29 | node_modules
30 | jspm_packages
31 |
32 | # Optional npm cache directory
33 | .npm
34 |
35 | # Optional REPL history
36 | .node_repl_history
37 |
38 |
39 | ### https://raw.github.com/github/gitignore/4caa9bfad9c46254724a3ff3c1f6b1c4c2a687a0/Global/osx.gitignore
40 |
41 | .DS_Store
42 | .AppleDouble
43 | .LSOverride
44 |
45 | # Icon must end with two \r
46 | Icon
47 |
48 | # Thumbnails
49 | ._*
50 |
51 | # Files that might appear in the root of a volume
52 | .DocumentRevisions-V100
53 | .fseventsd
54 | .Spotlight-V100
55 | .TemporaryItems
56 | .Trashes
57 | .VolumeIcon.icns
58 |
59 | # Directories potentially created on remote AFP share
60 | .AppleDB
61 | .AppleDesktop
62 | Network Trash Folder
63 | Temporary Items
64 | .apdisk
65 |
66 |
67 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Hiroki Usuba
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 | # Chain
2 |
3 | A New Visual Programming Environment to Build JavaScript By Linking Blocks.
4 |
5 | Also, you can coedit this by using [Cochain](https://github.com/mimorisuzuko/chain/tree/feature/co).
6 |
7 | ## Example:
8 |
9 | `'Hello, Chain!'` => `'!niahC ,olleH'`
10 |
11 | 
12 |
13 | ### JavaScript
14 |
15 | ```javascript
16 | Array.from('Hello, ' + 'World!').reverse().join('')
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
Chain: A New Visual Programming Language to Build a Program Like JavaScript
--------------------------------------------------------------------------------
/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mimorisuzuko/chain/000cc7bca863da4411eb65f7f387ad1950a29ac7/img.png
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "module": "amd",
6 | "target": "ES6"
7 | },
8 | "exclude": [
9 | "node_modules",
10 | "docs"
11 | ]
12 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chain",
3 | "version": "0.0.1",
4 | "description": "A New Visual Programming Environment to Build JavaScript By Linking Blocks",
5 | "main": "docs/index.htnl",
6 | "scripts": {
7 | "webpack": "./node_modules/.bin/webpack --config webpack.config.babel.js",
8 | "build": "npm-run-all build:*",
9 | "build:pug": "./node_modules/.bin/pug --hierarchy -o docs/ src/",
10 | "build:js": "npm run webpack",
11 | "watch": "npm-run-all --parallel watch:*",
12 | "watch:pug": "npm run build:pug -- -w",
13 | "watch:js": "WATCH=true ./node_modules/.bin/webpack-dev-server --config webpack.config.babel.js"
14 | },
15 | "keywords": [],
16 | "author": "Hiroki Usuba (http://mimorisuzuko.github.io/)",
17 | "license": "MIT",
18 | "dependencies": {
19 | "autobind-decorator": "^2.1.0",
20 | "immutable": "^3.8.1",
21 | "lodash": "^4.17.4",
22 | "react": "^15.6.1",
23 | "react-codemirror": "^1.0.0",
24 | "react-dom": "^15.6.1",
25 | "react-redux": "^5.0.6",
26 | "react-router-dom": "^4.1.2",
27 | "redux": "^3.7.2",
28 | "redux-actions": "^2.2.1",
29 | "redux-batched-actions": "^0.1.6"
30 | },
31 | "devDependencies": {
32 | "autoprefixer": "^7.1.2",
33 | "babel-core": "^6.25.0",
34 | "babel-loader": "^7.1.1",
35 | "babel-plugin-react-css-modules": "^3.1.0",
36 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
37 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
38 | "babel-preset-es2015": "^6.24.1",
39 | "babel-preset-react": "^6.24.1",
40 | "clean-webpack-plugin": "^0.1.16",
41 | "css-loader": "^0.28.4",
42 | "node-sass": "^4.5.3",
43 | "npm-run-all": "^4.0.2",
44 | "postcss-loader": "^2.0.6",
45 | "postcss-scss": "^1.0.2",
46 | "pug-cli": "^1.0.0-alpha6",
47 | "react-hot-loader": "^1.3.1",
48 | "sass-loader": "^6.0.6",
49 | "style-loader": "^0.18.2",
50 | "webpack": "^3.5.1",
51 | "webpack-dev-server": "^2.7.1"
52 | },
53 | "babel": {
54 | "plugins": [
55 | "transform-es2015-modules-commonjs"
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')({})
4 | ]
5 | };
--------------------------------------------------------------------------------
/src/actions/index.jsx:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { createActions } from 'redux-actions';
3 |
4 | let blockId = -1;
5 | let balloonId = -1;
6 |
7 | export default createActions(
8 | {
9 | ADD_BLOCK: (block) => _.merge({ id: blockId += 1 }, block),
10 | UPDATE_BLOCK: (id, patch) => ({ id, patch }),
11 | DELTA_MOVE_BLOCK: (id, dx, dy) => ({ id, dx, dy }),
12 | TOGGLE_BLOCK_CREATOR: (x, y) => ({ x, y }),
13 | ADD_BALLOON: (balloon) => _.merge({ id: balloonId += 1 }, balloon)
14 | },
15 | 'DELETE_BLOCK',
16 | 'UPDATE_BLOCK_CREATOR',
17 | 'ADD_PIN',
18 | 'DELETE_PIN',
19 | 'START_POINT_LINK',
20 | 'END_POINT_LINK',
21 | 'ADD_PIN_LINK',
22 | 'REMOVE_PIN_LINK_BY_QUERY',
23 | 'ON_CHANGE_HTML',
24 | 'CLEAR_VIEW_BLOCK',
25 | 'PUSH_VIEW_BLOCK',
26 | 'DECREMENT_BALLOONS'
27 | );
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import Chain from '../containers/Chain';
4 | import { createStore } from 'redux';
5 | import state from '../reducers';
6 | import { enableBatching } from 'redux-batched-actions';
7 | import { HashRouter, Route, NavLink, Redirect } from 'react-router-dom';
8 | import HTMLRenderer from '../containers/HTMLRenderer';
9 | import HTMLEditor from '../containers/HTMLEditor';
10 | import { BlockCreator } from '../models';
11 | import actions from '../actions';
12 | import Balloons from '../containers/Balloons';
13 | import styles from './App.scss';
14 |
15 | const store = createStore(enableBatching(state));
16 | store.dispatch(actions.addBlock({ x: 100, y: 100, type: BlockCreator.VIEW_BLOCK }));
17 |
18 | const redirectRender = () => ;
19 |
20 | const App = () => (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export default App;
--------------------------------------------------------------------------------
/src/components/App.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/vars.scss';
2 |
3 | .wrap {
4 | width: 100%;
5 | height: 100%;
6 | background-color: $black0;
7 |
8 | footer {
9 | border-top: 1px solid $black1;
10 | color: $white0;
11 | }
12 | }
13 |
14 | .base {
15 | width: 100%;
16 | height: calc(100% - #{$footer-height});
17 | }
18 |
19 | .link {
20 | font-weight: 100;
21 | padding: 5px 10px;
22 | color: $white1;
23 | display: inline-block;
24 | text-decoration: none;
25 |
26 | > span {
27 | padding: 3px 6px;
28 | display: inline-block;
29 | border-bottom: 1px transparent solid;
30 | }
31 | }
32 |
33 | .active {
34 | color: $white0;
35 |
36 | > span {
37 | border-bottom-color: $blue0;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/IndentTextarea.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import _ from 'lodash';
4 | import autobind from 'autobind-decorator';
5 |
6 | class IndentTextarea extends Component {
7 | constructor() {
8 | super();
9 |
10 | this.tab = false;
11 | this.selectionStart = -1;
12 | }
13 |
14 | render() {
15 | const { props: prev } = this;
16 | const props = _.cloneDeep(prev);
17 | delete props.onKeyDown;
18 |
19 | return ;
20 | }
21 |
22 | componentDidUpdate() {
23 | const { selectionStart } = this;
24 |
25 | if (-1 < selectionStart) {
26 | const caret = selectionStart + 1;
27 | ReactDOM.findDOMNode(this).setSelectionRange(caret, caret);
28 | }
29 |
30 | this.selectionStart = -1;
31 | }
32 |
33 | @autobind
34 | onKeyDown(...args) {
35 | const { props: { onKeyDown } } = this;
36 | const { keyCode, currentTarget: { selectionStart } } = args[0];
37 |
38 | if (keyCode === 9) {
39 | this.selectionStart = selectionStart;
40 | }
41 |
42 | if (typeof onKeyDown === 'function') {
43 | onKeyDown(...args);
44 | }
45 | }
46 | }
47 |
48 | export default IndentTextarea;
--------------------------------------------------------------------------------
/src/components/Link.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cos from '../cosine-curve-points';
3 | import vars from '../shared/vars.scss';
4 |
5 | const { white0 } = vars;
6 |
7 | const Link = (props) => {
8 | const { points, strokeDasharray } = props;
9 |
10 | return (
11 |
12 | );
13 | };
14 |
15 | export default Link;
--------------------------------------------------------------------------------
/src/components/Pin.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Pin as PinModel } from '../models';
3 | import autobind from 'autobind-decorator';
4 |
5 | const d = PinModel.RADIUS + 1;
6 |
7 | export default class Pin extends Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = { enter: false, connecting: false };
12 | }
13 |
14 | render() {
15 | const { props: { model }, state: { enter, connecting } } = this;
16 | const color = model.get('color');
17 | const type = model.get('type');
18 |
19 | return (
20 |
36 | );
37 | }
38 |
39 | /**
40 | * @param {MouseEvent} e
41 | */
42 | @autobind
43 | onMouseDown(e) {
44 | const { props: { model, onMouseDown, parent } } = this;
45 |
46 | onMouseDown(e, model, parent);
47 | document.addEventListener('mouseup', this.onMouseUpDocument);
48 | this.setState({ connecting: true });
49 | }
50 |
51 | /**
52 | * @param {MouseEvent} e
53 | */
54 | @autobind
55 | onMouseup(e) {
56 | const { props: { model, onMouseUp, parent } } = this;
57 |
58 | onMouseUp(e, model, parent);
59 | }
60 |
61 | @autobind
62 | onMouseEnter() {
63 | this.setState({ enter: true });
64 | }
65 |
66 | @autobind
67 | onMouseLeave() {
68 | this.setState({ enter: false });
69 | }
70 |
71 | @autobind
72 | onMouseUpDocument() {
73 | document.removeEventListener('mouseup', this.onMouseUpDocument);
74 | this.setState({ connecting: false });
75 | }
76 | }
--------------------------------------------------------------------------------
/src/components/PointLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from './Link';
3 |
4 | const PointLink = (props) => {
5 | const { model } = props;
6 |
7 | return (
8 |
9 | );
10 | };
11 |
12 | export default PointLink;
--------------------------------------------------------------------------------
/src/containers/Balloons.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import autobind from 'autobind-decorator';
4 | import actions from '../actions';
5 | import './Balloons.scss';
6 |
7 | @connect(
8 | (state) => ({
9 | balloons: state.balloons
10 | })
11 | )
12 | export default class Balloons extends Component {
13 | componentDidMount() {
14 | this.loop();
15 | }
16 |
17 | render() {
18 | const { props: { balloons } } = this;
19 |
20 | return (
21 | {balloons.map((a) =>
{a.get('value')}
)}
22 | );
23 | }
24 |
25 | @autobind
26 | loop() {
27 | const { props: { dispatch } } = this;
28 |
29 | dispatch(actions.decrementBalloons());
30 | requestAnimationFrame(this.loop);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/containers/Balloons.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/vars.scss';
2 | @import '../shared/util.scss';
3 |
4 | .base {
5 | position: fixed;
6 | right: 15px;
7 | bottom: 15px;
8 |
9 | > div {
10 | font-family: $font;
11 | color: white;
12 | background-color: $blue0;
13 | padding: 5px 10px;
14 | border-radius: 4px;
15 |
16 | &:not(:first-of-type) {
17 | margin-top: 10px;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/containers/Block.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import actions from '../actions';
4 | import { Pin as PinModel } from '../models';
5 | import autobind from 'autobind-decorator';
6 | import IndentTextarea from '../components/IndentTextarea';
7 | import './Block.scss';
8 |
9 | @connect()
10 | export default class Block extends Component {
11 | constructor() {
12 | super();
13 |
14 | this.mouseDownX = 0;
15 | this.mouseDownY = 0;
16 | }
17 |
18 | render() {
19 | const { props: { model } } = this;
20 | const color = model.get('color');
21 |
22 | return (
23 |
29 |
30 | {model.get('deletable') ? : null}
31 | {model.get('changeable') ? : null}
32 | {model.get('changeable') ? : null}
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | /**
42 | * @param {Event} e
43 | */
44 | @autobind
45 | onChange(e) {
46 | const { props: { model, dispatch } } = this;
47 |
48 | dispatch(actions.updateBlock(model.get('id'), { value: e.currentTarget.value }));
49 | }
50 |
51 | @autobind
52 | onClickDeleteButton() {
53 | const { props: { model, dispatch } } = this;
54 | const id = model.get('id');
55 |
56 | dispatch(actions.deleteBlock(id));
57 | }
58 |
59 | /**
60 | * @param {MouseEvent} e
61 | */
62 | @autobind
63 | onMouseDown(e) {
64 | const { target: { nodeName }, pageX, pageY } = e;
65 |
66 | if (nodeName === 'DIV') {
67 | this.mouseDownX = pageX;
68 | this.mouseDownY = pageY;
69 | document.body.classList.add('cursor-move');
70 | document.addEventListener('mousemove', this.onMouseMoveDocument);
71 | document.addEventListener('mouseup', this.onMouseUpDocument);
72 | }
73 | }
74 |
75 | /**
76 | * @param {MouseEvent} e
77 | */
78 | @autobind
79 | onMouseMoveDocument(e) {
80 | const { pageX, pageY } = e;
81 | const { props: { model, dispatch }, mouseDownX, mouseDownY } = this;
82 |
83 | dispatch(actions.deltaMoveBlock(model.get('id'), pageX - mouseDownX, pageY - mouseDownY));
84 | this.mouseDownX = pageX;
85 | this.mouseDownY = pageY;
86 | }
87 |
88 | @autobind
89 | onMouseUpDocument() {
90 | document.body.classList.remove('cursor-move');
91 | document.removeEventListener('mousemove', this.onMouseMoveDocument);
92 | document.removeEventListener('mouseup', this.onMouseUpDocument);
93 | }
94 |
95 | @autobind
96 | addPin() {
97 | const { props: { model, dispatch } } = this;
98 |
99 | dispatch(actions.addPin(model.get('id')));
100 | }
101 |
102 | @autobind
103 | deletePin() {
104 | const { props: { model, dispatch } } = this;
105 |
106 | dispatch(actions.deletePin({
107 | id: model.get('id'),
108 | removed: model.get('inputPins').size - 1
109 | }));
110 | }
111 |
112 | /**
113 | * @param {KeyboardEvent} e
114 | */
115 | @autobind
116 | onKeyDown(e) {
117 | const { keyCode, currentTarget: { selectionStart, selectionEnd } } = e;
118 | const { props: { dispatch, model } } = this;
119 |
120 | if (keyCode === 9) {
121 | e.preventDefault();
122 | const v = model.get('value');
123 | dispatch(actions.updateBlock(model.get('id'), { value: `${v.substring(0, selectionStart)}\t${v.substring(selectionEnd)}` }));
124 | }
125 | }
126 |
127 | static convertPinType(pinType) {
128 | if (pinType === PinModel.OUTPUT) {
129 | return 'output';
130 | } else if (pinType === PinModel.INPUT) {
131 | return 'input';
132 | }
133 |
134 | return 'unknown';
135 | }
136 | }
--------------------------------------------------------------------------------
/src/containers/Block.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/vars.scss';
2 | @import '../shared/util.scss';
3 |
4 | $block-header-height: 16px;
5 |
6 | .base {
7 | @extend .shadow;
8 |
9 | font-size: 12px;
10 | color: $white0;
11 | border: 1px solid $black1;
12 | background-color: $black0;
13 | box-sizing: border-box;
14 | font-family: $font;
15 | width: $block-width;
16 | display: flex;
17 | flex-direction: column;
18 |
19 | button {
20 | display: inline-block;
21 | text-decoration: none;
22 | color: inherit;
23 | border: none;
24 | font: inherit;
25 | padding: 1px 8px;
26 | outline: none;
27 | cursor: pointer;
28 | background-color: $black1;
29 | user-select: none;
30 |
31 | &:not(:first-of-type) {
32 | margin-left: 1px;
33 | }
34 | }
35 |
36 | textarea {
37 | width: 100%;
38 | border: none;
39 | background-color: $black1;
40 | font: inherit;
41 | color: inherit;
42 | height: 100%;
43 | box-sizing: border-box;
44 | resize: vertical;
45 | outline: none;
46 | display: block;
47 | user-select: none;
48 | }
49 | }
50 |
51 | .textarea-div {
52 | height: calc(100% - #{$block-header-height});
53 | padding: 5px;
54 | box-sizing: border-box;
55 | }
56 |
57 | .red {
58 | background-color: $red0 !important;
59 | }
60 |
--------------------------------------------------------------------------------
/src/containers/BlockCreator.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import { connect } from 'react-redux';
4 | import _ from 'lodash';
5 | import actions from '../actions';
6 | import { BlockCreator as BlockCreatorModel } from '../models';
7 | import { batchActions } from 'redux-batched-actions';
8 | import autobind from 'autobind-decorator';
9 | import IndentTextarea from '../components/IndentTextarea';
10 | import './BlockCreator.scss';
11 |
12 | const { CREATABLE_TYPE_KEYS: OPTION_LIST } = BlockCreatorModel;
13 | const PASCAL_OPTION_LIST = _.map(OPTION_LIST, (a) => _.upperFirst((_.camelCase(a))));
14 |
15 | @connect(
16 | (state) => ({
17 | model: state.blockCreator
18 | })
19 |
20 | )
21 | export default class BlockCreator extends Component {
22 | componentDidMount() {
23 | document.addEventListener('mousedown', this.onMouseDownDocument);
24 | }
25 |
26 | componentWillUnmount() {
27 | document.removeEventListener('mousedown', this.onMouseDownDocument);
28 | }
29 |
30 | render() {
31 | const { props: { model } } = this;
32 |
33 | return model.get('visible') ? (
34 |
39 |
42 |
43 |
46 |
47 | ) : null;
48 | }
49 |
50 | @autobind
51 | onClick() {
52 | const { props: { model, dispatch } } = this;
53 |
54 | dispatch(batchActions([
55 | actions.addBlock({ x: model.get('x'), y: model.get('y'), value: _.trim(model.get('value')), type: model.get('selected') }),
56 | actions.toggleBlockCreator()
57 | ]));
58 | }
59 |
60 | /**
61 | * @param {Event} e
62 | */
63 | @autobind
64 | onChangeTextarea(e) {
65 | const { currentTarget: { value } } = e;
66 | const { props: { dispatch } } = this;
67 |
68 | dispatch(actions.updateBlockCreator({ value }));
69 | }
70 |
71 | /**
72 | * @param {Event} e
73 | */
74 | @autobind
75 | onChangeSelect(e) {
76 | const { currentTarget: { value } } = e;
77 | const { props: { dispatch } } = this;
78 |
79 | dispatch(actions.updateBlockCreator({ selected: value }));
80 | }
81 |
82 | /**
83 | * @param {MouseEvent} e
84 | */
85 | @autobind
86 | onMouseDownDocument(e) {
87 | const { target } = e;
88 | const { props: { dispatch } } = this;
89 | const $e = findDOMNode(this);
90 |
91 | if ($e && !findDOMNode(this).contains(target)) {
92 | dispatch(actions.updateBlockCreator({ visible: false }));
93 | }
94 | }
95 |
96 | /**
97 | * @param {KeyboardEvent} e
98 | */
99 | @autobind
100 | onKeyDown(e) {
101 | const { keyCode, currentTarget: { selectionStart, selectionEnd } } = e;
102 | const { props: { dispatch, model } } = this;
103 |
104 | if (keyCode === 9) {
105 | e.preventDefault();
106 | const v = model.get('value');
107 | dispatch(actions.updateBlockCreator({ value: `${v.substring(0, selectionStart)}\t${v.substring(selectionEnd)}` }));
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/src/containers/BlockCreator.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/vars.scss';
2 | @import '../shared/util.scss';
3 |
4 | .base {
5 | @extend .shadow;
6 |
7 | background-color: $black0;
8 | border: 1px solid $black1;
9 | color: $white0;
10 | width: 200px;
11 | padding: 10px;
12 | box-sizing: border-box;
13 | font-family: $font;
14 | font-size: 12px;
15 |
16 | select {
17 | display: block;
18 | background-color: $black1;
19 | color: inherit;
20 | width: 100%;
21 | border: 1px solid transparent;
22 | margin-bottom: 5px;
23 | font: inherit;
24 | outline: none;
25 |
26 | &:focus {
27 | border-color: $blue0;
28 | }
29 | }
30 |
31 | textarea {
32 | width: 100%;
33 | background-color: $black1;
34 | font: inherit;
35 | color: inherit;
36 | height: 100%;
37 | box-sizing: border-box;
38 | resize: vertical;
39 | outline: none;
40 | display: block;
41 | margin-bottom: 5px;
42 | border: 1px solid transparent;
43 |
44 | &:focus {
45 | border-color: $blue0;
46 | }
47 | }
48 |
49 | button {
50 | display: inline-block;
51 | background-color: $black1;
52 | color: inherit;
53 | border-radius: 4px;
54 | font: inherit;
55 | border: none;
56 | outline: none;
57 | cursor: pointer;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/containers/Chain.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { findDOMNode } from 'react-dom';
3 | import { connect } from 'react-redux';
4 | import Block from '../containers/Block';
5 | import BlockCreator from '../containers/BlockCreator';
6 | import actions from '../actions';
7 | import PointLink from '../components/PointLink';
8 | import PinLink from '../containers/PinLink';
9 | import _ from 'lodash';
10 | import autobind from 'autobind-decorator';
11 | import Pin from '../components/Pin';
12 | import { Pin as PinModel } from '../models';
13 | import { batchActions } from 'redux-batched-actions';
14 | import './Chain.scss';
15 |
16 | @connect(
17 | (state) => ({
18 | blocks: state.blocks,
19 | link: state.pointLink,
20 | links: state.pinLinks
21 | })
22 | )
23 | export default class Chain extends Component {
24 | componentDidMount() {
25 | window.addEventListener('message', this.onMessage);
26 | window.addEventListener('contextmenu', this.onContextmenu);
27 | }
28 |
29 | componentWillUnmount() {
30 | window.removeEventListener('contextmenu', this.onContextmenu);
31 | }
32 |
33 | render() {
34 | const { props: { link, links } } = this;
35 | let { props: { blocks } } = this;
36 |
37 | return (
38 |
39 |
50 | {blocks.map((model) => {
51 | const id = model.get('id');
52 | return [
53 |
,
54 | model.get('inputPins').map((pin) =>
),
55 | model.get('outputPins').map((pin) =>
)
56 | ];
57 | })}
58 |
59 |
60 | );
61 | }
62 |
63 | /**
64 | * @param {MouseEvent} e
65 | * @param {any} pinModel
66 | * @param {number} block
67 | */
68 | @autobind
69 | onConnectStart(e, pinModel, block) {
70 | const { props: { dispatch } } = this;
71 | const pinType = pinModel.get('type');
72 | const pin = pinModel.get('index');
73 | const batch = [actions.startPointLink({ x: pinModel.get('cx'), y: pinModel.get('cy') })];
74 |
75 | document.addEventListener('mousemove', this.onConnecting);
76 | document.addEventListener('mouseup', this.onConnectEnd);
77 | window.__connection__ = { block, pin, pinType };
78 |
79 | if (pinType === PinModel.INPUT) {
80 | batch.push(actions.removePinLinkByQuery({ input: { block, pin } }));
81 | }
82 |
83 | dispatch(batchActions(batch));
84 | }
85 |
86 | /**
87 | * @param {MouseEvent} e
88 | */
89 | @autobind
90 | onConnecting(e) {
91 | const { props: { dispatch } } = this;
92 | const { clientX, clientY } = e;
93 |
94 | dispatch(actions.endPointLink({ x: clientX, y: clientY }));
95 | }
96 |
97 | @autobind
98 | onConnectEnd() {
99 | const { props: { dispatch } } = this;
100 |
101 | dispatch(actions.startPointLink({ x: 0, y: 0 }));
102 | document.removeEventListener('mousemove', this.onConnecting);
103 | document.removeEventListener('mouseup', this.onConnectEnd);
104 | }
105 |
106 | /**
107 | * @param {MouseEvent} e
108 | * @param {any} pin
109 | * @param {number} block1
110 | */
111 | @autobind
112 | onConnectPin(e, pin, block1) {
113 | const { __connection__: { block: block0, pin: pin0, pinType: pinType0 } } = window;
114 | const { props: { dispatch } } = this;
115 | const pin1 = pin.get('index');
116 | const pinType1 = pin.get('type');
117 |
118 | if (block0 !== block1 && pinType0 !== pinType1) {
119 | dispatch(actions.addPinLink({
120 | [Block.convertPinType(pinType0)]: { block: block0, pin: pin0 },
121 | [Block.convertPinType(pinType1)]: { block: block1, pin: pin1 }
122 | }));
123 | }
124 | }
125 |
126 |
127 | /**
128 | * @param {MouseEvent} e
129 | */
130 | @autobind
131 | onContextmenu(e) {
132 | const { clientX, clientY } = e;
133 | const { props: { dispatch } } = this;
134 | const { left, top } = findDOMNode(this).getBoundingClientRect();
135 |
136 | e.preventDefault();
137 | dispatch(actions.toggleBlockCreator(clientX - left, clientY - top));
138 | }
139 |
140 | /**
141 | *
142 | * @param {MessageEvent} e
143 | */
144 | @autobind
145 | onMessage(e) {
146 | const { props: { dispatch } } = this;
147 | const { data: { type, value } } = e;
148 |
149 | if (type === 'chain-result') {
150 | dispatch(actions.pushViewBlock(JSON.stringify(value)));
151 | } else if (type === 'chain-error') {
152 | dispatch(actions.addBalloon({ value }));
153 | } else if (type === 'chain-clear') {
154 | dispatch(actions.clearViewBlock());
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/src/containers/Chain.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/vars.scss';
2 |
3 | .base {
4 | background-color: $black0;
5 | position: relative;
6 | width: 100%;
7 | height: 100%;
8 |
9 | > svg {
10 | display: block;
11 | width: 100%;
12 | height: 100%;
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/containers/HTMLEditor.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import actions from '../actions';
4 | import CodeMirror from 'react-codemirror';
5 | import 'codemirror/lib/codemirror.css';
6 | import 'codemirror/theme/monokai.css';
7 | import 'codemirror/mode/htmlmixed/htmlmixed';
8 | import 'codemirror/addon/selection/active-line';
9 | import autobind from 'autobind-decorator';
10 |
11 | @connect(
12 | (state) => ({
13 | code: state.htmlEditor
14 | })
15 | )
16 | export default class HTMLRenderer extends Component {
17 | render() {
18 | const { props: { code } } = this;
19 |
20 | return ;
26 | }
27 |
28 | @autobind
29 | onChange(code) {
30 | const { props: { dispatch } } = this;
31 |
32 | dispatch(actions.onChangeHtml(code));
33 | }
34 | }
--------------------------------------------------------------------------------
/src/containers/HTMLRenderer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import _ from 'lodash';
4 | import { BlockCreator as BlockCreatorModel } from '../models';
5 | import './HTMLRenderer.scss';
6 |
7 | const parser = new DOMParser();
8 |
9 | @connect(
10 | (state) => ({
11 | blocks: state.blocks,
12 | links: state.pinLinks,
13 | html: state.htmlEditor
14 | })
15 | )
16 | export default class HTMLRenderer extends Component {
17 | render() {
18 | const { props: { html } } = this;
19 | const $doc = parser.parseFromString(html, 'text/html');
20 | const $body = $doc.querySelector('body');
21 | const $script = document.createElement('script');
22 | const script = _.join(_.map(this.toEvalableString(), (a) => `parent.postMessage({ type: 'chain-result', value: ${a} }, '*')`), '\n');
23 | $script.innerHTML = `
24 | parent.postMessage({ type: 'chain-clear' }, '*');
25 | try {
26 | (0, eval)(${JSON.stringify(script)});
27 | } catch (err) {
28 | parent.postMessage({ type: 'chain-error', value: String(err) }, '*');
29 | }
30 | `;
31 | $body.appendChild($script);
32 |
33 | return