├── src
├── styles
│ └── main.css
├── views
│ └── home.js
├── elements
│ └── codemirror.js
├── index.js
├── lib
│ └── codemirror-portal.js
└── models
│ └── codemirror.js
├── .editorconfig
├── .yo-rc.json
├── scripts
├── dev-server.sh
└── build-prod.sh
├── static
└── index.html
├── package.json
├── license
├── .gitignore
└── readme.md
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | .split {
2 | width: 50%;
3 | float: left;
4 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [{package.json,*.yml}]
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-choo": {
3 | "template": {
4 | "projectName": "cm-wrapper",
5 | "projectDescription": "",
6 | "githubUsername": "test",
7 | "name": "Matt McFarland",
8 | "email": "contact@mattmcfarland.com"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/scripts/dev-server.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | NODE_ENV=development budo src/index.js:js/main.js --live \
4 | --open \
5 | --host localhost \
6 | --dir static \
7 | --pushstate \
8 | --title cm-wrapper \
9 | --port 3000 \
10 | -- -t sheetify/transform -g es2040
11 |
--------------------------------------------------------------------------------
/src/views/home.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html')
2 | const codemirror = require('../elements/codemirror')
3 |
4 | module.exports = (state, prev, send) => {
5 | return html`
6 |
7 | ${codemirror()}
8 | ${state.codemirror.value}
9 |
10 | `
11 | }
12 |
--------------------------------------------------------------------------------
/src/elements/codemirror.js:
--------------------------------------------------------------------------------
1 | const codemirror = require('../lib/codemirror-portal').create({
2 | namespace: 'codemirror'
3 | }, {
4 | lineNumbers: true,
5 | autofocus: true
6 | })
7 |
8 | module.exports = () => {
9 | if (codemirror.innerHTML) {
10 | const element = document.createElement('div')
11 | element.innerHTML = codemirror.innerHTML
12 | return element
13 | } else {
14 | return codemirror
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const sf = require('sheetify')
2 | const choo = require('choo')
3 |
4 | sf('normalize.css', { global: true })
5 | sf('./styles/main.css', { global: true })
6 | sf('codemirror/lib/codemirror.css', { global: true })
7 |
8 | const app = choo()
9 |
10 | if (process.env.NODE_ENV !== 'production') {
11 | const log = require('choo-log')
12 | app.use(log())
13 | }
14 |
15 | app.model(require('./models/codemirror'))
16 | app.router(['/', require('./views/home')])
17 |
18 | const tree = app.start()
19 |
20 | document.body.appendChild(tree)
21 |
--------------------------------------------------------------------------------
/scripts/build-prod.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Clean distribution directory.
4 | rm -rf dist && mkdir dist && mkdir dist/js
5 | # Copy static files to distribution.
6 | cp -r static/* dist
7 |
8 | # Duplicate index.html as 200.html for Surge pushState routing.
9 | cp static/index.html dist/200.html
10 |
11 | # Bundle the main js file.
12 |
13 | # add -d switch for sourcemapping and debugging production.
14 | NODE_ENV=production browserify -e src/index.js -o dist/js/main.js \
15 | -t envify \
16 | -t sheetify/transform \
17 | -g yo-yoify \
18 | -g unassertify \
19 | -g es2040 \
20 | -g uglifyify | uglifyjs
21 |
22 | echo 'Built dist directory'
23 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cm-wrapper
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/codemirror-portal.js:
--------------------------------------------------------------------------------
1 | const CodeMirror = require('codemirror')
2 | const defer = fn => setTimeout(fn, 0)
3 | const subscribers = []
4 | const broadcast = type => (...payload) =>
5 | subscribers.forEach(sub => {
6 | defer(sub(type, {
7 | evt: payload,
8 | doc: payload[0] && payload[0].doc || null,
9 | change: payload[1] || null
10 | }))
11 | })
12 |
13 | exports.subscribe = listener => subscribers.push(listener)
14 | exports.create = ({namespace}, options) => {
15 | const node = document.createElement('div')
16 |
17 | defer(() => {
18 | const editor = CodeMirror(node, options)
19 | editor.on('change', broadcast('change'))
20 | editor.on('focus', broadcast('focus'))
21 | editor.on('blur', broadcast('blur'))
22 | editor.on('scroll', broadcast('scroll'))
23 | })
24 | return node
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cm-wrapper",
3 | "version": "0.0.0",
4 | "description": "",
5 | "license": "MIT",
6 | "repository": "test/cm-wrapper",
7 | "scripts": {
8 | "start": "npm run dev:server",
9 | "deploy": "surge dist || exit 0",
10 | "dev:server": "scripts/dev-server.sh",
11 | "build:prod": "scripts/build-prod.sh",
12 | "lint": "standard --verbose | snazzy",
13 | "test": "npm run lint"
14 | },
15 | "dependencies": {
16 | "choo": "^5.6.1",
17 | "codemirror": "^5.26.0",
18 | "data.task": "^3.1.1",
19 | "vdom-thunk": "^3.0.0"
20 | },
21 | "devDependencies": {
22 | "browserify": "^14.4.0",
23 | "budo": "10.0.3",
24 | "choo-log": "^6.1.2",
25 | "es2040": "1.2.5",
26 | "envify": "^4.0.0",
27 | "normalize.css": "^7.0.0",
28 | "sheetify": "^6.0.2",
29 | "standard": "^10.0.2",
30 | "snazzy": "^7.0.0",
31 | "uglifyify": "^3.0.4",
32 | "unassertify": "^2.0.4",
33 | "yo-yoify": "^3.7.3"
34 | },
35 | "standard": {
36 | "ignore": [
37 | "scripts"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Matt McFarland
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/models/codemirror.js:
--------------------------------------------------------------------------------
1 | const { subscribe } = require('../lib/codemirror-portal')
2 |
3 | module.exports = {
4 | /* namespace the model so that it cannot access any properties and handlers in other models */
5 | namespace: 'codemirror',
6 | state: {
7 | value: '',
8 | isFocused: true,
9 | scroll: 0
10 | },
11 | reducers: {
12 | update: (action, state) => ({ value: action.value }),
13 | focusChange: (action, state) => ({ isFocused: action.focused }),
14 | scrollChange: (action, state) => ({ scroll: action.scroll })
15 | },
16 | effects: {
17 | change: (data, state, send, done) => {
18 | if (data.change && data.change.origin !== 'setValue') {
19 | send('codemirror:update', { value: data.doc.getValue() }, done)
20 | }
21 | },
22 | focus: (data, state, send, done) => {
23 | send('codemirror:focusChange', { focused: true }, done)
24 | },
25 | blur: (data, state, send, done) => {
26 | send('codemirror:focusChange', { focused: false }, done)
27 | },
28 | scroll: (data, state, send, done) => {
29 | send('codemirror:scrollChange', { scroll: data.doc.cm.getScrollInfo() }, done)
30 | }
31 | },
32 | subscriptions: [
33 | (send, done) =>
34 | subscribe((type, payload) => {
35 | send(`codemirror:${type}`, payload, done)
36 | })
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 | *.pid.lock
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 | dist
30 |
31 | # Dependency directories
32 | node_modules
33 | jspm_packages
34 |
35 | # Optional npm cache directory
36 | .npm
37 |
38 | # Optional REPL history
39 | .node_repl_history
40 | Contact GitHub API Training Shop Blog About
41 |
42 | # Mac OSX
43 | *.DS_Store
44 | .AppleDouble
45 | .LSOverride
46 |
47 | # Icon must end with two \r
48 | Icon
49 |
50 |
51 | # Thumbnails
52 | ._*
53 |
54 | # Files that might appear in the root of a volume
55 | .DocumentRevisions-V100
56 | .fseventsd
57 | .Spotlight-V100
58 | .TemporaryItems
59 | .Trashes
60 | .VolumeIcon.icns
61 | .com.apple.timemachine.donotpresent
62 |
63 | # Directories potentially created on remote AFP share
64 | .AppleDB
65 | .AppleDesktop
66 | Network Trash Folder
67 | Temporary Items
68 | .apdisk
69 |
70 |
71 | # Linux
72 |
73 | # temporary files which can be created if a process still has a handle open of a deleted file
74 | .fuse_hidden*
75 |
76 | # KDE directory preferences
77 | .directory
78 |
79 | # Linux trash folder which might appear on any partition or disk
80 | .Trash-*
81 |
82 | # Windows
83 |
84 | # Windows image file caches
85 | Thumbs.db
86 | ehthumbs.db
87 |
88 | # Folder config file
89 | Desktop.ini
90 |
91 | # Recycle Bin used on file shares
92 | $RECYCLE.BIN/
93 |
94 | # Windows Installer files
95 | *.cab
96 | *.msi
97 | *.msm
98 | *.msp
99 |
100 | # Windows shortcuts
101 | *.lnk
102 |
103 | # VS Code
104 | .vscode
105 |
106 | #IntelliJ
107 |
108 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
109 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
110 |
111 | # User-specific stuff:
112 | .idea/workspace.xml
113 | .idea/tasks.xml
114 | .idea/dictionaries
115 | .idea/vcs.xml
116 | .idea/jsLibraryMappings.xml
117 |
118 | # Sensitive or high-churn files:
119 | .idea/dataSources.ids
120 | .idea/dataSources.xml
121 | .idea/dataSources.local.xml
122 | .idea/sqlDataSources.xml
123 | .idea/dynamic.xml
124 | .idea/uiDesigner.xml
125 |
126 | # Gradle:
127 | .idea/gradle.xml
128 | .idea/libraries
129 |
130 | # Mongo Explorer plugin:
131 | .idea/mongoSettings.xml
132 |
133 | ## File-based project format:
134 | *.iws
135 |
136 | ## Plugin-specific files:
137 |
138 | # IntelliJ
139 | /out/
140 |
141 | # mpeltonen/sbt-idea plugin
142 | .idea_modules/
143 |
144 | # JIRA plugin
145 | atlassian-ide-plugin.xml
146 |
147 | # Crashlytics plugin (for Android Studio and IntelliJ)
148 | com_crashlytics_export_strings.xml
149 | crashlytics.properties
150 | crashlytics-build.properties
151 | fabric.properties
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # choo-codemirror
2 |
3 | How to wrap codemirror with choo
4 |
5 | ## The Portal
6 |
7 | Because codemirror has its own state management system, we need to be careful not to clobber ours.
8 | What we can do is create a portal, this is basically a concept that puts in place strict `message passing`
9 |
10 | ### lib/codemirror-portal
11 |
12 | ```javascript
13 | const CodeMirror = require('codemirror')
14 | const defer = fn => setTimeout(fn, 0)
15 | const subscribers = []
16 | const broadcast = type => (...payload) =>
17 | subscribers.forEach(sub => {
18 | defer(sub(type, {
19 | evt: payload,
20 | doc: payload[0] && payload[0].doc || null,
21 | change: payload[1] || null
22 | }))
23 | })
24 |
25 | exports.subscribe = listener => subscribers.push(listener)
26 | exports.create = ({namespace}, options) => {
27 | const node = document.createElement('div')
28 |
29 | defer(() => {
30 | const editor = CodeMirror(node, options)
31 | editor.on('change', broadcast('change'))
32 | editor.on('focus', broadcast('focus'))
33 | editor.on('blur', broadcast('blur'))
34 | editor.on('scroll', broadcast('scroll'))
35 | })
36 | return node
37 | }
38 | ```
39 |
40 | Here we can use `create` to return a new node that asynchronously turns into a codemirror instance,
41 | and `subscribe` which allows us to use a choo `model` to listen to state changes.
42 |
43 | ## Model
44 |
45 | ```javascript
46 |
47 | const { subscribe } = require('../lib/codemirror-portal')
48 |
49 | module.exports = {
50 | /* namespace the model so that it cannot access any properties and handlers in other models */
51 | namespace: 'codemirror',
52 | state: {
53 | value: '',
54 | isFocused: true,
55 | scroll: 0
56 | },
57 | reducers: {
58 | update: (action, state) => ({ value: action.value }),
59 | focusChange: (action, state) => ({ isFocused: action.focused }),
60 | scrollChange: (action, state) => ({ scroll: action.scroll })
61 | },
62 | effects: {
63 | change: (data, state, send, done) => {
64 | if (data.change && data.change.origin !== 'setValue') {
65 | send('codemirror:update', { value: data.doc.getValue() }, done)
66 | }
67 | },
68 | focus: (data, state, send, done) => {
69 | send('codemirror:focusChange', { focused: true }, done)
70 | },
71 | blur: (data, state, send, done) => {
72 | send('codemirror:focusChange', { focused: false }, done)
73 | },
74 | scroll: (data, state, send, done) => {
75 | send('codemirror:scrollChange', { scroll: data.doc.cm.getScrollInfo() }, done)
76 | }
77 | },
78 | subscriptions: [
79 | (send, done) =>
80 | subscribe((type, payload) => {
81 | send(`codemirror:${type}`, payload, done)
82 | })
83 | ]
84 | }
85 | ```
86 |
87 | As you can see we are using `subscriptions` to subscribe to the `portal`, and we use `effects` to
88 | carefully route the changes to the `reducers`
89 |
90 | ## Rendering
91 |
92 | Rendering was really tricky at first, because the `morphdom` does not see the modified `codemirror`, but
93 | rather the simple `div` that was created originally. To get around this, we need to grab the innerHTML
94 | of the `codemirror` and pass it to `bel`
95 |
96 | ```javascript
97 | const codemirror = require('../lib/codemirror-portal').create({
98 | namespace: 'codemirror'
99 | }, {
100 | lineNumbers: true,
101 | autofocus: true
102 | })
103 |
104 | module.exports = () => {
105 | if (codemirror.innerHTML) {
106 | const element = document.createElement('div')
107 | element.innerHTML = codemirror.innerHTML
108 | return element
109 | } else {
110 | return codemirror
111 | }
112 | }
113 | ```
114 |
115 | This ensures the morphDOM is capable of seeing the entire lsit of nodes so when things update it doesnt wipe out
116 | the whole thing.
117 |
118 | Finally we can see our codemirror here:
119 |
120 | ```javascript
121 | const html = require('choo/html')
122 | const codemirror = require('../elements/codemirror')
123 |
124 | module.exports = (state, prev, send) => {
125 | return html`
126 |
127 | ${codemirror()}
128 | ${state.codemirror.value}
129 |
130 | `
131 | }
132 | ```
133 |
134 | I hope this was helpful.
135 |
--------------------------------------------------------------------------------