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