├── .DS_Store ├── demoit.zip ├── _assets ├── .DS_Store ├── demoit.png ├── demoit.psd ├── layouts.psd ├── demoit.app.psd ├── demoit_32x32.png ├── demoit_64x64.png ├── demoit_dark.png ├── demoit_light.png ├── demoit_100x100.png └── demoit_200x200.png ├── dist ├── img │ └── demoit_64x64.png ├── resources │ ├── state-example2.json │ ├── state-example.json │ └── FileSaver.min.js └── index.html ├── src ├── img │ └── demoit_64x64.png ├── js │ ├── utils │ │ ├── setTheme.js │ │ ├── transpile.js │ │ ├── localStorage.js │ │ ├── executeCSS.js │ │ ├── commitDiff.js │ │ ├── codeMirrorCommands.js │ │ ├── svg.js │ │ ├── cleanUpMarkdown.js │ │ ├── index.js │ │ ├── element.js │ │ └── icons.js │ ├── story │ │ ├── getTitleFromCommitMessage.js │ │ ├── setAnnotationLink.js │ │ ├── codeMirror.js │ │ ├── renderGraph.js │ │ ├── renderDiffs.js │ │ ├── renderCommits.js │ │ └── index.js │ ├── constants.js │ ├── popups │ │ ├── confirmPopUp.js │ │ ├── newFilePopUp.js │ │ ├── editFilePopUp.js │ │ ├── editNamePopUp.js │ │ ├── popup.js │ │ └── settingsPopUp.js │ ├── settings.js │ ├── annotate.js │ ├── storyPreview.js │ ├── providers │ │ └── api.js │ ├── dependencies.js │ ├── download.js │ ├── output.js │ ├── execute.js │ ├── console.js │ ├── __tests__ │ │ └── commitDiff.spec.js │ ├── index.js │ ├── storyReadOnly.js │ ├── layout.js │ ├── editor.js │ ├── statusBar.js │ └── state.js ├── resources │ ├── state-example2.json │ ├── state-example.json │ └── FileSaver.min.js ├── js-vendor │ ├── colorize.js │ ├── runmode.js │ ├── overlay.js │ ├── mark-selection.js │ ├── split.js │ ├── gfm.js │ ├── jsx.js │ ├── htmlmixed.js │ ├── match-highlighter.js │ ├── matchbrackets.js │ └── closebrackets.js ├── css │ ├── la.css │ ├── light_theme.css │ └── dark_theme.css └── index.html ├── .npmignore ├── scripts └── zipit.js ├── webpack.config.prod.js ├── webpack.config.js ├── samples ├── HTML+CSS.json ├── Vue.json ├── React.json ├── index.html └── Canvas.json ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── .eslintrc /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/.DS_Store -------------------------------------------------------------------------------- /demoit.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/demoit.zip -------------------------------------------------------------------------------- /_assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/.DS_Store -------------------------------------------------------------------------------- /_assets/demoit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.png -------------------------------------------------------------------------------- /_assets/demoit.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.psd -------------------------------------------------------------------------------- /_assets/layouts.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/layouts.psd -------------------------------------------------------------------------------- /_assets/demoit.app.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit.app.psd -------------------------------------------------------------------------------- /_assets/demoit_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_32x32.png -------------------------------------------------------------------------------- /_assets/demoit_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_64x64.png -------------------------------------------------------------------------------- /_assets/demoit_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_dark.png -------------------------------------------------------------------------------- /_assets/demoit_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_light.png -------------------------------------------------------------------------------- /dist/img/demoit_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/dist/img/demoit_64x64.png -------------------------------------------------------------------------------- /src/img/demoit_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/src/img/demoit_64x64.png -------------------------------------------------------------------------------- /_assets/demoit_100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_100x100.png -------------------------------------------------------------------------------- /_assets/demoit_200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/demoit/master/_assets/demoit_200x200.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demoit.png 2 | src 3 | webpack.config.js 4 | webpack.config.prod.js 5 | *.log 6 | yarn.lock 7 | scripts 8 | NOTES 9 | _assets 10 | .tmp -------------------------------------------------------------------------------- /src/js/utils/setTheme.js: -------------------------------------------------------------------------------- 1 | import el from './element'; 2 | 3 | export default function setTheme(theme) { 4 | el.withRelaxedCleanup('.app').attr('class', 'app ' + theme); 5 | } -------------------------------------------------------------------------------- /src/js/story/getTitleFromCommitMessage.js: -------------------------------------------------------------------------------- 1 | import { truncate } from '../utils'; 2 | import cleanUpMarkdown from '../utils/cleanUpMarkdown'; 3 | 4 | export default function getTitleFromCommitMessage(text) { 5 | return cleanUpMarkdown(truncate(text.split('\n').shift(), 36)); 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/zipit.js: -------------------------------------------------------------------------------- 1 | var zipFolder = require('zip-folder'); 2 | 3 | zipFolder(__dirname + '/../dist', __dirname + '/../demoit.zip', function (err) { 4 | if (err) { 5 | console.log('Zipping extension failed.', err); 6 | } else { 7 | console.log('Zipping extension successful.'); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/js/utils/transpile.js: -------------------------------------------------------------------------------- 1 | const OPTIONS = { 2 | presets: [ 3 | 'react', 4 | ['es2015', 5 | { 'modules': false } 6 | ], 7 | 'es2016', 8 | 'es2017', 9 | 'stage-0', 10 | 'stage-1', 11 | 'stage-2', 12 | 'stage-3' 13 | ], 14 | plugins: [ 15 | 'transform-es2015-modules-commonjs' 16 | ] 17 | }; 18 | 19 | export default function preprocess(str) { 20 | const { code } = Babel.transform(str, OPTIONS); 21 | 22 | return code; 23 | }; 24 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | function matchAgainstURL(pattern) { 2 | return window.location.href.match(pattern); 3 | } 4 | 5 | export const DEBUG = false; 6 | 7 | export const IS_PROD = matchAgainstURL(/^https:\/\/poet.krasimir.now.sh/) || matchAgainstURL(/^http:\/\/localhost:8004/); 8 | export const DEV = matchAgainstURL(/^http:\/\/localhost:8004/); 9 | 10 | export const SAVE_DEMO_URL = !DEV ? 'https://poet.krasimir.now.sh/api/demo' : 'http://localhost:8004/api/demo'; 11 | export const GET_DEMOS_URL = !DEV ? 'https://poet.krasimir.now.sh/api/profile' : 'http://localhost:8004/api/profile'; 12 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: [ 5 | './src/js/index.js' 6 | ], 7 | output: { 8 | path: path.resolve(__dirname, '.tmp'), 9 | filename: 'demoit.js' 10 | }, 11 | mode: 'production', 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.m?js$/, 16 | exclude: /(node_modules|bower_components)/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env'], 21 | plugins: ['@babel/plugin-transform-runtime'] 22 | } 23 | } 24 | } 25 | ] 26 | } 27 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: [ 5 | './src/js/index.js' 6 | ], 7 | output: { 8 | path: path.resolve(__dirname, '.tmp'), 9 | filename: 'demoit.js' 10 | }, 11 | mode: 'development', 12 | watch: true, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.m?js$/, 17 | exclude: /(node_modules|bower_components)/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'], 22 | plugins: ['@babel/plugin-transform-runtime'] 23 | } 24 | } 25 | } 26 | ] 27 | } 28 | }; -------------------------------------------------------------------------------- /src/js/popups/confirmPopUp.js: -------------------------------------------------------------------------------- 1 | import { CHECK_ICON, CLOSE_ICON } from '../utils/icons'; 2 | import createPopup from './popup'; 3 | 4 | export default function confirmPopUp(title, message, onChange) { 5 | createPopup({ 6 | title, 7 | content: ` 8 |

${ message }

9 | 10 | 11 | `, 12 | onRender({ yesButton, noButton, closePopup }) { 13 | const save = (value) => { 14 | onChange(value); 15 | closePopup(); 16 | }; 17 | 18 | yesButton.onClick(() => save(true)); 19 | noButton.onClick(() => save(false)); 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/js/utils/localStorage.js: -------------------------------------------------------------------------------- 1 | const AVAILABLE = (function () { 2 | const test = 'test'; 3 | 4 | try { 5 | localStorage.setItem(test, test); 6 | localStorage.removeItem(test); 7 | return true; 8 | } catch(e) { 9 | return false; 10 | } 11 | })(); 12 | 13 | const LS = function (key, value) { 14 | if (!AVAILABLE) return null; 15 | 16 | // setting 17 | if (typeof value !== 'undefined') { 18 | localStorage.setItem(key, JSON.stringify(value)); 19 | } 20 | 21 | // reading 22 | const data = localStorage.getItem(key); 23 | 24 | try { 25 | if (data) return JSON.parse(data); 26 | } catch(error) { 27 | console.error(`There is some data in the local storage under the ${ key } key. However, it is not a valid JSON.`); 28 | } 29 | return null 30 | } 31 | 32 | export default LS; -------------------------------------------------------------------------------- /src/js/settings.js: -------------------------------------------------------------------------------- 1 | import settingsPopUp from './popups/settingsPopUp'; 2 | import download from './download'; 3 | 4 | const filterDeps = deps => deps.filter(dep => (dep !== '' && dep !== '\n')); 5 | 6 | export default function settings(state, render, executeCurrentFile) { 7 | const showSettingsPopUp = tab => settingsPopUp( 8 | download(state), 9 | state.getEditorSettings(), 10 | filterDeps(state.getDependencies()).join('\n'), 11 | function onDepsUpdated(newDeps) { 12 | if (newDeps) { 13 | state.setDependencies(newDeps); 14 | executeCurrentFile(); 15 | } 16 | }, 17 | function onGeneralUpdate(newTheme, newLayout) { 18 | state.updateThemeAndLayout(newLayout, newTheme); 19 | render(); 20 | }, 21 | tab, 22 | state.version() 23 | ); 24 | 25 | return showSettingsPopUp; 26 | } 27 | -------------------------------------------------------------------------------- /src/js/story/setAnnotationLink.js: -------------------------------------------------------------------------------- 1 | export default function setAnnotationLink(editor, code, list, activeFile) { 2 | let thingToInsert = ''; 3 | 4 | try { 5 | let { line, ch } = editor.getCursor(); 6 | const currentLine = editor.getValue().split('\n')[line]; 7 | 8 | if ( 9 | currentLine.charAt(ch) === ')' && 10 | currentLine.charAt(ch - 1) === '(' && 11 | currentLine.charAt(ch - 2) === ']' 12 | ) { 13 | let { anchor, head } = list.shift(); 14 | 15 | thingToInsert = ['a', activeFile, anchor.line, anchor.ch, head.line, head.ch ].join(':'); 16 | } else if ( 17 | currentLine.charAt(ch - 1) === '' && 18 | currentLine.charAt(ch + 1) === '' 19 | ) { 20 | thingToInsert = code; 21 | } 22 | } catch (error) { 23 | console.log('Error while setting annotation.'); 24 | } 25 | 26 | editor.replaceSelection(thingToInsert); 27 | } 28 | -------------------------------------------------------------------------------- /src/js/utils/executeCSS.js: -------------------------------------------------------------------------------- 1 | const STYLES_CACHE = {}; 2 | const guaranteeValidIdName = filename => filename.replace(/\./g, '_'); 3 | 4 | export const injectCSS = function (css, id) { 5 | const node = document.querySelector('#' + id); 6 | 7 | if (node) { 8 | node.innerHTML = css; 9 | } else { 10 | const node = document.createElement('style'); 11 | 12 | id && node.setAttribute('id', id); 13 | node.innerHTML = css; 14 | document.body.appendChild(node); 15 | } 16 | }; 17 | 18 | export const executeCSS = function (filename, content) { 19 | if (!STYLES_CACHE[filename]) { 20 | const node = document.createElement('style'); 21 | 22 | node.setAttribute('id', guaranteeValidIdName(filename)); 23 | node.innerHTML = content; 24 | setTimeout(function () { 25 | document.body.appendChild(node); 26 | }, 1); 27 | STYLES_CACHE[filename] = node; 28 | } else { 29 | STYLES_CACHE[filename].innerHTML = content; 30 | } 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /src/js/popups/newFilePopUp.js: -------------------------------------------------------------------------------- 1 | import createPopup from './popup'; 2 | import { CHECK_ICON } from '../utils/icons'; 3 | 4 | const ENTER_KEY = 13; 5 | 6 | export default function newFilePopUp() { 7 | return new Promise(done => createPopup({ 8 | title: 'New file', 9 | content: ` 10 | 11 | 12 | `, 13 | cleanUp() { 14 | done(); 15 | }, 16 | onRender({ filenameInput, saveButton, closePopup }) { 17 | const save = () => { 18 | filenameInput.e.value !== '' && done(filenameInput.e.value); 19 | closePopup(); 20 | }; 21 | 22 | filenameInput.e.focus(); 23 | filenameInput.onKeyUp(e => { 24 | if (e.keyCode === ENTER_KEY) { 25 | save(); 26 | } 27 | }); 28 | saveButton.onClick(save); 29 | } 30 | })); 31 | }; 32 | -------------------------------------------------------------------------------- /samples/HTML+CSS.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor": { 3 | "theme": "light", 4 | "statusBar": true, 5 | "layout": { 6 | "name": "default", 7 | "direction": "horizontal", 8 | "sizes": [ 9 | 30, 10 | 70 11 | ], 12 | "elements": [ 13 | { 14 | "direction": "vertical", 15 | "sizes": [ 16 | 50, 17 | 50 18 | ], 19 | "elements": [ 20 | "output", 21 | "log" 22 | ] 23 | }, 24 | "editor" 25 | ] 26 | } 27 | }, 28 | "dependencies": [], 29 | "files": [ 30 | { 31 | "content": "import 'styles.css';\nimport 'markup.html';", 32 | "filename": "app.js", 33 | "editing": false, 34 | "entryPoint": true 35 | }, 36 | { 37 | "content": "h1 {\n color: purple;\n}", 38 | "filename": "styles.css", 39 | "editing": false 40 | }, 41 | { 42 | "content": "

Hello app

", 43 | "filename": "markup.html", 44 | "editing": false 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Krasimir Tsonev 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 | -------------------------------------------------------------------------------- /dist/resources/state-example2.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor": { 3 | "theme": "light", 4 | "statusBar": false, 5 | "layout": { 6 | "elements": [ 7 | { 8 | "name": "story", 9 | "elements": [] 10 | }, 11 | { 12 | "name": "story-preview", 13 | "elements": [] 14 | } 15 | ], 16 | "direction": "horizontal" 17 | } 18 | }, 19 | "dependencies": [], 20 | "files": { 21 | "working": [ 22 | [ 23 | "code.js", 24 | { 25 | "c": "document.querySelector('#output').innerHTML = 'Hello world';\n\nconsole.log('Hello world');" 26 | } 27 | ] 28 | ], 29 | "head": "_2", 30 | "i": 2, 31 | "stage": [], 32 | "commits": { 33 | "_1": { 34 | "message": "first commit", 35 | "parent": null, 36 | "files": "[[\"code.js\",{\"c\":\"document.querySelector('#output').innerHTML = 'Hello world';\\n\\nconsole.log('Hello world');\"}]]" 37 | }, 38 | "_2": { 39 | "message": "second commit", 40 | "parent": "_1", 41 | "files": "" 42 | } 43 | } 44 | }, 45 | "v": "7.1.0" 46 | } -------------------------------------------------------------------------------- /src/resources/state-example2.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor": { 3 | "theme": "light", 4 | "statusBar": false, 5 | "layout": { 6 | "elements": [ 7 | { 8 | "name": "story", 9 | "elements": [] 10 | }, 11 | { 12 | "name": "story-preview", 13 | "elements": [] 14 | } 15 | ], 16 | "direction": "horizontal" 17 | } 18 | }, 19 | "dependencies": [], 20 | "files": { 21 | "working": [ 22 | [ 23 | "code.js", 24 | { 25 | "c": "document.querySelector('#output').innerHTML = 'Hello world';\n\nconsole.log('Hello world');" 26 | } 27 | ] 28 | ], 29 | "head": "_2", 30 | "i": 2, 31 | "stage": [], 32 | "commits": { 33 | "_1": { 34 | "message": "first commit", 35 | "parent": null, 36 | "files": "[[\"code.js\",{\"c\":\"document.querySelector('#output').innerHTML = 'Hello world';\\n\\nconsole.log('Hello world');\"}]]" 37 | }, 38 | "_2": { 39 | "message": "second commit", 40 | "parent": "_1", 41 | "files": "" 42 | } 43 | } 44 | }, 45 | "v": "7.1.0" 46 | } -------------------------------------------------------------------------------- /src/js/utils/commitDiff.js: -------------------------------------------------------------------------------- 1 | const gitfred = require('gitfred'); 2 | 3 | const git = gitfred(); 4 | const toDict = arr => arr.reduce((dict, file) => { 5 | dict[file[0]] = file[1].c; 6 | return dict; 7 | }, {}); 8 | 9 | module.exports = function commitDiff(oldFiles, newFiles) { 10 | const diffs = []; 11 | const dictA = toDict(oldFiles); 12 | const dictB = toDict(newFiles); 13 | 14 | Object.keys(dictA).forEach(filename => { 15 | if (dictB.hasOwnProperty(filename)) { 16 | const strDiff = git.calcStrDiff(dictA[filename], dictB[filename]); 17 | 18 | if (strDiff !== null) { 19 | diffs.push(['E', filename, strDiff]); 20 | } 21 | } else { 22 | diffs.push(['D', filename, dictA[filename]]); 23 | Object.keys(dictB).forEach(filenameInB => { 24 | if (dictA[filename] === dictB[filenameInB]) { 25 | diffs.push(['R', filename, filenameInB]); 26 | delete dictB[filenameInB]; 27 | } 28 | }); 29 | } 30 | }); 31 | Object.keys(dictB).forEach(filename => { 32 | if (!dictA.hasOwnProperty(filename)) { 33 | diffs.push(['N', filename, dictB[filename]]); 34 | } 35 | }); 36 | 37 | return diffs; 38 | }; 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | NOTES 64 | 65 | *.log 66 | 67 | .vscode 68 | .tmp -------------------------------------------------------------------------------- /src/js/story/codeMirror.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-sequences */ 2 | 3 | import defineCodeMirrorCommands from '../utils/codeMirrorCommands'; 4 | 5 | export default function codeMirror(container, editorSettings, value, onSave, onChange, onCancel) { 6 | defineCodeMirrorCommands(CodeMirror); 7 | 8 | const editor = CodeMirror(container.e, { 9 | value: value || '', 10 | mode: 'gfm', 11 | tabSize: 2, 12 | lineNumbers: false, 13 | autofocus: true, 14 | foldGutter: false, 15 | gutters: [], 16 | styleSelectedText: true, 17 | lineWrapping: true, 18 | highlightFormatting: true, 19 | ...editorSettings 20 | }); 21 | const save = () => onSave(editor.getValue()); 22 | const change = () => onChange(editor.getValue()); 23 | 24 | editor.on('change', change); 25 | editor.setOption('extraKeys', { 26 | 'Ctrl-S': save, 27 | 'Cmd-S': save, 28 | Esc: () => onCancel(editor.getValue()), 29 | 'Ctrl-Enter': () => (save(), onCancel(editor.getValue())), 30 | 'Cmd-Enter': () => (save(), onCancel(editor.getValue())) 31 | }); 32 | CodeMirror.normalizeKeyMap(); 33 | setTimeout(() => { 34 | editor.focus(); 35 | console.log(editor.defaultTextHeight()); 36 | }, 50); 37 | 38 | return editor; 39 | } 40 | -------------------------------------------------------------------------------- /samples/Vue.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor": { 3 | "theme": "light", 4 | "statusBar": true, 5 | "layout": { 6 | "name": "default", 7 | "direction": "horizontal", 8 | "sizes": [ 9 | 30, 10 | 70 11 | ], 12 | "elements": [ 13 | { 14 | "direction": "vertical", 15 | "sizes": [ 16 | 50, 17 | 50 18 | ], 19 | "elements": [ 20 | "output", 21 | "log" 22 | ] 23 | }, 24 | "editor" 25 | ] 26 | } 27 | }, 28 | "dependencies": [ 29 | "https://unpkg.com/vue@2.5.21/dist/vue.min.js", 30 | "./resources/marked@0.3.6.js", 31 | "./resources/lodash@4.16.0.js" 32 | ], 33 | "files": [ 34 | { 35 | "content": "document.querySelector('.output').innerHTML = `\n
\n \n
\n
\n`;\n\nnew Vue({\n el: '#editor',\n data: {\n input: '# hello'\n },\n computed: {\n compiledMarkdown: function () {\n return marked(this.input, { sanitize: true })\n }\n },\n methods: {\n update: _.debounce(function (e) {\n this.input = e.target.value\n }, 300)\n }\n});", 36 | "filename": "Vue.js", 37 | "editing": false 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /src/js/story/renderGraph.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, max-len */ 2 | 3 | import { connectCommits, empty as emptySVGGraph } from '../utils/svg'; 4 | import el from '../utils/element'; 5 | import { DEBUG } from '../constants'; 6 | 7 | const SVG_X = 4; 8 | const SVG_INITIAL_Y = 25; 9 | 10 | export default function renderGraph(sortedCommits, tree) { 11 | setTimeout(() => { 12 | emptySVGGraph(); 13 | 14 | DEBUG && console.log(JSON.stringify(sortedCommits.map(({ hash, position }) => ({ hash, position })), null, 2)); 15 | 16 | const { connections, commitsYs } = renderCommitGraphs(getYValueOfCommitElement(sortedCommits[0].hash), tree); 17 | 18 | connections.forEach(([ hashA, hashB ]) => connectCommits(SVG_X, SVG_INITIAL_Y + commitsYs[hashA], SVG_INITIAL_Y + commitsYs[hashB])); 19 | }, 30); 20 | } 21 | function renderCommitGraphs(rootY, { parent, hash, derivatives }, result = { commitsYs: {}, connections: [] }) { 22 | result.commitsYs[hash] = getYValueOfCommitElement(hash) - rootY; 23 | if (parent !== null) { 24 | result.connections.push([ parent, hash ]); 25 | } 26 | if (derivatives.length > 0) { 27 | derivatives.forEach(commit => renderCommitGraphs(rootY, commit, result)); 28 | } 29 | return result; 30 | } 31 | function getYValueOfCommitElement(hash) { 32 | if (el.exists('#c' + hash)) { 33 | return el('#c' + hash).e.getBoundingClientRect().top + 0.3; 34 | } 35 | return 0; 36 | } 37 | -------------------------------------------------------------------------------- /src/js/annotate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import el from './utils/element'; 3 | 4 | export default function (state) { 5 | if (!el.exists('#annotate')) return; 6 | 7 | const git = state.git(); 8 | 9 | window.addEventListener('message', function (e) { 10 | if (e.data.checkoutTo) { 11 | git.checkout(e.data.checkoutTo); 12 | } 13 | }); 14 | 15 | const container = el.withFallback('#annotate'); 16 | const demoId = state.getDemoId(); 17 | const preview = (input, hash, form) => { 18 | if (git.head() !== null) { 19 | input.prop('value', JSON.stringify(git.export())); 20 | hash.prop('value', git.head()); 21 | form.e.submit(); 22 | } 23 | }; 24 | 25 | container.content(` 26 |
27 | 28 | 29 |
30 | 15 | 16 | 17 | 18 | 21 | 22 | 28 | 29 | 32 | 33 | 38 | 39 | 44 | 45 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demoit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 21 | 22 | 28 | 29 | 32 | 33 | 38 | 39 | 44 | 45 | 51 | 52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /dist/resources/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Depricated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;return b.open("HEAD",a,!1),b.send(),200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||"object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"object"==typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}};f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); -------------------------------------------------------------------------------- /src/resources/FileSaver.min.js: -------------------------------------------------------------------------------- 1 | (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Depricated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(b,c,d){var e=new XMLHttpRequest;e.open("GET",b),e.responseType="blob",e.onload=function(){a(e.response,c,d)},e.onerror=function(){console.error("could not download file")},e.send()}function d(a){var b=new XMLHttpRequest;return b.open("HEAD",a,!1),b.send(),200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.saveAs||"object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(a,b,d,e){if(e=e||open("","_blank"),e&&(e.document.title=e.document.body.innerText="downloading..."),"string"==typeof a)return c(a,b,d);var g="application/octet-stream"===a.type,h=/constructor/i.test(f.HTMLElement)||f.safari,i=/CriOS\/[\d]+/.test(navigator.userAgent);if((i||g&&h)&&"object"==typeof FileReader){var j=new FileReader;j.onloadend=function(){var a=j.result;a=i?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),e?e.location.href=a:location=a,e=null},j.readAsDataURL(a)}else{var k=f.URL||f.webkitURL,l=k.createObjectURL(a);e?e.location=l:location.href=l,e=null,setTimeout(function(){k.revokeObjectURL(l)},4E4)}};f.saveAs=a.saveAs=a,"undefined"!=typeof module&&(module.exports=a)}); -------------------------------------------------------------------------------- /src/js/popups/popup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-use-before-define */ 2 | import el from '../utils/element'; 3 | import { CLOSE_ICON } from '../utils/icons'; 4 | 5 | const ESC_KEY = 27; 6 | 7 | const DEFAULT_MARKUP = ({ title, content }) => `
8 |

${ title }

9 | 10 | ${ content } 11 |
`; 12 | const MULTIPLE_PAGES_MARKUP = ({ buttons, content }, index) => `
13 | 16 | 17 |
${ content[index] }
18 |
`; 19 | 20 | export default function popup(config) { 21 | const container = el.fromString(''); 22 | const body = el.withRelaxedCleanup('body'); 23 | const layout = el.withRelaxedCleanup('.layout'); 24 | const escHandler = e => (e.keyCode === ESC_KEY && close()); 25 | const removeKeyUpListener = body.onKeyUp(escHandler); 26 | const close = () => { 27 | removeKeyUpListener(); 28 | container.css('opacity', 0); 29 | config.cleanUp && config.cleanUp(); 30 | setTimeout(() => container.destroy(), 200); 31 | layout.css('filter', 'none'); 32 | }; 33 | const render = (markup) => { 34 | container.content(markup).forEach(button => { 35 | const dataExport = button.attr('data-export'); 36 | 37 | if (dataExport === 'close') { 38 | button.onClick(close); 39 | } else if (dataExport.match(/^page/)) { 40 | button.onClick(() => render(MULTIPLE_PAGES_MARKUP(config, Number(dataExport.split(':').pop())))) 41 | } 42 | }); 43 | config.onRender && config.onRender({ 44 | closePopup: close, 45 | ...container.namedExports() 46 | }); 47 | }; 48 | 49 | layout.css('filter', 'blur(2px)'); 50 | container.appendTo(body); 51 | render('buttons' in config ? MULTIPLE_PAGES_MARKUP(config, config.defaultTab) : DEFAULT_MARKUP(config)); 52 | setTimeout(() => container.css('opacity', 1), 1); 53 | }; 54 | -------------------------------------------------------------------------------- /samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Demoit samples 8 | 32 | 33 | 34 |
35 |

Empty

36 | 37 |
38 | 39 |
40 | 41 |
42 |

React

43 | 44 |
45 | 46 |
47 | 48 |
49 |

Vue

50 | 51 |
52 | 53 |
54 | 55 |
56 |

HTML+CSS

57 | 58 |
59 | 60 |
61 | 62 |
63 |

Canvas

64 | 65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/js-vendor/runmode.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | CodeMirror.runMode = function(string, modespec, callback, options) { 15 | var mode = CodeMirror.getMode(CodeMirror.defaults, modespec); 16 | var ie = /MSIE \d/.test(navigator.userAgent); 17 | var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); 18 | 19 | if (callback.appendChild) { 20 | var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize; 21 | var node = callback, col = 0; 22 | node.innerHTML = ""; 23 | callback = function(text, style) { 24 | if (text == "\n") { 25 | // Emitting LF or CRLF on IE8 or earlier results in an incorrect display. 26 | // Emitting a carriage return makes everything ok. 27 | node.appendChild(document.createTextNode(ie_lt9 ? '\r' : text)); 28 | col = 0; 29 | return; 30 | } 31 | var content = ""; 32 | // replace tabs 33 | for (var pos = 0;;) { 34 | var idx = text.indexOf("\t", pos); 35 | if (idx == -1) { 36 | content += text.slice(pos); 37 | col += text.length - pos; 38 | break; 39 | } else { 40 | col += idx - pos; 41 | content += text.slice(pos, idx); 42 | var size = tabSize - col % tabSize; 43 | col += size; 44 | for (var i = 0; i < size; ++i) content += " "; 45 | pos = idx + 1; 46 | } 47 | } 48 | 49 | if (style) { 50 | var sp = node.appendChild(document.createElement("span")); 51 | sp.className = "cm-" + style.replace(/ +/g, " cm-"); 52 | sp.appendChild(document.createTextNode(content)); 53 | } else { 54 | node.appendChild(document.createTextNode(content)); 55 | } 56 | }; 57 | } 58 | 59 | var lines = CodeMirror.splitLines(string), state = (options && options.state) || CodeMirror.startState(mode); 60 | for (var i = 0, e = lines.length; i < e; ++i) { 61 | if (i) callback("\n"); 62 | var stream = new CodeMirror.StringStream(lines[i]); 63 | if (!stream.string && mode.blankLine) mode.blankLine(state); 64 | while (!stream.eol()) { 65 | var style = mode.token(stream, state); 66 | callback(stream.current(), style, i, stream.start, state); 67 | stream.start = stream.pos; 68 | } 69 | } 70 | }; 71 | 72 | }); -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, no-sequences */ 2 | import pkg from '../../package.json'; 3 | import el from './utils/element'; 4 | import layout from './layout'; 5 | import editor, { ON_SELECT, ON_FILE_CHANGE, ON_FILE_SAVE } from './editor'; 6 | import createState from './state'; 7 | import newFilePopUp from './popups/newFilePopUp'; 8 | import editFilePopUp from './popups/editFilePopUp'; 9 | import editNamePopUp from './popups/editNamePopUp'; 10 | import settings from './settings'; 11 | import statusBar from './statusBar'; 12 | import story from './story'; 13 | import storyReadOnly from './storyReadOnly'; 14 | import storyPreview from './storyPreview'; 15 | import annotate from './annotate'; 16 | import { DEBUG } from './constants'; 17 | 18 | createState(pkg.version).then(state => { 19 | async function render() { 20 | layout(state); 21 | 22 | let setFilePendingStatus = () => {}; 23 | const addToStory = story(state, () => executeCurrentFile()); 24 | const { loadFileInEditor: executeCurrentFile, save: saveCurrentFile } = await editor( 25 | state, 26 | (event, data, editor) => { 27 | DEBUG && console.log('editor event=' + event); 28 | switch (event) { 29 | case ON_SELECT: 30 | addToStory(data, editor); 31 | break; 32 | case ON_FILE_CHANGE: 33 | setFilePendingStatus(true); 34 | break; 35 | case ON_FILE_SAVE: 36 | setFilePendingStatus(false); 37 | break; 38 | } 39 | } 40 | ); 41 | 42 | storyReadOnly(state, () => executeCurrentFile()); 43 | storyPreview(state); 44 | annotate(state); 45 | executeCurrentFile(); 46 | 47 | const showSettings = settings( 48 | state, 49 | () => { 50 | state.removeListeners(), 51 | el.destroy(); 52 | render(); 53 | }, 54 | () => executeCurrentFile() 55 | ); 56 | 57 | setFilePendingStatus = statusBar( 58 | state, 59 | function showFile(filename) { 60 | state.setActiveFile(filename); 61 | executeCurrentFile(); 62 | }, 63 | async function newFile() { 64 | const newFilename = await newFilePopUp(); 65 | 66 | if (newFilename) { 67 | state.addNewFile(newFilename); 68 | executeCurrentFile(); 69 | } 70 | }, 71 | async function editFile(filename) { 72 | editFilePopUp( 73 | filename, 74 | state.getNumOfFiles(), 75 | function onDelete() { 76 | state.deleteFile(filename); 77 | executeCurrentFile(); 78 | }, 79 | function onRename(newName) { 80 | state.renameFile(filename, newName); 81 | executeCurrentFile(); 82 | }, 83 | function onSetAsEntryPoint() { 84 | state.setEntryPoint(filename); 85 | executeCurrentFile(); 86 | } 87 | ); 88 | }, 89 | showSettings, 90 | saveCurrentFile, 91 | function editName() { 92 | editNamePopUp(state.meta(), meta => state.meta(meta)); 93 | } 94 | ); 95 | }; 96 | render(); 97 | }); 98 | -------------------------------------------------------------------------------- /src/css/light_theme.css: -------------------------------------------------------------------------------- 1 | .cm-s-light.CodeMirror { 2 | background: #fff; 3 | color: #24292e; } 4 | 5 | .cm-s-light .CodeMirror-gutters { 6 | background: #fff; 7 | border-right-width: 0; } 8 | 9 | .cm-s-light .CodeMirror-guttermarker { 10 | color: white; } 11 | 12 | .cm-s-light .CodeMirror-guttermarker-subtle { 13 | color: #d0d0d0; } 14 | 15 | .cm-s-light .CodeMirror-linenumber { 16 | color: #959da5; 17 | padding: 0 16px 0 16px; } 18 | 19 | .cm-s-light .CodeMirror-cursor { 20 | border-left: 1px solid #24292e; } 21 | 22 | .cm-s-light div.CodeMirror-selected, 23 | .cm-s-light .CodeMirror-line::selection, 24 | .cm-s-light .CodeMirror-line > span::selection, 25 | .cm-s-light .CodeMirror-line > span > span::selection, 26 | .cm-s-light .CodeMirror-line::-moz-selection, 27 | .cm-s-light .CodeMirror-line > span::-moz-selection, 28 | .cm-s-light .CodeMirror-line > span > span::-moz-selection { 29 | background: #c8c8fa; } 30 | 31 | .cm-s-light .CodeMirror-activeline-background { 32 | background: #fafbfc; } 33 | 34 | .cm-s-light .CodeMirror-lines { 35 | font-family: inherit; 36 | font-size: inherit; 37 | background: #fff; 38 | line-height: 1.5; } 39 | 40 | .cm-s-light .cm-comment { 41 | color: #6a737d; } 42 | 43 | .cm-s-light .cm-constant { 44 | color: #005cc5; } 45 | 46 | .cm-s-light .cm-entity { 47 | font-weight: normal; 48 | font-style: normal; 49 | text-decoration: none; 50 | color: #6f42c1; } 51 | 52 | .cm-s-light .cm-keyword, .cm-s-light .cm-qualifier { 53 | font-weight: normal; 54 | font-style: normal; 55 | text-decoration: none; 56 | color: #d73a49; } 57 | 58 | .cm-s-light .cm-storage { 59 | color: #d73a49; } 60 | 61 | .cm-s-light .cm-string { 62 | font-weight: normal; 63 | font-style: normal; 64 | text-decoration: none; 65 | color: #03941e; } 66 | 67 | .cm-s-light .cm-support { 68 | font-weight: normal; 69 | font-style: normal; 70 | text-decoration: none; 71 | color: #005cc5; } 72 | 73 | .cm-s-light .cm-property { 74 | font-weight: normal; 75 | font-style: normal; 76 | text-decoration: none; 77 | color: #245082; 78 | } 79 | 80 | .cm-s-light .cm-variable { 81 | font-weight: normal; 82 | font-style: normal; 83 | text-decoration: none; 84 | color: #e36209; } 85 | 86 | .cm-s-light .cm-tag { 87 | font-weight: normal; 88 | font-style: normal; 89 | text-decoration: none; 90 | color: #0d6eab; } 91 | 92 | .cm-s-light .cm-string-2 { 93 | font-weight: normal; 94 | font-style: normal; 95 | text-decoration: none; 96 | color: #03941e; } 97 | 98 | .CodeMirror-selectedtext { 99 | color: #000 !important; 100 | background: #e0e0e0 !important; 101 | } 102 | .CodeMirror-selected { 103 | background: #e0e0e0 !important; 104 | } 105 | .CodeMirror-markedText { 106 | background: #fff; 107 | box-shadow: 11px 6px 21px 0 rgba(111, 111, 111, 0.75); 108 | -webkit-box-shadow: 11px 6px 21px 0 rgba(111, 111, 111, 0.75); 109 | } 110 | .cm-s-light .cm-error {color: #f00;} 111 | .cm-s-light .cm-number {color: #164;} 112 | .cm-s-light .cm-atom {color: #219;} 113 | .cm-s-light .cm-string {color: #a11;} 114 | 115 | .cm-s-light .cm-matchhighlight { 116 | background: #f9f9f9; 117 | } -------------------------------------------------------------------------------- /src/css/dark_theme.css: -------------------------------------------------------------------------------- 1 | .cm-s-dark.CodeMirror { 2 | background: #20303a; 3 | color: #f3f3f3; } 4 | 5 | .cm-s-dark .CodeMirror-gutters { 6 | background: #20303a; 7 | border-right-width: 0; } 8 | 9 | .cm-s-dark .CodeMirror-guttermarker { 10 | color: black; } 11 | 12 | .cm-s-dark .CodeMirror-guttermarker-subtle { 13 | color: #d0d0d0; } 14 | 15 | .cm-s-dark .CodeMirror-linenumber { 16 | color: #959da5; 17 | padding: 0 16px 0 16px; } 18 | 19 | .cm-s-dark .CodeMirror-cursor { 20 | border-left: 1px solid #f3f3f3; } 21 | 22 | .cm-s-dark div.CodeMirror-selected, 23 | .cm-s-dark .CodeMirror-line::selection, 24 | .cm-s-dark .CodeMirror-line > span::selection, 25 | .cm-s-dark .CodeMirror-line > span > span::selection, 26 | .cm-s-dark .CodeMirror-line::-moz-selection, 27 | .cm-s-dark .CodeMirror-line > span::-moz-selection, 28 | .cm-s-dark .CodeMirror-line > span > span::-moz-selection { 29 | background: #3a3a3a; } 30 | 31 | .cm-s-dark .CodeMirror-activeline-background { 32 | background: #3a3a3a; } 33 | 34 | .cm-s-dark .CodeMirror-lines { 35 | font-family: inherit; 36 | font-size: inherit; 37 | background: #20303a; 38 | line-height: 1.5; } 39 | 40 | .cm-s-dark .cm-comment { 41 | color: #6a737d; } 42 | 43 | .cm-s-dark .cm-constant { 44 | color: #005cc5; } 45 | 46 | .cm-s-dark .cm-entity { 47 | font-weight: normal; 48 | font-style: normal; 49 | text-decoration: none; 50 | color: #6f42c1; } 51 | 52 | .cm-s-dark .cm-keyword, .cm-s-dark .cm-qualifier { 53 | font-weight: normal; 54 | font-style: normal; 55 | text-decoration: none; 56 | color: #b2c2e2; } 57 | 58 | .cm-s-dark .cm-storage { 59 | color: #b2c2e2; } 60 | 61 | .cm-s-dark .cm-string { 62 | font-weight: normal; 63 | font-style: normal; 64 | text-decoration: none; 65 | color: #8eaf86; } 66 | 67 | .cm-s-dark .cm-support { 68 | font-weight: normal; 69 | font-style: normal; 70 | text-decoration: none; 71 | color: #005cc5; } 72 | 73 | .cm-s-light .cm-property { 74 | font-weight: normal; 75 | font-style: normal; 76 | text-decoration: none; 77 | color: #245082; 78 | } 79 | 80 | .cm-s-dark .cm-variable { 81 | font-weight: normal; 82 | font-style: normal; 83 | text-decoration: none; 84 | color: #deb79c; } 85 | 86 | .cm-s-dark .cm-def { 87 | font-weight: normal; 88 | font-style: normal; 89 | text-decoration: none; 90 | color: #deb79c; } 91 | 92 | .cm-s-dark .cm-tag { 93 | font-weight: normal; 94 | font-style: normal; 95 | text-decoration: none; 96 | color: #afbfe6; } 97 | 98 | .cm-s-dark .cm-string-2 { 99 | font-weight: normal; 100 | font-style: normal; 101 | text-decoration: none; 102 | color: #8eaf86; } 103 | 104 | .cm-s-dark .CodeMirror-selectedtext { 105 | color: #000 !important; 106 | background: #6b95af !important; 107 | } 108 | .cm-s-dark .CodeMirror-selected { 109 | background: #6b95af !important; 110 | } 111 | .cm-s-dark .CodeMirror-markedText { 112 | background: #386480; 113 | box-shadow: 11px 6px 21px 0 rgba(0, 0, 0, 0.75); 114 | -webkit-box-shadow: 11px 6px 21px 0 rgba(0, 0, 0, 0.75); 115 | } 116 | .cm-s-dark .cm-error {color: #f00;} 117 | .cm-s-dark .cm-number {color: #a6d4c1;} 118 | .cm-s-dark .cm-atom {color: #9a91d6;} 119 | .cm-s-dark .cm-string {color: #d46a6a;} 120 | 121 | .cm-s-dark .cm-matchhighlight { 122 | background: #18242b; 123 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demoit", 3 | "version": "7.10.0", 4 | "description": "A live coding tool", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/krasimir/demoit.git" 9 | }, 10 | "scripts": { 11 | "clean-demoit": "shx rm -rf ./dist/*", 12 | "copy-static": "shx cp ./src/index.html ./dist/index.html && shx cp ./src/sandbox.html ./dist/sandbox.html && shx cp -r ./src/resources ./dist && shx cp -r ./src/img ./dist", 13 | "produce-minified-js": "uglifyjs ./src/js-vendor/split.js ./.tmp/demoit.js -c -m -o ./dist/demoit.js", 14 | "produce-js": "shx cat ./src/js-vendor/split.js ./.tmp/demoit.js > ./dist/demoit.js", 15 | "produce-css": "shx cat ./src/css/codemirror.css ./src/css/la.css ./src/css/styles.css ./src/css/light_theme.css ./src/css/dark_theme.css | uglifycss > ./dist/styles.css", 16 | "produce-editor-js": "uglifyjs ./src/js-vendor/codemirror.js ./src/js-vendor/javascript.js ./src/js-vendor/xml.js ./src/js-vendor/jsx.js ./src/js-vendor/mark-selection.js ./src/js-vendor/matchbrackets.js ./src/js-vendor/comment.js ./src/js-vendor/search_cursor.js ./src/js-vendor/overlay.js ./src/js-vendor/markdown.js ./src/js-vendor/gfm.js ./src/js-vendor/runmode.js ./src/js-vendor/colorize.js ./src/js-vendor/closebrackets.js ./src/js-vendor/match-highlighter.js ./src/js-vendor/css.js ./src/js-vendor/htmlmixed.js ./src/js-vendor/deep-diff.js ./src/js-vendor/babel-6.26.0.min.js ./src/js-vendor/babel-polyfill@6.26.0.js ./src/js-vendor/babel-plugin-transform-es2015-modules-commonjs@6.26.2.js -c -m -o ./dist/resources/editor.js", 17 | "dev": "yarn build && concurrently \"webpack\" \"onchange ./src/css/*.css -- yarn produce-css\" \"onchange ./.tmp/*.js -- yarn produce-js\" \"cpx ./src/index.html ./dist/ -w\" \"cpx ./src/sandbox.html ./dist/ -w\"", 18 | "build": "yarn clean-demoit && yarn copy-static && yarn produce-css && yarn produce-editor-js && webpack --config ./webpack.config.prod.js && yarn produce-minified-js", 19 | "zip": "node ./scripts/zipit.js", 20 | "release": "yarn test && yarn build && yarn zip", 21 | "test": "jest", 22 | "test-watch": "jest --watch --verbose false", 23 | "lint": "./node_modules/.bin/eslint --ext .js src/js" 24 | }, 25 | "keywords": [ 26 | "demo", 27 | "code", 28 | "live", 29 | "coding" 30 | ], 31 | "author": "Krasimir Tsonev", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/krasimir/demoit/issues" 35 | }, 36 | "homepage": "https://github.com/krasimir/demoit#readme", 37 | "devDependencies": { 38 | "@babel/core": "7.1.5", 39 | "@babel/plugin-transform-runtime": "7.1.0", 40 | "@babel/preset-env": "7.1.5", 41 | "@babel/runtime": "7.1.5", 42 | "babel-core": "^7.0.0-bridge.0", 43 | "babel-eslint": "8.0.3", 44 | "babel-jest": "23.6.0", 45 | "babel-loader": "8.0.4", 46 | "clean-css-cli": "4.2.1", 47 | "concurrently": "4.0.1", 48 | "cpx": "1.5.0", 49 | "eslint": "4.12.1", 50 | "jest": "23.6.0", 51 | "onchange": "5.1.3", 52 | "regenerator-runtime": "0.13.1", 53 | "shx": "^0.3.2", 54 | "uglify-js": "3.4.9", 55 | "uglifycss": "0.0.29", 56 | "webpack": "4.25.1", 57 | "webpack-cli": "3.1.2", 58 | "zip-folder": "1.0.0" 59 | }, 60 | "dependencies": { 61 | "gitfred": "7.2.4", 62 | "hashids": "1.2.2", 63 | "jszip": "3.1.5", 64 | "layout-architect": "3.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![demoit](./_assets/demoit_light.png) 2 | ![demoit](./_assets/demoit_dark.png) 3 | 4 | # **Demoit** is the front-end app behind [Poet](https://poet.krasimir.now.sh) 5 | 6 | * No installation. 7 | * No server needed. It works offline. 8 | * No building process needed. Built-in Babel support. It translates your code at runtime. 9 | * Supports external libraries and styles. Like React for example. 10 | * Export your work to an external file 11 | * Supports `import` statement (between the files of app) 12 | * Supports `import`ing of CSS and HTML files 13 | * Supports dependencies via HTTP (everything from [unpkg](https://unpkg.com/#/) or [cdnjs](https://cdnjs.com)) 14 | 15 | ## Demo :rocket: 16 | 17 | [https://poet.krasimir.now.sh/new](https://poet.krasimir.now.sh/new) 18 | 19 | --- 20 | 21 | ## Usage 22 | 23 | * Online at [poet.krasimir.now.sh](https://poet.krasimir.now.sh) 24 | * Offline by downloading [Demoit.zip](https://github.com/krasimir/demoit/raw/master/demoit.zip) 25 | 26 | ## Configuration 27 | 28 | When you open the app and start writing code you progress gets saved to an internal state. You can grab it by opening the bar at the top and clicking on the gear icon (check the "Export" section). The JSON there contains all the configuration that Demoit needs. You can save this configuration to an external file and let Demoit knows the path to it via the `state` GET parameter (for example `http://localhost/demoit?state=./mycode.json`). 29 | 30 | ## GET Params 31 | 32 | * `?state=` - relative path to a JSON file 33 | * `?mode=preview` - it loads the editor just in a preview mode. The code is visible but not compiled, not editable and not executed. This significantly reduces the file size and it is useful for showing your code in a blog post for example. 34 | * `?mode=readonly` - it loads the editor in a readonly mode. It means that the code is transpiled and executed but you can't make changes. This also reduces the page's size because it is not loading Babel and CodeMirror (which is roughly 1.5MB) 35 | 36 | ## Continuing your work offline 37 | 38 | * You have to download [Demoit.zip](https://github.com/krasimir/demoit/raw/master/demoit.zip) 39 | * You need to transfer your progress to a JSON file and pass it to the app via `state` GET param 40 | * If you use external dependencies make sure that they are also saved locally and the path to the files is properly set (check the gear icon in the status bar at the top of the app) 41 | 42 | ## Keyboard shortcuts when the focus is on the editor 43 | 44 | * `Ctrl + S` / `Cmd + S` - essential for seeing the result of your code. This keys combination triggers transpilation and execution. 45 | * `Ctrl + <0-9>` / `Cmd + <0-9>` - switch between files 46 | 47 | ## Editing filenames and deleting files 48 | 49 | Right mouse click on the file's tab. 50 | 51 | ## Troubleshooting 52 | 53 | ### Error `URL scheme must be "http" or "https" for CORS request.` 54 | 55 | It means that the browser doesn't load the files that the tool needs because the protocol is `file://`. That's a problem in Chrome at the moment. Everything works fine in Firefox. To fix the problem in Chrome you have to run it like so: 56 | 57 | ``` 58 | open /Applications/Google\ Chrome.app/ --args --disable-web-security 59 | ``` 60 | or under Windows: 61 | ``` 62 | chrome.exe --disable-web-security 63 | ``` 64 | 65 | Of course Demoit works just fine if you open `index.html` via `http` protocol but to do that you need a server. 66 | -------------------------------------------------------------------------------- /src/js/storyReadOnly.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-sequences */ 2 | import el from './utils/element'; 3 | import commitDiff from './utils/commitDiff'; 4 | import confirmPopUp from './popups/confirmPopUp'; 5 | import { DEBUG } from './constants'; 6 | import getTitleFromCommitMessage from './story/getTitleFromCommitMessage'; 7 | 8 | function renderCommits(git, commits) { 9 | function process(commit) { 10 | const { hash, message, position } = commit; 11 | const currentPosition = position && position > 0 ? `${position}` : ''; 12 | const messageFirstLine = getTitleFromCommitMessage(message); 13 | const isCurrent = git.head() === hash; 14 | let html = ''; 15 | 16 | html += `
`; 17 | html += ` 18 | 19 | ${currentPosition}${ messageFirstLine ? messageFirstLine : '...' } 20 | 21 | `; 22 | html += '
'; 23 | return html; 24 | } 25 | 26 | if (commits.length === 0) { 27 | return ''; 28 | } 29 | return commits.map(process).join(''); 30 | }; 31 | 32 | export default function story(state, onChange) { 33 | const container = el.withFallback('.read-only'); 34 | const git = state.git(); 35 | 36 | if (!container.found()) return; 37 | 38 | const render = () => { 39 | DEBUG && console.log('story:render'); 40 | const allCommits = git.log(); 41 | const commits = Object.keys(allCommits).map(hash => ({ 42 | hash, 43 | message: allCommits[hash].message, 44 | position: allCommits[hash].meta ? parseInt(allCommits[hash].meta.position, 10) || null : null 45 | })).sort((a, b) => { 46 | if (a.position !== null && b.position !== null) { 47 | return a.position - b.position; 48 | } else if (a.position !== null && b.position === null) { 49 | return -1; 50 | } else if (a.position === null && b.position !== null) { 51 | return 1; 52 | } 53 | return a.hash - b.hash; 54 | }); 55 | const numOfCommits = commits.length; 56 | const diffs = commitDiff(numOfCommits > 0 ? git.show().files : [], git.getAll()); 57 | const renderedCommits = renderCommits(git, commits); 58 | 59 | container.attr('class', 'editor-section story'); 60 | container.content(` 61 | ${ renderedCommits !== '' ? '
' + renderedCommits + '
' : '' } 62 | `).forEach(el => { 63 | if (el.attr('data-export') === 'checkoutLink') { 64 | el.onClick(() => { 65 | const hashToCheckout = el.attr('data-hash'); 66 | 67 | if (diffs.length > 0) { 68 | confirmPopUp( 69 | 'Checkout', 70 | 'You are about to checkout another commit. You have an unstaged changes. Are you sure?', 71 | decision => { 72 | if (decision && allCommits[hashToCheckout]) { 73 | git.checkout(hashToCheckout, true); 74 | onChange(); 75 | render(); 76 | } 77 | } 78 | ); 79 | } else { 80 | if (allCommits[hashToCheckout]) { 81 | git.checkout(hashToCheckout); 82 | onChange(); 83 | render(); 84 | } 85 | } 86 | }); 87 | } 88 | }); 89 | }; 90 | 91 | state.listen(event => { 92 | render(); 93 | }); 94 | 95 | render(); 96 | } 97 | -------------------------------------------------------------------------------- /src/js-vendor/overlay.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 3 | // Distributed under an MIT license: https://codemirror.net/LICENSE 4 | 5 | // Utility function that allows modes to be combined. The mode given 6 | // as the base argument takes care of most of the normal mode 7 | // functionality, but a second (typically simple) mode is used, which 8 | // can override the style of text. Both modes get to parse all of the 9 | // text, but when both assign a non-null style to a piece of code, the 10 | // overlay wins, unless the combine argument was true and not overridden, 11 | // or state.overlay.combineTokens was true, in which case the styles are 12 | // combined. 13 | 14 | (function(mod) { 15 | if (typeof exports == "object" && typeof module == "object") // CommonJS 16 | mod(require("../../lib/codemirror")); 17 | else if (typeof define == "function" && define.amd) // AMD 18 | define(["../../lib/codemirror"], mod); 19 | else // Plain browser env 20 | mod(CodeMirror); 21 | })(function(CodeMirror) { 22 | "use strict"; 23 | 24 | CodeMirror.overlayMode = function(base, overlay, combine) { 25 | return { 26 | startState: function() { 27 | return { 28 | base: CodeMirror.startState(base), 29 | overlay: CodeMirror.startState(overlay), 30 | basePos: 0, baseCur: null, 31 | overlayPos: 0, overlayCur: null, 32 | streamSeen: null 33 | }; 34 | }, 35 | copyState: function(state) { 36 | return { 37 | base: CodeMirror.copyState(base, state.base), 38 | overlay: CodeMirror.copyState(overlay, state.overlay), 39 | basePos: state.basePos, baseCur: null, 40 | overlayPos: state.overlayPos, overlayCur: null 41 | }; 42 | }, 43 | 44 | token: function(stream, state) { 45 | if (stream != state.streamSeen || 46 | Math.min(state.basePos, state.overlayPos) < stream.start) { 47 | state.streamSeen = stream; 48 | state.basePos = state.overlayPos = stream.start; 49 | } 50 | 51 | if (stream.start == state.basePos) { 52 | state.baseCur = base.token(stream, state.base); 53 | state.basePos = stream.pos; 54 | } 55 | if (stream.start == state.overlayPos) { 56 | stream.pos = stream.start; 57 | state.overlayCur = overlay.token(stream, state.overlay); 58 | state.overlayPos = stream.pos; 59 | } 60 | stream.pos = Math.min(state.basePos, state.overlayPos); 61 | 62 | // state.overlay.combineTokens always takes precedence over combine, 63 | // unless set to null 64 | if (state.overlayCur == null) return state.baseCur; 65 | else if (state.baseCur != null && 66 | state.overlay.combineTokens || 67 | combine && state.overlay.combineTokens == null) 68 | return state.baseCur + " " + state.overlayCur; 69 | else return state.overlayCur; 70 | }, 71 | 72 | indent: base.indent && function(state, textAfter, line) { 73 | return base.indent(state.base, textAfter, line); 74 | }, 75 | electricChars: base.electricChars, 76 | 77 | innerMode: function(state) { return {state: state.base, mode: base}; }, 78 | 79 | blankLine: function(state) { 80 | var baseToken, overlayToken; 81 | if (base.blankLine) baseToken = base.blankLine(state.base); 82 | if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay); 83 | 84 | return overlayToken == null ? 85 | baseToken : 86 | (combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken); 87 | } 88 | }; 89 | }; 90 | 91 | }); -------------------------------------------------------------------------------- /src/js/story/renderCommits.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-use-before-define */ 2 | import { CHECK_ICON, CLOSE_ICON, TRASH_ICON, DOT_CIRCLE, BOOK } from '../utils/icons'; 3 | import getTitleFromCommitMessage from './getTitleFromCommitMessage'; 4 | 5 | export default function renderCommits(git, commits, editMode, currentlyEditingHash) { 6 | function process(commit) { 7 | const { hash, message, position } = commit; 8 | const isEditing = currentlyEditingHash === hash && editMode; 9 | const currentPosition = position && position > 0 ? `${position}` : ''; 10 | const messageFirstLine = getTitleFromCommitMessage(message); 11 | const isCurrent = git.head() === hash; 12 | let html = ''; 13 | 14 | html += `
`; 15 | html += !isEditing ? ` 16 | 17 | ${currentPosition}${ messageFirstLine ? messageFirstLine : '...' } 18 | 19 | ` : ''; 20 | if (isEditing) { 21 | html += ` 22 | ${CHECK_ICON(12)} save 23 | ${ git.head() !== hash ? `${ DOT_CIRCLE(12) } checkout` : '' } 24 | ${ TRASH_ICON(12) } delete 25 | 26 | ${ CLOSE_ICON(12) } 27 | 28 |
29 | 32 | 37 | `; 38 | } else { 39 | html += ` 40 | 41 | ${ BOOK(12) + ' edit' } 42 | 43 | `; 44 | } 45 | html += '
'; 46 | 47 | if (isEditing) { 48 | html += ` 49 |
50 |
51 |
52 | `; 53 | } 54 | return html; 55 | } 56 | 57 | if (commits.length === 0) { 58 | return ''; 59 | } 60 | return commits.map(process).join(''); 61 | }; 62 | 63 | function getPublishOptions(git, currentHash) { 64 | const allCommits = git.log(); 65 | const { meta } = allCommits[currentHash]; 66 | const currentPosition = meta ? parseInt(meta.position, 10) : 0; 67 | let options = []; 68 | 69 | options.push(``); 70 | for (let i = 1; i < Object.keys(allCommits).length + 1; i++) { 71 | options.push(``); 72 | } 73 | 74 | return options.join(''); 75 | } 76 | function getFileInjectionOptions(git, currentHash) { 77 | const files = git.show(currentHash).files; 78 | 79 | return files.map(file => ``); 80 | } 81 | -------------------------------------------------------------------------------- /samples/Canvas.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor": { 3 | "theme": "dark", 4 | "statusBar": true, 5 | "layout": { 6 | "name": "layoutEO", 7 | "direction": "horizontal", 8 | "sizes": [ 9 | 69.0625, 10 | 30.9375 11 | ], 12 | "elements": [ 13 | "editor", 14 | "output" 15 | ] 16 | } 17 | }, 18 | "dependencies": [], 19 | "files": [ 20 | { 21 | "content": "import 'layout.html';\nimport 'styles.css';\n\n// JS Port of PlusParticle\n// k3lab ( http://wonderfl.net/user/k3lab )\n// http://wa.zozuar.org/code.php?c=hEiW\n\"use strict\";\nconst canvas = document.querySelector(\"canvas\");\nconst ctx = canvas.getContext(\"2d\", { lowLatency: true, alpha: false });\nconst w = (canvas.width = canvas.offsetWidth / 2);\nconst h = (canvas.height = canvas.offsetHeight / 2);\n///////////////////////////////////////\nconst createDot = () => {\n\tconst dot = document.createElement('canvas');\n\tdot.width = 20;\n\tdot.height = 20;\n\tconst ctx = dot.getContext(\"2d\");\n\tctx.globalCompositeOperation = \"xor\";\n\tctx.fillStyle = \"#fff\";\n\tctx.fillRect(0, 10 - 2, 20, 2);\n\tctx.fillRect(10 - 2, 0, 2, 20);\n\treturn dot;\n}\n///////////////////////////////////////\nconst Particle = class {\n\tconstructor(x, y, z) {\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t\tthis.z = z;\n\t}\n};\n///////////////////////////////////////\nconst count = 4000;\nconst degree = 2 * Math.PI / count;\nconst particles = [];\nconst dot = createDot();\n///////////////////////////////////////\nconst run = time => {\n\trequestAnimationFrame(run);\n\tctx.fillRect(0, 0, w, h);\n\tconst horizontal = Math.sin(time / 2000) * 2;\n\tconst vertical = Math.sin(time / 30000);\n\tconst cosY = Math.cos(horizontal);\n\tconst sinY = Math.sin(horizontal);\n\tconst cosX = Math.cos(vertical);\n\tconst sinX = Math.sin(vertical);\n\tconst s = 2500 + 1500 * Math.sin(time / 1000);\n\tconst a = s * 2 / count;\n\tconst round = degree * 2 * Math.sin(time / 10000);\n\tfor (let i = 0; i < count; i++) {\n\t\tconst p = particles[i];\n\t\tconst size = s * Math.sin(time / 10000 + i / 10);\n\t\tconst radius = size * Math.sin(degree / 2 * i);\n\t\tp.x = radius * Math.sin(round * i + time / 1000);\n\t\tp.y = radius * Math.cos(round * i + time / 1000);\n\t\tp.z = -s + i * a;\n\t\tconst z1 = p.z * cosY + p.x * sinY;\n\t\tconst z2 = z1 * cosX + p.y * sinX;\n\t\tif (z2 > -600) {\n\t\t\tconst perspective = 300 / (600 + z2);\n\t\t\tconst px = w / 2 + (p.x * cosY - p.z * sinY) * perspective;\n\t\t\tconst py = h / 2 + (p.y * cosX - z1 * sinX) * perspective;\n\t\t\tif (px > -30 && px < w && py > -30 && py < h) {\n\t\t\t\tlet wi = (z2 - 200) * -1 / 200;\n\t\t\t\tif (wi < 0.1) wi = 0.1;\n\t\t\t\tctx.save();\n\t\t\t\tctx.translate(px, py);\n\t\t\t\tctx.rotate(i / 100);\n\t\t\t\tctx.scale(wi, wi);\n\t\t\t\tctx.drawImage(dot, -10, -10);\n\t\t\t\tctx.restore();\n\t\t\t}\n\t\t}\n\t}\n};\n///////////////////////////////////////\nfor (let i = 0; i < count; i++) {\n\tparticles.push(\n\t\tnew Particle(\n\t\t\t200 * Math.sin(degree * i),\n\t\t\t200 * Math.cos(degree * i),\n\t\t\t-100 + i / 100\n\t\t)\n\t);\n}\nrun();\n", 22 | "filename": "app.js", 23 | "entryPoint": true, 24 | "editing": false 25 | }, 26 | { 27 | "content": "", 28 | "filename": "layout.html", 29 | "editing": false 30 | }, 31 | { 32 | "content": "#output {\n\toverflow: hidden;\n\ttouch-action: none;\n\tcontent-zooming: none;\n\tmargin: 0;\n\tpadding: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: #111;\n}\n#output canvas {\n\tposition:absolute;\n width: 100%;\n height: 100%;\n\tbackground: #000;\n}", 33 | "filename": "styles.css", 34 | "editing": false 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/js/popups/settingsPopUp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import Layout from 'layout-architect'; 3 | import createPopup from './popup'; 4 | import { LAYOUT_BLOCKS, DEFAULT_LAYOUT } from '../layout'; 5 | import { IS_PROD } from '../constants'; 6 | 7 | const generateIframe = url => ``; 8 | 9 | export default function settingsPopUp( 10 | enableDownload, 11 | { layout, theme }, 12 | dependenciesStr, 13 | onDepsUpdated, 14 | onGeneralUpdate, 15 | defaultTab, 16 | version 17 | ) { 18 | return new Promise(done => createPopup({ 19 | defaultTab: defaultTab || 0, 20 | buttons: [ 21 | 'General', 22 | 'Dependencies', 23 | 'Export/Share', 24 | 'About' 25 | ], 26 | content: [ 27 | ` 28 |

29 | Theme: 30 | 34 |

35 |

Layout:

36 |
37 | 38 | `, 39 | ` 40 | 41 |

(Separate your dependencies by a new line)

42 | 43 | `, 44 | ` 45 |

Embed

46 | 47 | ${ IS_PROD ? ` 48 |

Download/Offline mode

49 |

The archive contains all the files that you need to run the app locally. Including your dependencies.

50 | ` : '' } 51 | `, 52 | ` 53 |

54 | v${ version }
55 | On the web: poet.krasimir.now.sh
56 | GitHub repo: github.com/krasimir/poet.krasimir.now.sh.feedback 57 |

58 | ` 59 | ], 60 | cleanUp() { 61 | done(); 62 | }, 63 | onRender({ 64 | closePopup, 65 | saveGeneral, 66 | dependenciesTextarea, 67 | saveDependenciesButton, 68 | themePicker, 69 | iframeTextarea, 70 | layoutArchitectContainer, 71 | downloadButton 72 | }) { 73 | // general settings 74 | if (layoutArchitectContainer && themePicker) { 75 | const la = Layout(layoutArchitectContainer.e, LAYOUT_BLOCKS, layout); 76 | 77 | themePicker.e.value = theme || 'light'; 78 | saveGeneral.onClick(() => { 79 | onGeneralUpdate(themePicker.e.value, la.get() || DEFAULT_LAYOUT); 80 | closePopup(); 81 | }); 82 | } 83 | // share 84 | if (iframeTextarea) { 85 | iframeTextarea.selectOnClick(); 86 | } 87 | if (downloadButton) { 88 | enableDownload(downloadButton); 89 | } 90 | // managing dependencies 91 | if (dependenciesTextarea && saveDependenciesButton) { 92 | dependenciesTextarea.prop('value', dependenciesStr); 93 | saveDependenciesButton.onClick(() => { 94 | onDepsUpdated(dependenciesTextarea.prop('value').split(/\r?\n/)); 95 | closePopup(); 96 | }); 97 | } 98 | } 99 | })); 100 | }; 101 | -------------------------------------------------------------------------------- /src/js/utils/cleanUpMarkdown.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Source: https://github.com/stiang/remove-markdown 4 | Author: https://github.com/stiang 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Stian Grytøyr 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | */ 29 | 30 | export default function cleanUpMarkdown(md, options) { 31 | options = options || {}; 32 | options.listUnicodeChar = options.hasOwnProperty('listUnicodeChar') ? options.listUnicodeChar : false; 33 | options.stripListLeaders = options.hasOwnProperty('stripListLeaders') ? options.stripListLeaders : true; 34 | options.gfm = options.hasOwnProperty('gfm') ? options.gfm : true; 35 | options.useImgAltText = options.hasOwnProperty('useImgAltText') ? options.useImgAltText : true; 36 | 37 | let output = md || ''; 38 | 39 | // Remove horizontal rules (stripListHeaders conflict with this rule, which is why it has been moved to the top) 40 | output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, ''); 41 | 42 | try { 43 | if (options.stripListLeaders) { 44 | if (options.listUnicodeChar) { 45 | output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, options.listUnicodeChar + ' $1'); 46 | } else { 47 | output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, '$1'); 48 | } 49 | } 50 | if (options.gfm) { 51 | output = output 52 | // Header 53 | .replace(/\n={2,}/g, '\n') 54 | // Fenced codeblocks 55 | .replace(/~{3}.*\n/g, '') 56 | // Strikethrough 57 | .replace(/~~/g, '') 58 | // Fenced codeblocks 59 | .replace(/`{3}.*\n/g, ''); 60 | } 61 | output = output 62 | // Remove HTML tags 63 | .replace(/<[^>]*>/g, '') 64 | // Remove setext-style headers 65 | .replace(/^[=\-]{2,}\s*$/g, '') 66 | // Remove footnotes? 67 | .replace(/\[\^.+?\](\: .*?$)?/g, '') 68 | .replace(/\s{0,2}\[.*?\]: .*?$/g, '') 69 | // Remove images 70 | .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? '$1' : '') 71 | // Remove inline links 72 | .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1') 73 | // Remove blockquotes 74 | .replace(/^\s{0,3}>\s?/g, '') 75 | // Remove reference-style links? 76 | .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '') 77 | // Remove atx-style headers 78 | .replace(/^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, '$1$2$3') 79 | // Remove emphasis (repeat the line to remove double emphasis) 80 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2') 81 | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, '$2') 82 | // Remove code blocks 83 | .replace(/(`{3,})(.*?)\1/gm, '$2') 84 | // Remove inline code 85 | .replace(/`(.+?)`/g, '$1') 86 | // Replace two or more newlines with exactly two? Not entirely sure this belongs here... 87 | .replace(/\n{2,}/g, '\n\n'); 88 | } catch (e) { 89 | console.error(e); 90 | return md; 91 | } 92 | return output; 93 | }; 94 | -------------------------------------------------------------------------------- /src/js/utils/index.js: -------------------------------------------------------------------------------- 1 | export const debounce = function (func, wait, immediate) { 2 | var timeout; 3 | 4 | return function () { 5 | var context = this, args = arguments; 6 | var later = function () { 7 | timeout = null; 8 | if (!immediate) func.apply(context, args); 9 | }; 10 | let callNow = immediate && !timeout; 11 | 12 | clearTimeout(timeout); 13 | timeout = setTimeout(later, wait); 14 | if (callNow) func.apply(context, args); 15 | }; 16 | }; 17 | 18 | export const delay = async (amount = 1) => new Promise(done => setTimeout(done, amount)); 19 | export const once = callback => { 20 | let called = false; 21 | 22 | return (...args) => { 23 | if (called) return; 24 | called = true; 25 | callback(...args); 26 | }; 27 | }; 28 | export const getParam = (parameterName, defaultValue) => { 29 | var result = defaultValue, tmp = []; 30 | 31 | location.search 32 | .substr(1) 33 | .split('&') 34 | .forEach(function (item) { 35 | tmp = item.split('='); 36 | if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); 37 | }); 38 | return result; 39 | }; 40 | 41 | export const readFromJSONFile = async function (file) { 42 | const res = await fetch(file); 43 | 44 | return await res.json(); 45 | }; 46 | 47 | export const removeParam = function (key, sourceURL) { 48 | const urlWithoutParams = sourceURL.split('?')[0]; 49 | const hash = sourceURL.split('#')[1]; 50 | const queryString = (sourceURL.indexOf('?') !== -1) ? sourceURL.split('?')[1] : ''; 51 | let params = []; 52 | let param; 53 | 54 | if (queryString !== '') { 55 | params = queryString.split('&'); 56 | for (let i = params.length - 1; i >= 0; i -= 1) { 57 | param = params[i].split('=')[0]; 58 | if (param === key) { 59 | params.splice(i, 1); 60 | } 61 | } 62 | params = params.join('&'); 63 | 64 | return [ 65 | urlWithoutParams, 66 | params !== '' ? '?' + params : '', 67 | hash ? '#' + hash : '' 68 | ].join(''); 69 | } 70 | return urlWithoutParams; 71 | }; 72 | 73 | export const ensureDemoIdInPageURL = demoId => { 74 | const currentURL = window.location.href; 75 | const hash = currentURL.split('#')[1]; 76 | 77 | history.pushState(null, null, `/e/${ demoId }${ hash ? '#' + hash : '' }`); 78 | }; 79 | 80 | export const ensureUniqueFileName = (filename) => { 81 | const tmp = filename.split('.'); 82 | 83 | if (tmp.length === 1) { 84 | return tmp[0] + '.1'; 85 | } else if (tmp.length === 2) { 86 | return `${ tmp[0] }.1.${ tmp[1] }`; 87 | } 88 | const ext = tmp.pop(); 89 | const num = tmp.pop(); 90 | 91 | if (isNaN(parseInt(num, 10))) { 92 | return `${ tmp.join('.') }.${ num }.1.${ ext }`; 93 | } 94 | return `${ tmp.join('.') }.${ (parseInt(num, 10) + 1) }.${ ext }`; 95 | }; 96 | 97 | export const truncate = function (str, len) { 98 | if (str.length > len) { 99 | return str.substr(0, len) + '...'; 100 | } 101 | return str; 102 | }; 103 | 104 | export const escapeHTML = function (html) { 105 | const tagsToReplace = { 106 | '&': '&', 107 | '<': '<', 108 | '>': '>' 109 | }; 110 | const replaceTag = (tag) => { 111 | return tagsToReplace[tag] || tag; 112 | }; 113 | 114 | return html.replace(/[&<>]/g, replaceTag); 115 | }; 116 | 117 | export function jsEncode(s) { 118 | let enc = ''; 119 | 120 | s = s.toString(); 121 | for (let i = 0; i < s.length; i++) { 122 | let a = s.charCodeAt(i); 123 | let b = a ^ 3; 124 | 125 | enc = enc + String.fromCharCode(b); 126 | } 127 | return enc; 128 | }; 129 | 130 | export const clone = function (data) { 131 | return JSON.parse(JSON.stringify(data)); 132 | }; 133 | 134 | export const isEmbedded = function () { 135 | try { 136 | return window.self !== window.top; 137 | } catch (e) { 138 | return true; 139 | } 140 | }; 141 | export const formatDate = function (date = new Date()) { 142 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 143 | const day = date.getDate(), 144 | monthIndex = date.getMonth(), 145 | year = date 146 | .getFullYear() 147 | .toString() 148 | .substr(-2); 149 | 150 | return day + ' ' + monthNames[monthIndex] + ' ' + year + ' ' + date.getHours() + ':' + date.getMinutes(); 151 | } 152 | -------------------------------------------------------------------------------- /src/js/layout.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import el from './utils/element'; 3 | import setTheme from './utils/setTheme'; 4 | import { IS_PROD } from './constants'; 5 | 6 | export const LAYOUT_BLOCKS = ['editor', 'HTML', 'console', 'story']; 7 | 8 | if (IS_PROD) { 9 | LAYOUT_BLOCKS.push('story-preview'); 10 | LAYOUT_BLOCKS.push('story-read-only'); 11 | LAYOUT_BLOCKS.push('annotate'); 12 | } 13 | 14 | export const DEFAULT_LAYOUT = { 15 | elements: [ 16 | { 17 | name: 'editor', 18 | elements: [] 19 | }, 20 | { 21 | elements: [ 22 | { 23 | name: 'HTML', 24 | elements: [] 25 | }, 26 | { 27 | name: 'console', 28 | elements: [] 29 | } 30 | ], 31 | direction: 'horizontal' 32 | } 33 | ], 34 | direction: 'vertical' 35 | }; 36 | 37 | function validateLayout(item) { 38 | if (typeof item === 'string') { 39 | if (item === 'output') item = 'HTML'; 40 | if (item === 'log') item = 'console'; 41 | return { 42 | name: item, 43 | elements: [] 44 | }; 45 | } 46 | if (item.elements.length > 0) { 47 | item.elements.forEach((i, index) => (item.elements[index] = validateLayout(i))); 48 | } 49 | return item; 50 | } 51 | function generateSizes(elements) { 52 | return elements.map(() => 100 / elements.length); 53 | } 54 | 55 | export default state => { 56 | const container = el.withRelaxedCleanup('.app .layout'); 57 | const body = el.withRelaxedCleanup('body'); 58 | 59 | setTheme(state.getEditorSettings().theme); 60 | 61 | const layout = validateLayout(state.getEditorSettings().layout || DEFAULT_LAYOUT); 62 | const HTML = el.fromTemplate('#template-html'); 63 | const consoleE = el.fromTemplate('#template-console'); 64 | const editor = el.fromTemplate('#template-editor'); 65 | const story = el.fromTemplate('#template-story'); 66 | const storyPreview = el.fromTemplate('#template-story-preview'); 67 | const storyReadOnly = el.fromTemplate('#template-story-read-only'); 68 | const annotate = el.fromTemplate('#template-annotate'); 69 | const empty = el.withFallback('.does-not-exists'); 70 | const elementsMap = { 71 | HTML, 72 | console: consoleE, 73 | editor, 74 | story, 75 | 'story-preview': storyPreview, 76 | 'story-read-only': storyReadOnly, 77 | annotate 78 | }; 79 | const usedBlocks = []; 80 | 81 | const splitFuncs = []; 82 | let splits; 83 | const build = block => { 84 | let { direction, elements, sizes } = block; 85 | const normalizedElements = elements.map(item => { 86 | if (item.elements.length > 0) { 87 | const wrapper = el.wrap(build(item)); 88 | 89 | wrapper.attr('class', 'editor-section'); 90 | return wrapper; 91 | } 92 | usedBlocks.push(item.name); 93 | return elementsMap[item.name] ? elementsMap[item.name] : empty; 94 | }); 95 | 96 | if (sizes && sizes.length !== elements.length) { 97 | sizes = elements.map(() => (100 / elements.length)); 98 | } 99 | 100 | splitFuncs.push(() => ({ 101 | b: block, 102 | split: Split(normalizedElements.map(({ e }) => e), { 103 | sizes: sizes || generateSizes(normalizedElements), 104 | gutterSize: 2, 105 | direction, 106 | onDragEnd: () => { 107 | splits.forEach(({ b, split }) => { 108 | b.sizes = split.getSizes(); 109 | }); 110 | state.updateThemeAndLayout(layout); 111 | } 112 | }) 113 | })); 114 | 115 | if (direction === 'horizontal') { 116 | normalizedElements.map(el => el.css('float', 'left')); 117 | } 118 | 119 | return normalizedElements; 120 | }; 121 | 122 | container.empty().appendChildren(build({ elements: [layout] })); 123 | 124 | if (usedBlocks.indexOf('HTML') === -1) { 125 | HTML.css('position', 'absolute'); 126 | HTML.css('width', '10px'); 127 | HTML.css('height', '10px'); 128 | HTML.css('overflow', 'hidden'); 129 | HTML.css('top', '-100px'); 130 | HTML.css('left', '-100px'); 131 | HTML.css('visibility', 'hidden'); 132 | HTML.css('display', 'none'); 133 | HTML.appendTo(body); 134 | } 135 | 136 | setTimeout(() => (splits = splitFuncs.map(f => f())), 1); 137 | }; 138 | -------------------------------------------------------------------------------- /src/js-vendor/mark-selection.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | // Because sometimes you need to mark the selected *text*. 5 | // 6 | // Adds an option 'styleSelectedText' which, when enabled, gives 7 | // selected text the CSS class given as option value, or 8 | // "CodeMirror-selectedtext" when the value is not a string. 9 | 10 | (function(mod) { 11 | if (typeof exports == "object" && typeof module == "object") // CommonJS 12 | mod(require("../../lib/codemirror")); 13 | else if (typeof define == "function" && define.amd) // AMD 14 | define(["../../lib/codemirror"], mod); 15 | else // Plain browser env 16 | mod(CodeMirror); 17 | })(function(CodeMirror) { 18 | "use strict"; 19 | 20 | CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) { 21 | var prev = old && old != CodeMirror.Init; 22 | if (val && !prev) { 23 | cm.state.markedSelection = []; 24 | cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext"; 25 | reset(cm); 26 | cm.on("cursorActivity", onCursorActivity); 27 | cm.on("change", onChange); 28 | } else if (!val && prev) { 29 | cm.off("cursorActivity", onCursorActivity); 30 | cm.off("change", onChange); 31 | clear(cm); 32 | cm.state.markedSelection = cm.state.markedSelectionStyle = null; 33 | } 34 | }); 35 | 36 | function onCursorActivity(cm) { 37 | if (cm.state.markedSelection) 38 | cm.operation(function() { update(cm); }); 39 | } 40 | 41 | function onChange(cm) { 42 | if (cm.state.markedSelection && cm.state.markedSelection.length) 43 | cm.operation(function() { clear(cm); }); 44 | } 45 | 46 | var CHUNK_SIZE = 8; 47 | var Pos = CodeMirror.Pos; 48 | var cmp = CodeMirror.cmpPos; 49 | 50 | function coverRange(cm, from, to, addAt) { 51 | if (cmp(from, to) == 0) return; 52 | var array = cm.state.markedSelection; 53 | var cls = cm.state.markedSelectionStyle; 54 | for (var line = from.line;;) { 55 | var start = line == from.line ? from : Pos(line, 0); 56 | var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line; 57 | var end = atEnd ? to : Pos(endLine, 0); 58 | var mark = cm.markText(start, end, {className: cls}); 59 | if (addAt == null) array.push(mark); 60 | else array.splice(addAt++, 0, mark); 61 | if (atEnd) break; 62 | line = endLine; 63 | } 64 | } 65 | 66 | function clear(cm) { 67 | var array = cm.state.markedSelection; 68 | for (var i = 0; i < array.length; ++i) array[i].clear(); 69 | array.length = 0; 70 | } 71 | 72 | function reset(cm) { 73 | clear(cm); 74 | var ranges = cm.listSelections(); 75 | for (var i = 0; i < ranges.length; i++) 76 | coverRange(cm, ranges[i].from(), ranges[i].to()); 77 | } 78 | 79 | function update(cm) { 80 | if (!cm.somethingSelected()) return clear(cm); 81 | if (cm.listSelections().length > 1) return reset(cm); 82 | 83 | var from = cm.getCursor("start"), to = cm.getCursor("end"); 84 | 85 | var array = cm.state.markedSelection; 86 | if (!array.length) return coverRange(cm, from, to); 87 | 88 | var coverStart = array[0].find(), coverEnd = array[array.length - 1].find(); 89 | if (!coverStart || !coverEnd || to.line - from.line <= CHUNK_SIZE || 90 | cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0) 91 | return reset(cm); 92 | 93 | while (cmp(from, coverStart.from) > 0) { 94 | array.shift().clear(); 95 | coverStart = array[0].find(); 96 | } 97 | if (cmp(from, coverStart.from) < 0) { 98 | if (coverStart.to.line - from.line < CHUNK_SIZE) { 99 | array.shift().clear(); 100 | coverRange(cm, from, coverStart.to, 0); 101 | } else { 102 | coverRange(cm, from, coverStart.from, 0); 103 | } 104 | } 105 | 106 | while (cmp(to, coverEnd.to) < 0) { 107 | array.pop().clear(); 108 | coverEnd = array[array.length - 1].find(); 109 | } 110 | if (cmp(to, coverEnd.to) > 0) { 111 | if (to.line - coverEnd.from.line < CHUNK_SIZE) { 112 | array.pop().clear(); 113 | coverRange(cm, coverEnd.from, to); 114 | } else { 115 | coverRange(cm, coverEnd.to, to); 116 | } 117 | } 118 | } 119 | }); -------------------------------------------------------------------------------- /src/js/editor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | import el from './utils/element'; 3 | import executeCode from './execute'; 4 | import createConsole from './console'; 5 | import output from './output'; 6 | import loadAppDeps from './dependencies'; 7 | import defineCodeMirrorCommands from './utils/codeMirrorCommands'; 8 | import { isEmbedded } from './utils'; 9 | 10 | export const ON_SELECT = 'e_ON_SELECT'; 11 | export const ON_FILE_CHANGE = 'e_ON_FILE_CHANGE'; 12 | export const ON_FILE_SAVE = 'e_ON_FILE_SAVE'; 13 | 14 | export default async function editor(state, listener) { 15 | const { clearConsole, addToConsole } = createConsole(); 16 | const { resetOutput, loadDependenciesInOutput, executeInOut } = await output(state, addToConsole, clearConsole); 17 | const execute = async () => { 18 | await resetOutput(); 19 | await loadDependenciesInOutput(); 20 | clearConsole(); 21 | await executeInOut(executeCode(state.getActiveFile(), state.getFiles())); 22 | }; 23 | const onSave = async (code) => { 24 | clearConsole(); 25 | state.editFile(state.getActiveFile(), { c: code }); 26 | listener(ON_FILE_SAVE, code, codeMirrorEditor); 27 | execute(); 28 | }; 29 | const container = el.withFallback('.js-code-editor'); 30 | 31 | clearConsole('
'); 32 | await loadAppDeps(); 33 | 34 | // Initializing CodeMirror 35 | const codeMirrorEditor = codeMirror( 36 | container.empty(), 37 | state.getEditorSettings(), 38 | state.getActiveFileContent(), 39 | onSave, 40 | function onChange() { 41 | listener(ON_FILE_CHANGE); 42 | }, 43 | function showFile(index) { 44 | state.setActiveFileByIndex(index); 45 | loadFileInEditor(); 46 | }, 47 | function onSelection(code, list) { 48 | listener(ON_SELECT, { code, list }, codeMirrorEditor); 49 | } 50 | ); 51 | 52 | // The function that we call to execute a file 53 | async function loadFileInEditor() { 54 | clearConsole(); 55 | codeMirrorEditor.setValue(state.getActiveFileContent()); 56 | if (!isEmbedded()) { codeMirrorEditor.focus(); } 57 | switch (state.getActiveFile().split('.').pop().toLowerCase()) { 58 | case 'css': codeMirrorEditor.setOption('mode', 'css'); break; 59 | case 'scss': codeMirrorEditor.setOption('mode', 'css'); break; 60 | case 'html': codeMirrorEditor.setOption('mode', 'htmlmixed'); break; 61 | case 'md': 62 | codeMirrorEditor.setOption('mode', { 63 | name: 'gfm', 64 | highlightFormatting: true, 65 | emoji: true, 66 | xml: true 67 | }); 68 | break; 69 | default: codeMirrorEditor.setOption('mode', 'jsx'); break; 70 | } 71 | execute(); 72 | } 73 | 74 | return { loadFileInEditor, save: () => { 75 | onSave(codeMirrorEditor.getValue()); 76 | codeMirrorEditor.focus(); 77 | }}; 78 | } 79 | 80 | function codeMirror(container, editorSettings, value, onSave, onChange, showFile, onSelection) { 81 | defineCodeMirrorCommands(CodeMirror); 82 | 83 | const editor = CodeMirror(container.e, { 84 | value: value || '', 85 | mode: 'jsx', 86 | tabSize: 2, 87 | lineNumbers: false, 88 | autofocus: false, 89 | foldGutter: false, 90 | gutters: [], 91 | styleSelectedText: true, 92 | matchBrackets: true, 93 | autoCloseBrackets: true, 94 | lineWrapping: true, 95 | theme: editorSettings.theme, 96 | highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true } 97 | }); 98 | const save = () => onSave(editor.getValue()); 99 | const change = (instance, { origin }) => { 100 | if (origin !== 'setValue') { 101 | onChange(editor.getValue()); 102 | } 103 | }; 104 | 105 | editor.on('change', change); 106 | editor.setOption('extraKeys', { 107 | 'Ctrl-S': save, 108 | 'Cmd-S': save, 109 | 'Cmd-1': () => showFile(0), 110 | 'Cmd-2': () => showFile(1), 111 | 'Cmd-3': () => showFile(2), 112 | 'Cmd-4': () => showFile(3), 113 | 'Cmd-5': () => showFile(4), 114 | 'Cmd-6': () => showFile(5), 115 | 'Cmd-7': () => showFile(6), 116 | 'Cmd-8': () => showFile(7), 117 | 'Cmd-9': () => showFile(8), 118 | 'Ctrl-1': () => showFile(0), 119 | 'Ctrl-2': () => showFile(1), 120 | 'Ctrl-3': () => showFile(2), 121 | 'Ctrl-4': () => showFile(3), 122 | 'Ctrl-5': () => showFile(4), 123 | 'Ctrl-6': () => showFile(5), 124 | 'Ctrl-7': () => showFile(6), 125 | 'Ctrl-8': () => showFile(7), 126 | 'Ctrl-9': () => showFile(8), 127 | 'Cmd-D': 'selectNextOccurrence', 128 | 'Ctrl-D': 'selectNextOccurrence', 129 | 'Cmd-/': 'toggleCommentIndented', 130 | 'Ctrl-/': 'toggleCommentIndented' 131 | }); 132 | CodeMirror.normalizeKeyMap(); 133 | 134 | container.onMouseUp(() => { 135 | const selection = editor.getSelection(); 136 | const list = editor.listSelections(); 137 | 138 | selection !== '' && onSelection(selection, list); 139 | }); 140 | 141 | return editor; 142 | }; 143 | -------------------------------------------------------------------------------- /src/js-vendor/split.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.5.7 */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var B=window,L=B.document,T="addEventListener",N="removeEventListener",R="getBoundingClientRect",q="horizontal",H=function(){return!1},I=B.attachEvent&&!B[T],i=["","-webkit-","-moz-","-o-"].filter(function(e){var t=L.createElement("div");return t.style.cssText="width:"+e+"calc(9px)",!!t.style.length}).shift()+"calc",s=function(e){return"string"==typeof e||e instanceof String},W=function(e){if(s(e)){var t=L.querySelector(e);if(!t)throw new Error("Selector "+e+" did not match a DOM element");return t}return e},X=function(e,t,n){var r=e[t];return void 0!==r?r:n},Y=function(e,t,n,r){if(t){if("end"===r)return 0;if("center"===r)return e/2}else if(n){if("start"===r)return 0;if("center"===r)return e/2}return e},G=function(e,t){var n=L.createElement("div");return n.className="gutter gutter-"+t,n},J=function(e,t,n){var r={};return s(t)?r[e]=t:r[e]=I?t+"%":i+"("+t+"% - "+n+"px)",r},K=function(e,t){var n;return(n={})[e]=t+"px",n};return function(e,i){void 0===i&&(i={});var u,t,s,o,r,a,l=e;Array.from&&(l=Array.from(l));var c=W(l[0]).parentNode,f=getComputedStyle?getComputedStyle(c).flexDirection:null,m=X(i,"sizes")||l.map(function(){return 100/l.length}),n=X(i,"minSize",100),h=Array.isArray(n)?n:l.map(function(){return n}),d=X(i,"expandToMin",!1),g=X(i,"gutterSize",10),v=X(i,"gutterAlign","center"),p=X(i,"snapOffset",30),y=X(i,"dragInterval",1),z=X(i,"direction",q),S=X(i,"cursor",z===q?"col-resize":"row-resize"),b=X(i,"gutter",G),_=X(i,"elementStyle",J),E=X(i,"gutterStyle",K);function w(t,e,n,r){var i=_(u,e,n,r);Object.keys(i).forEach(function(e){t.style[e]=i[e]})}function k(){return a.map(function(e){return e.size})}function x(e){return"touches"in e?e.touches[0][t]:e[t]}function M(e){var t=a[this.a],n=a[this.b],r=t.size+n.size;t.size=e/this.size*r,n.size=r-e/this.size*r,w(t.element,t.size,this._b,t.i),w(n.element,n.size,this._c,n.i)}function U(){var e=a[this.a].element,t=a[this.b].element,n=e[R](),r=t[R]();this.size=n[u]+r[u]+this._b+this._c,this.start=n[s],this.end=n[o]}function O(s){var o=function(e){if(!getComputedStyle)return null;var t=getComputedStyle(e),n=e[r];return n-=z===q?parseFloat(t.paddingLeft)+parseFloat(t.paddingRight):parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)}(c);if(null===o)return s;var a=0,u=[],e=s.map(function(e,t){var n=o*e/100,r=Y(g,0===t,t===s.length-1,v),i=h[t]+r;return n=this.size-(r.minSize+p+this._c)&&(t=this.size-(r.minSize+this._c)),M.call(this,t),X(i,"onDrag",H)())}.bind(t),t.stop=function(){var e=this,t=a[e.a].element,n=a[e.b].element;e.dragging&&X(i,"onDragEnd",H)(k()),e.dragging=!1,B[N]("mouseup",e.stop),B[N]("touchend",e.stop),B[N]("touchcancel",e.stop),B[N]("mousemove",e.move),B[N]("touchmove",e.move),e.stop=null,e.move=null,t[N]("selectstart",H),t[N]("dragstart",H),n[N]("selectstart",H),n[N]("dragstart",H),t.style.userSelect="",t.style.webkitUserSelect="",t.style.MozUserSelect="",t.style.pointerEvents="",n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",e.gutter.style.cursor="",e.parent.style.cursor="",L.body.style.cursor=""}.bind(t),B[T]("mouseup",t.stop),B[T]("touchend",t.stop),B[T]("touchcancel",t.stop),B[T]("mousemove",t.move),B[T]("touchmove",t.move),n[T]("selectstart",H),n[T]("dragstart",H),r[T]("selectstart",H),r[T]("dragstart",H),n.style.userSelect="none",n.style.webkitUserSelect="none",n.style.MozUserSelect="none",n.style.pointerEvents="none",r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",t.gutter.style.cursor=S,t.parent.style.cursor=S,L.body.style.cursor=S,U.call(t),t.dragOffset=x(e)-t.end}}z===q?(u="width",t="clientX",s="left",o="right",r="clientWidth"):"vertical"===z&&(u="height",t="clientY",s="top",o="bottom",r="clientHeight"),m=O(m);var D=[];function A(e){var t=e.i===D.length,n=t?D[e.i-1]:D[e.i];U.call(n);var r=t?n.size-e.minSize-n._c:e.minSize+n._b;M.call(n,r)}function j(e){var s=O(e);s.forEach(function(e,t){if(0]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i 16 | 17 | CodeMirror.defineMode("gfm", function(config, modeConfig) { 18 | var codeDepth = 0; 19 | function blankLine(state) { 20 | state.code = false; 21 | return null; 22 | } 23 | var gfmOverlay = { 24 | startState: function() { 25 | return { 26 | code: false, 27 | codeBlock: false, 28 | ateSpace: false 29 | }; 30 | }, 31 | copyState: function(s) { 32 | return { 33 | code: s.code, 34 | codeBlock: s.codeBlock, 35 | ateSpace: s.ateSpace 36 | }; 37 | }, 38 | token: function(stream, state) { 39 | state.combineTokens = null; 40 | 41 | // Hack to prevent formatting override inside code blocks (block and inline) 42 | if (state.codeBlock) { 43 | if (stream.match(/^```+/)) { 44 | state.codeBlock = false; 45 | return null; 46 | } 47 | stream.skipToEnd(); 48 | return null; 49 | } 50 | if (stream.sol()) { 51 | state.code = false; 52 | } 53 | if (stream.sol() && stream.match(/^```+/)) { 54 | stream.skipToEnd(); 55 | state.codeBlock = true; 56 | return null; 57 | } 58 | // If this block is changed, it may need to be updated in Markdown mode 59 | if (stream.peek() === '`') { 60 | stream.next(); 61 | var before = stream.pos; 62 | stream.eatWhile('`'); 63 | var difference = 1 + stream.pos - before; 64 | if (!state.code) { 65 | codeDepth = difference; 66 | state.code = true; 67 | } else { 68 | if (difference === codeDepth) { // Must be exact 69 | state.code = false; 70 | } 71 | } 72 | return null; 73 | } else if (state.code) { 74 | stream.next(); 75 | return null; 76 | } 77 | // Check if space. If so, links can be formatted later on 78 | if (stream.eatSpace()) { 79 | state.ateSpace = true; 80 | return null; 81 | } 82 | if (stream.sol() || state.ateSpace) { 83 | state.ateSpace = false; 84 | if (modeConfig.gitHubSpice !== false) { 85 | if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/)) { 86 | // User/Project@SHA 87 | // User@SHA 88 | // SHA 89 | state.combineTokens = true; 90 | return "link"; 91 | } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { 92 | // User/Project#Num 93 | // User#Num 94 | // #Num 95 | state.combineTokens = true; 96 | return "link"; 97 | } 98 | } 99 | } 100 | if (stream.match(urlRE) && 101 | stream.string.slice(stream.start - 2, stream.start) != "](" && 102 | (stream.start == 0 || /\W/.test(stream.string.charAt(stream.start - 1)))) { 103 | // URLs 104 | // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls 105 | // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine 106 | // And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL 107 | state.combineTokens = true; 108 | return "link"; 109 | } 110 | stream.next(); 111 | return null; 112 | }, 113 | blankLine: blankLine 114 | }; 115 | 116 | var markdownConfig = { 117 | taskLists: true, 118 | strikethrough: true, 119 | emoji: true 120 | }; 121 | for (var attr in modeConfig) { 122 | markdownConfig[attr] = modeConfig[attr]; 123 | } 124 | markdownConfig.name = "markdown"; 125 | return CodeMirror.overlayMode(CodeMirror.getMode(config, markdownConfig), gfmOverlay); 126 | 127 | }, "markdown"); 128 | 129 | CodeMirror.defineMIME("text/x-gfm", "gfm"); 130 | }); -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | 8 | "globals": { 9 | "document": false, 10 | "escape": false, 11 | "navigator": false, 12 | "unescape": false, 13 | "window": false, 14 | "describe": true, 15 | "before": true, 16 | "it": true, 17 | "expect": true, 18 | "sinon": true, 19 | "chrome": true, 20 | "browser": true, 21 | "Mousetrap": true, 22 | "io": true, 23 | "jest": true, 24 | "beforeEach": true, 25 | "afterEach": true, 26 | "CodeMirror": true 27 | }, 28 | 29 | "parser": "babel-eslint", 30 | 31 | "plugins": [ 32 | 33 | ], 34 | 35 | "extends": [ 36 | 37 | ], 38 | 39 | "rules": { 40 | "block-scoped-var": 2, 41 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 42 | "camelcase": [2, { "properties": "always" }], 43 | "comma-dangle": [2, "never"], 44 | "comma-spacing": [2, { "before": false, "after": true }], 45 | "comma-style": [2, "last"], 46 | "complexity": 0, 47 | "consistent-return": 2, 48 | "consistent-this": 0, 49 | "curly": [2, "multi-line"], 50 | "default-case": 0, 51 | "dot-location": [2, "property"], 52 | "dot-notation": 0, 53 | "eol-last": 2, 54 | "eqeqeq": [2, "allow-null"], 55 | "func-names": 0, 56 | "func-style": 0, 57 | "generator-star-spacing": [2, "both"], 58 | "guard-for-in": 0, 59 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 60 | "indent": "off", 61 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 62 | "keyword-spacing": [2, {"before": true, "after": true}], 63 | "linebreak-style": 0, 64 | "max-depth": 0, 65 | "max-len": [2, 120, 4], 66 | "max-nested-callbacks": 0, 67 | "max-params": 0, 68 | "max-statements": 0, 69 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 70 | "newline-after-var": [2, "always"], 71 | "new-parens": 2, 72 | "no-alert": 0, 73 | "no-array-constructor": 2, 74 | "no-bitwise": 0, 75 | "no-caller": 2, 76 | "no-catch-shadow": 0, 77 | "no-cond-assign": 2, 78 | "no-console": 0, 79 | "no-constant-condition": 0, 80 | "no-continue": 0, 81 | "no-control-regex": 2, 82 | "no-debugger": 2, 83 | "no-delete-var": 2, 84 | "no-div-regex": 0, 85 | "no-dupe-args": 2, 86 | "no-dupe-keys": 2, 87 | "no-duplicate-case": 2, 88 | "no-else-return": 2, 89 | "no-empty": 0, 90 | "no-empty-character-class": 2, 91 | "no-eq-null": 0, 92 | "no-eval": 2, 93 | "no-ex-assign": 2, 94 | "no-extend-native": 2, 95 | "no-extra-bind": 2, 96 | "no-extra-boolean-cast": 2, 97 | "no-extra-parens": 0, 98 | "no-extra-semi": 0, 99 | "no-extra-strict": 0, 100 | "no-fallthrough": 2, 101 | "no-floating-decimal": 2, 102 | "no-func-assign": 2, 103 | "no-implied-eval": 2, 104 | "no-inline-comments": 0, 105 | "no-inner-declarations": [2, "functions"], 106 | "no-invalid-regexp": 2, 107 | "no-irregular-whitespace": 2, 108 | "no-iterator": 2, 109 | "no-label-var": 2, 110 | "no-labels": 2, 111 | "no-lone-blocks": 0, 112 | "no-lonely-if": 0, 113 | "no-loop-func": 0, 114 | "no-mixed-requires": 0, 115 | "no-mixed-spaces-and-tabs": [2, false], 116 | "no-multi-spaces": 2, 117 | "no-multi-str": 2, 118 | "no-multiple-empty-lines": [2, { "max": 1 }], 119 | "no-native-reassign": 2, 120 | "no-negated-in-lhs": 2, 121 | "no-nested-ternary": 0, 122 | "no-new": 2, 123 | "no-new-func": 2, 124 | "no-new-object": 2, 125 | "no-new-require": 2, 126 | "no-new-wrappers": 2, 127 | "no-obj-calls": 2, 128 | "no-octal": 2, 129 | "no-octal-escape": 2, 130 | "no-path-concat": 0, 131 | "no-plusplus": 0, 132 | "no-process-env": 0, 133 | "no-process-exit": 0, 134 | "no-proto": 2, 135 | "no-redeclare": 2, 136 | "no-regex-spaces": 2, 137 | "no-reserved-keys": 0, 138 | "no-restricted-modules": 0, 139 | "no-return-assign": 2, 140 | "no-script-url": 0, 141 | "no-self-compare": 2, 142 | "no-sequences": 2, 143 | "no-shadow": 0, 144 | "no-shadow-restricted-names": 2, 145 | "no-spaced-func": 2, 146 | "no-sparse-arrays": 2, 147 | "no-sync": 0, 148 | "no-ternary": 0, 149 | "no-throw-literal": 2, 150 | "no-trailing-spaces": 2, 151 | "no-undef": 2, 152 | "no-undef-init": 2, 153 | "no-undefined": 0, 154 | "no-underscore-dangle": 0, 155 | "no-unneeded-ternary": 2, 156 | "no-unreachable": 2, 157 | "no-unused-expressions": 0, 158 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 159 | "no-use-before-define": 2, 160 | "no-var": 0, 161 | "no-void": 0, 162 | "no-warning-comments": 0, 163 | "no-with": 2, 164 | "one-var": 0, 165 | "operator-assignment": 0, 166 | "operator-linebreak": [2, "after"], 167 | "padded-blocks": 0, 168 | "quote-props": 0, 169 | "quotes": [2, "single", "avoid-escape"], 170 | "radix": 2, 171 | "semi": [2, "always"], 172 | "semi-spacing": 0, 173 | "sort-vars": 0, 174 | "space-before-blocks": [2, "always"], 175 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 176 | "space-in-brackets": 0, 177 | "space-in-parens": [2, "never"], 178 | "space-infix-ops": 2, 179 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 180 | "spaced-comment": [2, "always"], 181 | "strict": 0, 182 | "use-isnan": 2, 183 | "valid-jsdoc": 0, 184 | "valid-typeof": 2, 185 | "vars-on-top": 2, 186 | "wrap-iife": [2, "any"], 187 | "wrap-regex": 0, 188 | "yoda": [2, "never"] 189 | } 190 | } -------------------------------------------------------------------------------- /src/js/utils/element.js: -------------------------------------------------------------------------------- 1 | var createdElements = []; 2 | 3 | export default function el(selector, parent = document, fallbackToEmpty = false, relaxedCleanup = false) { 4 | const removeListenersCallbacks = []; 5 | var e = typeof selector === 'string' ? parent.querySelector(selector) : selector; 6 | var found = true; 7 | 8 | if (!e) { 9 | found = false; 10 | if (!fallbackToEmpty) { 11 | throw new Error(`Ops! There is no DOM element matching "${ selector }" selector.`); 12 | } else { 13 | e = document.createElement('div'); 14 | } 15 | } 16 | 17 | const registerEventListener = (type, callback) => { 18 | e.addEventListener(type, callback); 19 | 20 | const removeListener = () => e.removeEventListener(type, callback); 21 | 22 | removeListenersCallbacks.push(removeListener); 23 | return removeListener; 24 | }; 25 | 26 | const api = { 27 | e, 28 | found() { 29 | return found; 30 | }, 31 | content(str) { 32 | if (!str) { 33 | return e.innerHTML; 34 | } 35 | removeListenersCallbacks.forEach(c => c()); 36 | e.innerHTML = str; 37 | return this.exports(); 38 | }, 39 | text(str) { 40 | if (!str) { 41 | return e.innerText; 42 | } 43 | e.innerText = str; 44 | return str; 45 | }, 46 | appendChild(child) { 47 | e.appendChild(child); 48 | return this; 49 | }, 50 | appendChildren(children) { 51 | children.forEach(c => e.appendChild(c.e)); 52 | return this; 53 | }, 54 | css(prop, value) { 55 | if (typeof value !== 'undefined') { 56 | e.style[prop] = value; 57 | return this; 58 | } 59 | return e.style[prop]; 60 | }, 61 | clearCSS() { 62 | e.style = {}; 63 | return this; 64 | }, 65 | prop(name, value) { 66 | if (typeof value !== 'undefined') { 67 | e[name] = value; 68 | return this; 69 | } 70 | return e[name]; 71 | }, 72 | attr(attr, value) { 73 | if (typeof value !== 'undefined') { 74 | e.setAttribute(attr, value); 75 | return this; 76 | } 77 | return e.getAttribute(attr); 78 | }, 79 | onClick(callback) { 80 | return registerEventListener('click', callback); 81 | }, 82 | onKeyUp(callback) { 83 | return registerEventListener('keyup', callback); 84 | }, 85 | onKeyDown(callback) { 86 | return registerEventListener('keydown', callback); 87 | }, 88 | onMouseOver(callback) { 89 | return registerEventListener('mouseover', callback); 90 | }, 91 | onMouseOut(callback) { 92 | return registerEventListener('mouseout', callback); 93 | }, 94 | onMouseUp(callback) { 95 | return registerEventListener('mouseup', callback); 96 | }, 97 | onRightClick(callback) { 98 | const handler = event => { 99 | event.preventDefault(); 100 | callback(); 101 | }; 102 | 103 | e.addEventListener('contextmenu', handler); 104 | 105 | const removeListener = () => e.removeEventListener('oncontextmenu', handler); 106 | 107 | removeListenersCallbacks.push(removeListener); 108 | return removeListener; 109 | }, 110 | onChange(callback) { 111 | e.addEventListener('change', () => callback(e.value)); 112 | 113 | const removeListener = () => e.removeEventListener('change', callback); 114 | 115 | removeListenersCallbacks.push(removeListener); 116 | return removeListener; 117 | }, 118 | find(selector) { 119 | return el(selector, e); 120 | }, 121 | appendTo(parent) { 122 | parent.e.appendChild(e); 123 | }, 124 | exports() { 125 | return Array 126 | .prototype.slice.call(e.querySelectorAll('[data-export]')) 127 | .map(element => el(element, e)); 128 | }, 129 | namedExports() { 130 | return this.exports().reduce((result, exportedElement) => { 131 | result[exportedElement.attr('data-export')] = exportedElement; 132 | return result; 133 | }, {}); 134 | }, 135 | detach() { 136 | if (e.parentNode && e.parentNode.contains(e)) { 137 | e.parentNode.removeChild(e); 138 | } 139 | }, 140 | empty() { 141 | while (e.firstChild) { 142 | e.removeChild(e.firstChild); 143 | } 144 | return this; 145 | }, 146 | destroy() { 147 | removeListenersCallbacks.forEach(c => c()); 148 | if (!relaxedCleanup) { 149 | this.empty(); 150 | this.detach(); 151 | } 152 | }, 153 | scrollToBottom() { 154 | e.scrollTop = e.scrollHeight; 155 | }, 156 | selectOnClick() { 157 | const removeListener = this.onClick(() => { 158 | e.select(); 159 | removeListener(); 160 | }); 161 | } 162 | }; 163 | 164 | createdElements.push(api); 165 | 166 | return api; 167 | } 168 | 169 | el.fromString = str => { 170 | const node = document.createElement('div'); 171 | 172 | node.innerHTML = str; 173 | 174 | const filteredNodes = Array.prototype.slice.call(node.childNodes).filter(node => node.nodeType === 1); 175 | 176 | if (filteredNodes.length > 0) { 177 | return el(filteredNodes[0]); 178 | } 179 | throw new Error('fromString accepts HTMl with a single parent.'); 180 | }; 181 | el.wrap = elements => el(document.createElement('div')).appendChildren(elements); 182 | el.fromTemplate = selector => el.fromString(document.querySelector(selector).innerHTML); 183 | el.withFallback = selector => el(selector, document, true); 184 | el.withRelaxedCleanup = selector => el(selector, document, false, true); 185 | el.destroy = () => { 186 | createdElements.forEach(elInstance => elInstance.destroy()); 187 | createdElements = []; 188 | }; 189 | el.exists = (selector) => { 190 | return !!document.querySelector(selector); 191 | }; 192 | -------------------------------------------------------------------------------- /src/js-vendor/jsx.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript")) 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript"], mod) 9 | else // Plain browser env 10 | mod(CodeMirror) 11 | })(function(CodeMirror) { 12 | "use strict" 13 | 14 | // Depth means the amount of open braces in JS context, in XML 15 | // context 0 means not in tag, 1 means in tag, and 2 means in tag 16 | // and js block comment. 17 | function Context(state, mode, depth, prev) { 18 | this.state = state; this.mode = mode; this.depth = depth; this.prev = prev 19 | } 20 | 21 | function copyContext(context) { 22 | return new Context(CodeMirror.copyState(context.mode, context.state), 23 | context.mode, 24 | context.depth, 25 | context.prev && copyContext(context.prev)) 26 | } 27 | 28 | CodeMirror.defineMode("jsx", function(config, modeConfig) { 29 | var xmlMode = CodeMirror.getMode(config, {name: "xml", allowMissing: true, multilineTagIndentPastTag: false, allowMissingTagName: true}) 30 | var jsMode = CodeMirror.getMode(config, modeConfig && modeConfig.base || "javascript") 31 | 32 | function flatXMLIndent(state) { 33 | var tagName = state.tagName 34 | state.tagName = null 35 | var result = xmlMode.indent(state, "") 36 | state.tagName = tagName 37 | return result 38 | } 39 | 40 | function token(stream, state) { 41 | if (state.context.mode == xmlMode) 42 | return xmlToken(stream, state, state.context) 43 | else 44 | return jsToken(stream, state, state.context) 45 | } 46 | 47 | function xmlToken(stream, state, cx) { 48 | if (cx.depth == 2) { // Inside a JS /* */ comment 49 | if (stream.match(/^.*?\*\//)) cx.depth = 1 50 | else stream.skipToEnd() 51 | return "comment" 52 | } 53 | 54 | if (stream.peek() == "{") { 55 | xmlMode.skipAttribute(cx.state) 56 | 57 | var indent = flatXMLIndent(cx.state), xmlContext = cx.state.context 58 | // If JS starts on same line as tag 59 | if (xmlContext && stream.match(/^[^>]*>\s*$/, false)) { 60 | while (xmlContext.prev && !xmlContext.startOfLine) 61 | xmlContext = xmlContext.prev 62 | // If tag starts the line, use XML indentation level 63 | if (xmlContext.startOfLine) indent -= config.indentUnit 64 | // Else use JS indentation level 65 | else if (cx.prev.state.lexical) indent = cx.prev.state.lexical.indented 66 | // Else if inside of tag 67 | } else if (cx.depth == 1) { 68 | indent += config.indentUnit 69 | } 70 | 71 | state.context = new Context(CodeMirror.startState(jsMode, indent), 72 | jsMode, 0, state.context) 73 | return null 74 | } 75 | 76 | if (cx.depth == 1) { // Inside of tag 77 | if (stream.peek() == "<") { // Tag inside of tag 78 | xmlMode.skipAttribute(cx.state) 79 | state.context = new Context(CodeMirror.startState(xmlMode, flatXMLIndent(cx.state)), 80 | xmlMode, 0, state.context) 81 | return null 82 | } else if (stream.match("//")) { 83 | stream.skipToEnd() 84 | return "comment" 85 | } else if (stream.match("/*")) { 86 | cx.depth = 2 87 | return token(stream, state) 88 | } 89 | } 90 | 91 | var style = xmlMode.token(stream, cx.state), cur = stream.current(), stop 92 | if (/\btag\b/.test(style)) { 93 | if (/>$/.test(cur)) { 94 | if (cx.state.context) cx.depth = 0 95 | else state.context = state.context.prev 96 | } else if (/^ -1) { 100 | stream.backUp(cur.length - stop) 101 | } 102 | return style 103 | } 104 | 105 | function jsToken(stream, state, cx) { 106 | if (stream.peek() == "<" && jsMode.expressionAllowed(stream, cx.state)) { 107 | jsMode.skipExpression(cx.state) 108 | state.context = new Context(CodeMirror.startState(xmlMode, jsMode.indent(cx.state, "")), 109 | xmlMode, 0, state.context) 110 | return null 111 | } 112 | 113 | var style = jsMode.token(stream, cx.state) 114 | if (!style && cx.depth != null) { 115 | var cur = stream.current() 116 | if (cur == "{") { 117 | cx.depth++ 118 | } else if (cur == "}") { 119 | if (--cx.depth == 0) state.context = state.context.prev 120 | } 121 | } 122 | return style 123 | } 124 | 125 | return { 126 | startState: function() { 127 | return {context: new Context(CodeMirror.startState(jsMode), jsMode)} 128 | }, 129 | 130 | copyState: function(state) { 131 | return {context: copyContext(state.context)} 132 | }, 133 | 134 | token: token, 135 | 136 | indent: function(state, textAfter, fullLine) { 137 | return state.context.mode.indent(state.context.state, textAfter, fullLine) 138 | }, 139 | 140 | innerMode: function(state) { 141 | return state.context 142 | } 143 | } 144 | }, "xml", "javascript") 145 | 146 | CodeMirror.defineMIME("text/jsx", "jsx") 147 | CodeMirror.defineMIME("text/typescript-jsx", {name: "jsx", base: {name: "javascript", typescript: true}}) 148 | }); 149 | -------------------------------------------------------------------------------- /src/js/statusBar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-sequences */ 2 | import el from './utils/element'; 3 | import { CLOSE_ICON, PLUS_ICON, SETTINGS_ICON, NO_USER, FORK, SHARE, BARS } from './utils/icons'; 4 | import { IS_PROD } from './constants'; 5 | 6 | const STATUS_BAR_HIDDEN_HEIGHT = '6px'; 7 | const STATUS_BAR_VISIBLE_HEIGHT = '36px'; 8 | 9 | const showProfilePicAndName = profile => { 10 | return ``; 11 | }; 12 | const createLink = (exportKey, label, className = '', href = 'javascript:void(0)') => { 13 | return `${ label }`; 14 | }; 15 | const createStr = (str, n) => Array(n).join(str); 16 | 17 | export default function statusBar(state, showFile, newFile, editFile, showSettings, saveCurrentFile, editName) { 18 | const bar = el.withRelaxedCleanup('.status-bar'); 19 | const layout = el.withRelaxedCleanup('.app .layout'); 20 | const menu = el.withRelaxedCleanup('.status-bar-menu'); 21 | let visibility = !!state.getEditorSettings().statusBar; 22 | let visibilityMenu = false; 23 | let pending = false; 24 | 25 | const toggleMenu = () => { 26 | menu.css('display', (visibilityMenu = !visibilityMenu) ? 'block' : 'none'); 27 | }; 28 | 29 | const render = () => { 30 | const items = []; 31 | const menuItems = []; 32 | const files = state.getFiles(); 33 | 34 | items.push('
'); 35 | files.forEach(([ filename, file ]) => { 36 | const isCurrentFile = state.isCurrentFile(filename); 37 | 38 | items.push(createLink( 39 | 'file:' + filename, 40 | `${ filename }${ isCurrentFile && pending ? '*' : ''}`, 41 | `file${ isCurrentFile ? ' active' : '' }${ file.en ? ' entry' : ''}` 42 | )); 43 | }); 44 | items.push(createLink('newFileButton', PLUS_ICON(14), 'new-file')); 45 | items.push(` 46 |
47 | ${ state.meta().published ? '✔ ' : '' } 48 | ${ state.meta().name ? state.meta().name : 'unnamed' } 49 | ${ state.loggedIn() && !state.isDemoOwner() ? 'not yours' : '' } 50 | ${ IS_PROD ? `view Story` : '' } 51 |
52 | `); 53 | items.push(createLink('menuButton', BARS(14))); 54 | items.push(createLink('closeButton', CLOSE_ICON(14))); 55 | items.push('
'); 56 | 57 | // `/login?did=${ demoId }`; 58 | // '/u/' + state.getProfile().id; 59 | IS_PROD && menuItems.push(createLink( 60 | 'profileButton', 61 | state.loggedIn() ? 62 | showProfilePicAndName(state.getProfile()) + ' Profile' : 63 | NO_USER() + ' Log in', 64 | 'profile', 65 | state.loggedIn() ? 66 | '/u/' + state.getProfile().id : 67 | `/login?did=${ state.getDemoId() }` 68 | )); 69 | IS_PROD && menuItems.push(createLink('', PLUS_ICON(14) + ' New story', '', '/new')); 70 | state.isForkable() && menuItems.push(createLink('forkButton', FORK(14) + ' Fork')); 71 | IS_PROD && menuItems.push(createLink('shareButton', SHARE(14) + ' Share/Embed')); 72 | state.isDemoOwner() && menuItems.push(createLink('nameButton', SETTINGS_ICON(14) + ' Story')); 73 | menuItems.push(createLink('settingsButton', SETTINGS_ICON(14) + ' Editor')); 74 | state.isForkable() && menuItems.push(createLink('', NO_USER() + ' Log out', '', `/logout?r=e/${ state.getDemoId() }`)); 75 | 76 | bar.content(items.join('')).forEach(button => { 77 | if (button.attr('data-export').indexOf('file') === 0) { 78 | const filename = button.attr('data-export').split(':').pop(); 79 | 80 | button.onClick(() => { 81 | if (!state.isCurrentFile(filename)) { 82 | showFile(filename); 83 | } else if (pending) { 84 | saveCurrentFile(); 85 | } 86 | }); 87 | button.onRightClick(() => editFile(filename)); 88 | } 89 | }); 90 | menu.content(menuItems.join('')); 91 | 92 | const { newFileButton, closeButton, menuButton } = bar.namedExports(); 93 | const { forkButton, shareButton, nameButton, settingsButton } = menu.namedExports(); 94 | 95 | const manageVisibility = () => { 96 | const { buttons } = bar.namedExports(); 97 | 98 | buttons.css('display', visibility ? 'grid' : 'none'); 99 | buttons.css('gridTemplateColumns', [ 100 | createStr('minmax(auto, 135px) ', state.getNumOfFiles() + 1), 101 | '30px', 102 | '1fr', 103 | '30px', 104 | '30px' 105 | ].filter(value => value).join(' ')); 106 | bar.css('height', visibility ? STATUS_BAR_VISIBLE_HEIGHT : STATUS_BAR_HIDDEN_HEIGHT); 107 | layout.css('height', visibility ? `calc(100% - ${ STATUS_BAR_VISIBLE_HEIGHT })` : `calc(100% - ${ STATUS_BAR_HIDDEN_HEIGHT })`); 108 | state.updateStatusBarVisibility(visibility); 109 | }; 110 | 111 | newFileButton && newFileButton.onClick(newFile); 112 | shareButton && shareButton.onClick(() => (showSettings(2), toggleMenu())); 113 | settingsButton && settingsButton.onClick(() => (showSettings(), toggleMenu())); 114 | state.isDemoOwner() && nameButton && nameButton.onClick(() => (editName(), toggleMenu())); 115 | forkButton && forkButton.onClick(() => (state.fork(), toggleMenu())); 116 | menuButton && menuButton.onClick(toggleMenu); 117 | closeButton.onClick(e => { 118 | e.stopPropagation(); 119 | visibility = false; 120 | manageVisibility(); 121 | }); 122 | bar.onClick(() => { 123 | if (!visibility) { 124 | visibility = true; 125 | manageVisibility(); 126 | } 127 | }); 128 | 129 | manageVisibility(); 130 | }; 131 | 132 | render(); 133 | 134 | state.listen(render); 135 | 136 | return (value) => { 137 | pending = value; 138 | render(); 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /src/js-vendor/htmlmixed.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | var defaultTags = { 15 | script: [ 16 | ["lang", /(javascript|babel)/i, "javascript"], 17 | ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"], 18 | ["type", /./, "text/plain"], 19 | [null, null, "javascript"] 20 | ], 21 | style: [ 22 | ["lang", /^css$/i, "css"], 23 | ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"], 24 | ["type", /./, "text/plain"], 25 | [null, null, "css"] 26 | ] 27 | }; 28 | 29 | function maybeBackup(stream, pat, style) { 30 | var cur = stream.current(), close = cur.search(pat); 31 | if (close > -1) { 32 | stream.backUp(cur.length - close); 33 | } else if (cur.match(/<\/?$/)) { 34 | stream.backUp(cur.length); 35 | if (!stream.match(pat, false)) stream.match(cur); 36 | } 37 | return style; 38 | } 39 | 40 | var attrRegexpCache = {}; 41 | function getAttrRegexp(attr) { 42 | var regexp = attrRegexpCache[attr]; 43 | if (regexp) return regexp; 44 | return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"); 45 | } 46 | 47 | function getAttrValue(text, attr) { 48 | var match = text.match(getAttrRegexp(attr)) 49 | return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : "" 50 | } 51 | 52 | function getTagRegexp(tagName, anchored) { 53 | return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i"); 54 | } 55 | 56 | function addTags(from, to) { 57 | for (var tag in from) { 58 | var dest = to[tag] || (to[tag] = []); 59 | var source = from[tag]; 60 | for (var i = source.length - 1; i >= 0; i--) 61 | dest.unshift(source[i]) 62 | } 63 | } 64 | 65 | function findMatchingMode(tagInfo, tagText) { 66 | for (var i = 0; i < tagInfo.length; i++) { 67 | var spec = tagInfo[i]; 68 | if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2]; 69 | } 70 | } 71 | 72 | CodeMirror.defineMode("htmlmixed", function (config, parserConfig) { 73 | var htmlMode = CodeMirror.getMode(config, { 74 | name: "xml", 75 | htmlMode: true, 76 | multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, 77 | multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag 78 | }); 79 | 80 | var tags = {}; 81 | var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes; 82 | addTags(defaultTags, tags); 83 | if (configTags) addTags(configTags, tags); 84 | if (configScript) for (var i = configScript.length - 1; i >= 0; i--) 85 | tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]) 86 | 87 | function html(stream, state) { 88 | var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName 89 | if (tag && !/[<>\s\/]/.test(stream.current()) && 90 | (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) && 91 | tags.hasOwnProperty(tagName)) { 92 | state.inTag = tagName + " " 93 | } else if (state.inTag && tag && />$/.test(stream.current())) { 94 | var inTag = /^([\S]+) (.*)/.exec(state.inTag) 95 | state.inTag = null 96 | var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2]) 97 | var mode = CodeMirror.getMode(config, modeSpec) 98 | var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false); 99 | state.token = function (stream, state) { 100 | if (stream.match(endTagA, false)) { 101 | state.token = html; 102 | state.localState = state.localMode = null; 103 | return null; 104 | } 105 | return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState)); 106 | }; 107 | state.localMode = mode; 108 | state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "")); 109 | } else if (state.inTag) { 110 | state.inTag += stream.current() 111 | if (stream.eol()) state.inTag += " " 112 | } 113 | return style; 114 | }; 115 | 116 | return { 117 | startState: function () { 118 | var state = CodeMirror.startState(htmlMode); 119 | return {token: html, inTag: null, localMode: null, localState: null, htmlState: state}; 120 | }, 121 | 122 | copyState: function (state) { 123 | var local; 124 | if (state.localState) { 125 | local = CodeMirror.copyState(state.localMode, state.localState); 126 | } 127 | return {token: state.token, inTag: state.inTag, 128 | localMode: state.localMode, localState: local, 129 | htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; 130 | }, 131 | 132 | token: function (stream, state) { 133 | return state.token(stream, state); 134 | }, 135 | 136 | indent: function (state, textAfter, line) { 137 | if (!state.localMode || /^\s*<\//.test(textAfter)) 138 | return htmlMode.indent(state.htmlState, textAfter); 139 | else if (state.localMode.indent) 140 | return state.localMode.indent(state.localState, textAfter, line); 141 | else 142 | return CodeMirror.Pass; 143 | }, 144 | 145 | innerMode: function (state) { 146 | return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; 147 | } 148 | }; 149 | }, "xml", "javascript", "css"); 150 | 151 | CodeMirror.defineMIME("text/html", "htmlmixed"); 152 | }); -------------------------------------------------------------------------------- /src/js-vendor/match-highlighter.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | // Highlighting text that matches the selection 5 | // 6 | // Defines an option highlightSelectionMatches, which, when enabled, 7 | // will style strings that match the selection throughout the 8 | // document. 9 | // 10 | // The option can be set to true to simply enable it, or to a 11 | // {minChars, style, wordsOnly, showToken, delay} object to explicitly 12 | // configure it. minChars is the minimum amount of characters that should be 13 | // selected for the behavior to occur, and style is the token style to 14 | // apply to the matches. This will be prefixed by "cm-" to create an 15 | // actual CSS class name. If wordsOnly is enabled, the matches will be 16 | // highlighted only if the selected text is a word. showToken, when enabled, 17 | // will cause the current token to be highlighted when nothing is selected. 18 | // delay is used to specify how much time to wait, in milliseconds, before 19 | // highlighting the matches. If annotateScrollbar is enabled, the occurences 20 | // will be highlighted on the scrollbar via the matchesonscrollbar addon. 21 | 22 | (function(mod) { 23 | if (typeof exports == "object" && typeof module == "object") // CommonJS 24 | mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); 25 | else if (typeof define == "function" && define.amd) // AMD 26 | define(["../../lib/codemirror", "./matchesonscrollbar"], mod); 27 | else // Plain browser env 28 | mod(CodeMirror); 29 | })(function(CodeMirror) { 30 | "use strict"; 31 | 32 | var defaults = { 33 | style: "matchhighlight", 34 | minChars: 2, 35 | delay: 100, 36 | wordsOnly: false, 37 | annotateScrollbar: false, 38 | showToken: false, 39 | trim: true 40 | } 41 | 42 | function State(options) { 43 | this.options = {} 44 | for (var name in defaults) 45 | this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] 46 | this.overlay = this.timeout = null; 47 | this.matchesonscroll = null; 48 | this.active = false; 49 | } 50 | 51 | CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { 52 | if (old && old != CodeMirror.Init) { 53 | removeOverlay(cm); 54 | clearTimeout(cm.state.matchHighlighter.timeout); 55 | cm.state.matchHighlighter = null; 56 | cm.off("cursorActivity", cursorActivity); 57 | cm.off("focus", onFocus) 58 | } 59 | if (val) { 60 | var state = cm.state.matchHighlighter = new State(val); 61 | if (cm.hasFocus()) { 62 | state.active = true 63 | highlightMatches(cm) 64 | } else { 65 | cm.on("focus", onFocus) 66 | } 67 | cm.on("cursorActivity", cursorActivity); 68 | } 69 | }); 70 | 71 | function cursorActivity(cm) { 72 | var state = cm.state.matchHighlighter; 73 | if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) 74 | } 75 | 76 | function onFocus(cm) { 77 | var state = cm.state.matchHighlighter 78 | if (!state.active) { 79 | state.active = true 80 | scheduleHighlight(cm, state) 81 | } 82 | } 83 | 84 | function scheduleHighlight(cm, state) { 85 | clearTimeout(state.timeout); 86 | state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); 87 | } 88 | 89 | function addOverlay(cm, query, hasBoundary, style) { 90 | var state = cm.state.matchHighlighter; 91 | cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); 92 | if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { 93 | var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query; 94 | state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, 95 | {className: "CodeMirror-selection-highlight-scrollbar"}); 96 | } 97 | } 98 | 99 | function removeOverlay(cm) { 100 | var state = cm.state.matchHighlighter; 101 | if (state.overlay) { 102 | cm.removeOverlay(state.overlay); 103 | state.overlay = null; 104 | if (state.matchesonscroll) { 105 | state.matchesonscroll.clear(); 106 | state.matchesonscroll = null; 107 | } 108 | } 109 | } 110 | 111 | function highlightMatches(cm) { 112 | cm.operation(function() { 113 | var state = cm.state.matchHighlighter; 114 | removeOverlay(cm); 115 | if (!cm.somethingSelected() && state.options.showToken) { 116 | var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; 117 | var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; 118 | while (start && re.test(line.charAt(start - 1))) --start; 119 | while (end < line.length && re.test(line.charAt(end))) ++end; 120 | if (start < end) 121 | addOverlay(cm, line.slice(start, end), re, state.options.style); 122 | return; 123 | } 124 | var from = cm.getCursor("from"), to = cm.getCursor("to"); 125 | if (from.line != to.line) return; 126 | if (state.options.wordsOnly && !isWord(cm, from, to)) return; 127 | var selection = cm.getRange(from, to) 128 | if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") 129 | if (selection.length >= state.options.minChars) 130 | addOverlay(cm, selection, false, state.options.style); 131 | }); 132 | } 133 | 134 | function isWord(cm, from, to) { 135 | var str = cm.getRange(from, to); 136 | if (str.match(/^\w+$/) !== null) { 137 | if (from.ch > 0) { 138 | var pos = {line: from.line, ch: from.ch - 1}; 139 | var chr = cm.getRange(pos, from); 140 | if (chr.match(/\W/) === null) return false; 141 | } 142 | if (to.ch < cm.getLine(from.line).length) { 143 | var pos = {line: to.line, ch: to.ch + 1}; 144 | var chr = cm.getRange(to, pos); 145 | if (chr.match(/\W/) === null) return false; 146 | } 147 | return true; 148 | } else return false; 149 | } 150 | 151 | function boundariesAround(stream, re) { 152 | return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && 153 | (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); 154 | } 155 | 156 | function makeOverlay(query, hasBoundary, style) { 157 | return {token: function(stream) { 158 | if (stream.match(query) && 159 | (!hasBoundary || boundariesAround(stream, hasBoundary))) 160 | return style; 161 | stream.next(); 162 | stream.skipTo(query.charAt(0)) || stream.skipToEnd(); 163 | }}; 164 | } 165 | }); -------------------------------------------------------------------------------- /src/js/utils/icons.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | export const TRASH_ICON = (size = 20) => ``; 4 | export const CHECK_ICON = (size = 20) => ``; 5 | export const CLOSE_ICON = (size = 24) => ``; 6 | export const PLUS_ICON = (size = 24) => ``; 7 | export const SETTINGS_ICON = (size = 24) => ``; 8 | export const DOT_CIRCLE = (size = 24) => ``; 9 | export const NO_USER = (sizeW = 16, sizeH = 14) => ``; 10 | export const FORK = (size = 14) => ``; 11 | export const SHARE = (size = 14) => ``; 12 | export const BARS = (size = 14) => ``; 13 | export const EYE = (size = 14) => ``; 14 | export const BOOK = (size = 14) => ``; 15 | -------------------------------------------------------------------------------- /src/js-vendor/matchbrackets.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && 13 | (document.documentMode == null || document.documentMode < 8); 14 | 15 | var Pos = CodeMirror.Pos; 16 | 17 | var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"}; 18 | 19 | function bracketRegex(config) { 20 | return config && config.bracketRegex || /[(){}[\]]/ 21 | } 22 | 23 | function findMatchingBracket(cm, where, config) { 24 | var line = cm.getLineHandle(where.line), pos = where.ch - 1; 25 | var afterCursor = config && config.afterCursor 26 | if (afterCursor == null) 27 | afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) 28 | var re = bracketRegex(config) 29 | 30 | // A cursor is defined as between two characters, but in in vim command mode 31 | // (i.e. not insert mode), the cursor is visually represented as a 32 | // highlighted box on top of the 2nd character. Otherwise, we allow matches 33 | // from before or after the cursor. 34 | var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) || 35 | re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)]; 36 | if (!match) return null; 37 | var dir = match.charAt(1) == ">" ? 1 : -1; 38 | if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; 39 | var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); 40 | 41 | var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); 42 | if (found == null) return null; 43 | return {from: Pos(where.line, pos), to: found && found.pos, 44 | match: found && found.ch == match.charAt(0), forward: dir > 0}; 45 | } 46 | 47 | // bracketRegex is used to specify which type of bracket to scan 48 | // should be a regexp, e.g. /[[\]]/ 49 | // 50 | // Note: If "where" is on an open bracket, then this bracket is ignored. 51 | // 52 | // Returns false when no bracket was found, null when it reached 53 | // maxScanLines and gave up 54 | function scanForBracket(cm, where, dir, style, config) { 55 | var maxScanLen = (config && config.maxScanLineLength) || 10000; 56 | var maxScanLines = (config && config.maxScanLines) || 1000; 57 | 58 | var stack = []; 59 | var re = bracketRegex(config) 60 | var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) 61 | : Math.max(cm.firstLine() - 1, where.line - maxScanLines); 62 | for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { 63 | var line = cm.getLine(lineNo); 64 | if (!line) continue; 65 | var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; 66 | if (line.length > maxScanLen) continue; 67 | if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); 68 | for (; pos != end; pos += dir) { 69 | var ch = line.charAt(pos); 70 | if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { 71 | var match = matching[ch]; 72 | if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); 73 | else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; 74 | else stack.pop(); 75 | } 76 | } 77 | } 78 | return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; 79 | } 80 | 81 | function matchBrackets(cm, autoclear, config) { 82 | // Disable brace matching in long lines, since it'll cause hugely slow updates 83 | var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; 84 | var marks = [], ranges = cm.listSelections(); 85 | for (var i = 0; i < ranges.length; i++) { 86 | var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); 87 | if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { 88 | var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; 89 | marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); 90 | if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) 91 | marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); 92 | } 93 | } 94 | 95 | if (marks.length) { 96 | // Kludge to work around the IE bug from issue #1193, where text 97 | // input stops going to the textare whever this fires. 98 | if (ie_lt8 && cm.state.focused) cm.focus(); 99 | 100 | var clear = function() { 101 | cm.operation(function() { 102 | for (var i = 0; i < marks.length; i++) marks[i].clear(); 103 | }); 104 | }; 105 | if (autoclear) setTimeout(clear, 800); 106 | else return clear; 107 | } 108 | } 109 | 110 | function doMatchBrackets(cm) { 111 | cm.operation(function() { 112 | if (cm.state.matchBrackets.currentlyHighlighted) { 113 | cm.state.matchBrackets.currentlyHighlighted(); 114 | cm.state.matchBrackets.currentlyHighlighted = null; 115 | } 116 | cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); 117 | }); 118 | } 119 | 120 | CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { 121 | if (old && old != CodeMirror.Init) { 122 | cm.off("cursorActivity", doMatchBrackets); 123 | if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { 124 | cm.state.matchBrackets.currentlyHighlighted(); 125 | cm.state.matchBrackets.currentlyHighlighted = null; 126 | } 127 | } 128 | if (val) { 129 | cm.state.matchBrackets = typeof val == "object" ? val : {}; 130 | cm.on("cursorActivity", doMatchBrackets); 131 | } 132 | }); 133 | 134 | CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); 135 | CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ 136 | // Backwards-compatibility kludge 137 | if (oldConfig || typeof config == "boolean") { 138 | if (!oldConfig) { 139 | config = config ? {strict: true} : null 140 | } else { 141 | oldConfig.strict = config 142 | config = oldConfig 143 | } 144 | } 145 | return findMatchingBracket(this, pos, config) 146 | }); 147 | CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ 148 | return scanForBracket(this, pos, dir, style, config); 149 | }); 150 | }); -------------------------------------------------------------------------------- /src/js-vendor/closebrackets.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | var defaults = { 13 | pairs: "()[]{}''\"\"", 14 | triples: "", 15 | explode: "[]{}" 16 | }; 17 | 18 | var Pos = CodeMirror.Pos; 19 | 20 | CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { 21 | if (old && old != CodeMirror.Init) { 22 | cm.removeKeyMap(keyMap); 23 | cm.state.closeBrackets = null; 24 | } 25 | if (val) { 26 | ensureBound(getOption(val, "pairs")) 27 | cm.state.closeBrackets = val; 28 | cm.addKeyMap(keyMap); 29 | } 30 | }); 31 | 32 | function getOption(conf, name) { 33 | if (name == "pairs" && typeof conf == "string") return conf; 34 | if (typeof conf == "object" && conf[name] != null) return conf[name]; 35 | return defaults[name]; 36 | } 37 | 38 | var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; 39 | function ensureBound(chars) { 40 | for (var i = 0; i < chars.length; i++) { 41 | var ch = chars.charAt(i), key = "'" + ch + "'" 42 | if (!keyMap[key]) keyMap[key] = handler(ch) 43 | } 44 | } 45 | ensureBound(defaults.pairs + "`") 46 | 47 | function handler(ch) { 48 | return function(cm) { return handleChar(cm, ch); }; 49 | } 50 | 51 | function getConfig(cm) { 52 | var deflt = cm.state.closeBrackets; 53 | if (!deflt || deflt.override) return deflt; 54 | var mode = cm.getModeAt(cm.getCursor()); 55 | return mode.closeBrackets || deflt; 56 | } 57 | 58 | function handleBackspace(cm) { 59 | var conf = getConfig(cm); 60 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; 61 | 62 | var pairs = getOption(conf, "pairs"); 63 | var ranges = cm.listSelections(); 64 | for (var i = 0; i < ranges.length; i++) { 65 | if (!ranges[i].empty()) return CodeMirror.Pass; 66 | var around = charsAround(cm, ranges[i].head); 67 | if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; 68 | } 69 | for (var i = ranges.length - 1; i >= 0; i--) { 70 | var cur = ranges[i].head; 71 | cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); 72 | } 73 | } 74 | 75 | function handleEnter(cm) { 76 | var conf = getConfig(cm); 77 | var explode = conf && getOption(conf, "explode"); 78 | if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; 79 | 80 | var ranges = cm.listSelections(); 81 | for (var i = 0; i < ranges.length; i++) { 82 | if (!ranges[i].empty()) return CodeMirror.Pass; 83 | var around = charsAround(cm, ranges[i].head); 84 | if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; 85 | } 86 | cm.operation(function() { 87 | var linesep = cm.lineSeparator() || "\n"; 88 | cm.replaceSelection(linesep + linesep, null); 89 | cm.execCommand("goCharLeft"); 90 | ranges = cm.listSelections(); 91 | for (var i = 0; i < ranges.length; i++) { 92 | var line = ranges[i].head.line; 93 | cm.indentLine(line, null, true); 94 | cm.indentLine(line + 1, null, true); 95 | } 96 | }); 97 | } 98 | 99 | function contractSelection(sel) { 100 | var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; 101 | return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), 102 | head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; 103 | } 104 | 105 | function handleChar(cm, ch) { 106 | var conf = getConfig(cm); 107 | if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; 108 | 109 | var pairs = getOption(conf, "pairs"); 110 | var pos = pairs.indexOf(ch); 111 | if (pos == -1) return CodeMirror.Pass; 112 | var triples = getOption(conf, "triples"); 113 | 114 | var identical = pairs.charAt(pos + 1) == ch; 115 | var ranges = cm.listSelections(); 116 | var opening = pos % 2 == 0; 117 | 118 | var type; 119 | for (var i = 0; i < ranges.length; i++) { 120 | var range = ranges[i], cur = range.head, curType; 121 | var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); 122 | if (opening && !range.empty()) { 123 | curType = "surround"; 124 | } else if ((identical || !opening) && next == ch) { 125 | if (identical && stringStartsAfter(cm, cur)) 126 | curType = "both"; 127 | else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) 128 | curType = "skipThree"; 129 | else 130 | curType = "skip"; 131 | } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && 132 | cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { 133 | if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; 134 | curType = "addFour"; 135 | } else if (identical) { 136 | var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) 137 | if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; 138 | else return CodeMirror.Pass; 139 | } else if (opening) { 140 | curType = "both"; 141 | } else { 142 | return CodeMirror.Pass; 143 | } 144 | if (!type) type = curType; 145 | else if (type != curType) return CodeMirror.Pass; 146 | } 147 | 148 | var left = pos % 2 ? pairs.charAt(pos - 1) : ch; 149 | var right = pos % 2 ? ch : pairs.charAt(pos + 1); 150 | cm.operation(function() { 151 | if (type == "skip") { 152 | cm.execCommand("goCharRight"); 153 | } else if (type == "skipThree") { 154 | for (var i = 0; i < 3; i++) 155 | cm.execCommand("goCharRight"); 156 | } else if (type == "surround") { 157 | var sels = cm.getSelections(); 158 | for (var i = 0; i < sels.length; i++) 159 | sels[i] = left + sels[i] + right; 160 | cm.replaceSelections(sels, "around"); 161 | sels = cm.listSelections().slice(); 162 | for (var i = 0; i < sels.length; i++) 163 | sels[i] = contractSelection(sels[i]); 164 | cm.setSelections(sels); 165 | } else if (type == "both") { 166 | cm.replaceSelection(left + right, null); 167 | cm.triggerElectric(left + right); 168 | cm.execCommand("goCharLeft"); 169 | } else if (type == "addFour") { 170 | cm.replaceSelection(left + left + left + left, "before"); 171 | cm.execCommand("goCharRight"); 172 | } 173 | }); 174 | } 175 | 176 | function charsAround(cm, pos) { 177 | var str = cm.getRange(Pos(pos.line, pos.ch - 1), 178 | Pos(pos.line, pos.ch + 1)); 179 | return str.length == 2 ? str : null; 180 | } 181 | 182 | function stringStartsAfter(cm, pos) { 183 | var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) 184 | return /\bstring/.test(token.type) && token.start == pos.ch && 185 | (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) 186 | } 187 | }); -------------------------------------------------------------------------------- /src/js/story/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-use-before-define, no-sequences, no-undef */ 2 | import el from '../utils/element'; 3 | import commitDiff from '../utils/commitDiff'; 4 | import { formatDate } from '../utils'; 5 | import confirmPopUp from '../popups/confirmPopUp'; 6 | import { DEBUG } from '../constants'; 7 | import setAnnotationLink from './setAnnotationLink'; 8 | import renderCommits from './renderCommits'; 9 | import getTitleFromCommitMessage from './getTitleFromCommitMessage'; 10 | import renderDiffs from './renderDiffs'; 11 | import codeMirror from './codeMirror'; 12 | import renderGraph from './renderGraph'; 13 | 14 | export default function story(state, onChange) { 15 | const container = el.withFallback('.story'); 16 | const git = state.git(); 17 | let editor = null, 18 | editMode = false, 19 | currentlyEditingHash; 20 | const onSave = message => git.amend(currentlyEditingHash, { message }); 21 | const onCancel = message => { 22 | editMode = false; 23 | editor = null; 24 | if (message === '') { 25 | git.amend(currentlyEditingHash, { 26 | message: formatDate() 27 | }); 28 | } 29 | render(); 30 | }; 31 | 32 | if (!container.found()) return () => {}; 33 | 34 | const render = () => { 35 | DEBUG && console.log('story:render'); 36 | const allCommits = git.log(); 37 | const commits = Object.keys(allCommits).map(hash => ({ 38 | hash, 39 | message: allCommits[hash].message, 40 | position: allCommits[hash].meta ? parseInt(allCommits[hash].meta.position, 10) || null : null 41 | })).sort((a, b) => { 42 | if (a.position !== null && b.position !== null) { 43 | return a.position - b.position; 44 | } else if (a.position !== null && b.position === null) { 45 | return -1; 46 | } else if (a.position === null && b.position !== null) { 47 | return 1; 48 | } 49 | return a.hash - b.hash; 50 | }); 51 | const numOfCommits = commits.length; 52 | const diffs = commitDiff(numOfCommits > 0 ? git.show().files : [], git.getAll()); 53 | const renderedCommits = renderCommits(git, commits, editMode, currentlyEditingHash); 54 | 55 | container.attr('class', numOfCommits <= 1 || editMode ? 'editor-section story no-graph' : 'editor-section story'); 56 | container.content(` 57 | ${ renderedCommits !== '' ? '
' + renderedCommits + '
' : '' } 58 | ${ editMode ? '' : renderDiffs(git, diffs) } 59 |
60 | `).forEach(el => { 61 | if (el.attr('data-export') === 'checkoutLink') { 62 | el.onClick(() => { 63 | const hashToCheckout = el.attr('data-hash'); 64 | 65 | if (diffs.length > 0) { 66 | confirmPopUp( 67 | 'Checkout', 68 | 'You are about to checkout another commit. You have an unstaged changes. Are you sure?', 69 | decision => { 70 | if (decision && allCommits[hashToCheckout]) { 71 | git.checkout(hashToCheckout, true); 72 | onChange(); 73 | render(); 74 | } 75 | } 76 | ); 77 | } else { 78 | if (allCommits[hashToCheckout]) { 79 | git.checkout(hashToCheckout); 80 | onChange(); 81 | render(); 82 | } 83 | } 84 | }); 85 | } 86 | if (el.attr('data-export') === 'editMessage') { 87 | el.onClick(() => { 88 | const hash = el.attr('data-hash'); 89 | 90 | if (editMode && currentlyEditingHash === hash) { 91 | editMode = false; 92 | onCancel(); 93 | } else { 94 | editMode = true; 95 | currentlyEditingHash = el.attr('data-hash'); 96 | render(); 97 | } 98 | }); 99 | } 100 | if (el.attr('data-export') === 'deleteCommit') { 101 | el.onClick(() => { 102 | confirmPopUp( 103 | 'Deleting a commit', 104 | `Deleting "${el.attr('data-commit-message')}" commit. Are you sure?`, 105 | decision => { 106 | if (decision) { 107 | editMode = false; 108 | git.adios(el.attr('data-hash')); 109 | onChange(); 110 | } 111 | } 112 | ); 113 | }); 114 | } 115 | if (el.attr('data-export') === 'publishStatus') { 116 | el.onChange(position => { 117 | const hash = el.attr('data-hash'); 118 | 119 | git.amend(hash, { 120 | message: git.show(hash).message, 121 | meta: { position } 122 | }); 123 | render(); 124 | }); 125 | } 126 | }); 127 | 128 | const { 129 | editButton, 130 | addButton, 131 | discardButton, 132 | messageArea, 133 | confirmButton, 134 | injector 135 | } = container.namedExports(); 136 | 137 | editButton && 138 | editButton.onClick(() => { 139 | git.amend(); 140 | render(); 141 | }); 142 | 143 | addButton && 144 | addButton.onClick(() => { 145 | editMode = true; 146 | git.add(); 147 | currentlyEditingHash = git.commit(''); 148 | render(); 149 | }); 150 | 151 | discardButton && 152 | discardButton.onClick(() => { 153 | confirmPopUp('Discard changes', 'You are about to discard your current changes. Are you sure?', decision => { 154 | if (decision) { 155 | git.discard(); 156 | onChange(); 157 | } 158 | }); 159 | }); 160 | 161 | if (messageArea) { 162 | editor = codeMirror( 163 | messageArea, 164 | state.getEditorSettings(), 165 | git.show(currentlyEditingHash).message, 166 | function onSaveInEditor(message) { 167 | confirmButton.css('opacity', '0.3'); 168 | onSave(message); 169 | el.withFallback(`[data-hash="${ currentlyEditingHash }"] > .commit-message-text`) 170 | .text(getTitleFromCommitMessage(message)); 171 | }, 172 | function onChange() { 173 | confirmButton.css('opacity', '1'); 174 | numOfCommits > 1 && renderGraph(commits, git.logAsTree()); 175 | }, 176 | onCancel 177 | ); 178 | confirmButton.css('opacity', '0.3'); 179 | confirmButton.onClick(() => { 180 | onSave(editor.getValue()); 181 | editMode = false; 182 | editor = null; 183 | render(); 184 | }); 185 | injector.onChange(str => { 186 | setTimeout(() => { 187 | editor.focus(); 188 | editor.refresh(); 189 | editor.replaceSelection(str); 190 | injector.e.value = ''; 191 | }, 1); 192 | }); 193 | } 194 | 195 | numOfCommits > 1 && renderGraph(commits, git.logAsTree()); 196 | }; 197 | 198 | state.listen(event => { 199 | if (!editMode) render(); 200 | }); 201 | 202 | render(); 203 | 204 | return function addToStory({ code, list }, otherEditor) { 205 | if (editMode && editor) { 206 | otherEditor.setCursor({ line: 0, ch: 0 }); 207 | setTimeout(() => { 208 | editor.focus(); 209 | editor.refresh(); 210 | 211 | setAnnotationLink(editor, code, list, state.getActiveFile()); 212 | }, 1); 213 | } 214 | }; 215 | } 216 | -------------------------------------------------------------------------------- /src/js/state.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, no-undef */ 2 | import gitfred from 'gitfred'; 3 | import { 4 | getParam, 5 | readFromJSONFile, 6 | ensureDemoIdInPageURL, 7 | ensureUniqueFileName, 8 | jsEncode, 9 | clone 10 | } from './utils'; 11 | import { IS_PROD, DEBUG } from './constants'; 12 | import { DEFAULT_LAYOUT } from './layout'; 13 | import API from './providers/api'; 14 | import LS from './utils/localStorage'; 15 | 16 | const git = gitfred(); 17 | const LS_PROFILE_KEY = 'DEMOIT_PROFILE'; 18 | const DEFAULT_STATE = { 19 | 'editor': { 20 | 'theme': 'light', 21 | 'statusBar': true, 22 | 'layout': DEFAULT_LAYOUT 23 | }, 24 | 'dependencies': [], 25 | 'files': { 26 | 'working': [ 27 | [ 28 | 'code.js', 29 | { 30 | 'c': `document.querySelector('#output').innerHTML = 'Hello world'; 31 | 32 | console.log('Hello world');` 33 | } 34 | ] 35 | ], 36 | 'head': null, 37 | 'i': 0, 38 | 'stage': [], 39 | 'commits': {} 40 | } 41 | }; 42 | 43 | const getFirstFile = function () { 44 | const allFiles = git.getAll(); 45 | 46 | if (allFiles.length === 0) { 47 | return 'untitled.js'; 48 | } 49 | return git.getAll()[0][0]; 50 | }; 51 | const resolveActiveFile = function () { 52 | const hash = location.hash.replace(/^#/, ''); 53 | 54 | if (hash !== '' && git.get(hash)) return hash; 55 | return getFirstFile(); 56 | }; 57 | 58 | export const FILE_CHANGED = 'FILE_CHANGED'; 59 | 60 | export default async function createState(version) { 61 | let onChangeListeners = []; 62 | const onChange = event => { 63 | DEBUG && console.log('state:onChange event=' + event); 64 | onChangeListeners.forEach(c => c(event)); 65 | }; 66 | let profile = LS(LS_PROFILE_KEY); 67 | 68 | var state = window.state; 69 | var initialState; 70 | 71 | if (!state) { 72 | const stateFromURL = getParam('state'); 73 | 74 | if (stateFromURL) { 75 | try { 76 | state = await readFromJSONFile(stateFromURL); 77 | } catch (error) { 78 | console.error(`Error reading ${ stateFromURL }`); 79 | } 80 | } else { 81 | state = DEFAULT_STATE; 82 | } 83 | } 84 | 85 | state.v = version; 86 | initialState = clone(state); 87 | 88 | git.import(state.files); 89 | git.listen(event => { 90 | if (event === git.ON_COMMIT) { 91 | DEBUG && console.log('state:git:commit event=' + event); 92 | persist('git.listen'); 93 | DEBUG && console.log('state:git:checkout event=' + event); 94 | } else if (event === git.ON_CHECKOUT) { 95 | api.setActiveFileByIndex(0); 96 | persist('git.listen'); 97 | } 98 | onChange(event); 99 | }); 100 | 101 | let activeFile = resolveActiveFile(); 102 | 103 | const persist = (reason, fork = false, done = () => {}) => { 104 | DEBUG && console.log('state:persist reason=' + reason); 105 | if (api.isForkable()) { 106 | if (!fork && !api.isDemoOwner()) { return; } 107 | let diff = DeepDiff.diff(initialState, state); 108 | let stateData; 109 | 110 | initialState = clone(state); 111 | if (fork) { 112 | diff = ''; 113 | delete state.owner; 114 | stateData = state; 115 | } else if (typeof diff === 'undefined' || !diff) { 116 | // no diff and no forking so doesn't make sense to call the API 117 | return; 118 | } else { 119 | diff = jsEncode(JSON.stringify(diff)); 120 | stateData = { demoId: state.demoId, owner: state.owner }; 121 | } 122 | API.saveDemo(stateData, profile.token, diff).then(demoId => { 123 | if (demoId && demoId !== state.demoId) { 124 | state.demoId = demoId; 125 | state.owner = profile.id; 126 | ensureDemoIdInPageURL(demoId); 127 | } 128 | done(); 129 | }); 130 | } 131 | }; 132 | 133 | const api = { 134 | getDemoId() { 135 | return state.demoId; 136 | }, 137 | getActiveFile() { 138 | return activeFile; 139 | }, 140 | getActiveFileContent() { 141 | return git.get(activeFile).c; 142 | }, 143 | setActiveFile(filename) { 144 | activeFile = filename; 145 | location.hash = filename; 146 | onChange('setActiveFile'); 147 | return filename; 148 | }, 149 | setActiveFileByIndex(index) { 150 | const filename = git.getAll()[index][0]; 151 | 152 | if (filename) { 153 | this.setActiveFile(filename); 154 | onChange(FILE_CHANGED); 155 | } 156 | }, 157 | isCurrentFile(filename) { 158 | return activeFile === filename; 159 | }, 160 | isDemoOwner() { 161 | return state.owner && profile && state.owner === profile.id; 162 | }, 163 | getFiles() { 164 | return git.getAll(); 165 | }, 166 | getNumOfFiles() { 167 | return git.getAll().length; 168 | }, 169 | meta(meta) { 170 | if (meta) { 171 | const { name, description, published, storyWithCode, comments } = meta; 172 | 173 | state.name = name; 174 | state.desc = description; 175 | state.published = !!published; 176 | state.storyWithCode = !!storyWithCode; 177 | state.comments = !!comments; 178 | onChange('meta'); 179 | persist('meta'); 180 | return null; 181 | } 182 | 183 | const m = { 184 | name: state.name, 185 | description: state.desc, 186 | published: !!state.published, 187 | storyWithCode: !!state.storyWithCode, 188 | comments: !!state.comments 189 | }; 190 | 191 | if (state.demoId) m.id = state.demoId; 192 | 193 | return m; 194 | }, 195 | getDependencies() { 196 | return state.dependencies; 197 | }, 198 | setDependencies(dependencies) { 199 | state.dependencies = dependencies; 200 | persist('setDependencies'); 201 | }, 202 | getEditorSettings() { 203 | return state.editor; 204 | }, 205 | editFile(filename, updates) { 206 | git.save(filename, updates); 207 | persist('editFile'); 208 | }, 209 | renameFile(filename, newName) { 210 | if (activeFile === filename) { 211 | this.setActiveFile(newName); 212 | } 213 | git.rename(filename, newName); 214 | persist('renameFile'); 215 | }, 216 | addNewFile(filename = 'untitled.js') { 217 | filename = git.get(filename) ? ensureUniqueFileName(filename) : filename; 218 | git.save(filename, { c: '' }); 219 | this.setActiveFile(filename); 220 | persist('addNewFile'); 221 | }, 222 | deleteFile(filename) { 223 | git.del(filename); 224 | if (filename === activeFile) { 225 | this.setActiveFile(getFirstFile()); 226 | } 227 | persist('deleteFile'); 228 | }, 229 | listen(callback) { 230 | onChangeListeners.push(callback); 231 | }, 232 | removeListeners() { 233 | onChangeListeners = []; 234 | }, 235 | updateThemeAndLayout(newLayout, newTheme) { 236 | if (newLayout) { 237 | state.editor.layout = newLayout; 238 | } 239 | if (newTheme) { 240 | state.editor.theme = newTheme; 241 | }; 242 | persist('updateThemeAndLayout'); 243 | }, 244 | updateStatusBarVisibility(value) { 245 | if (state.editor.statusBar !== value) { 246 | state.editor.statusBar = value; 247 | persist('updateStatusBarVisibility'); 248 | } 249 | }, 250 | setEntryPoint(filename) { 251 | const newValue = !git.get(filename).en; 252 | 253 | git.saveAll({ en: false }); 254 | git.save(filename, { en: newValue }); 255 | persist(); 256 | }, 257 | dump() { 258 | return state; 259 | }, 260 | // forking 261 | isForkable() { 262 | return IS_PROD && api.loggedIn(); 263 | }, 264 | fork() { 265 | persist('fork', true, () => onChange('fork')); 266 | }, 267 | // profile methods 268 | loggedIn() { 269 | return profile !== null; 270 | }, 271 | getProfile() { 272 | return profile; 273 | }, 274 | getDemos() { 275 | return API.getDemos(profile.id, profile.token); 276 | }, 277 | // misc 278 | version() { 279 | return state.v; 280 | }, 281 | git() { 282 | return git; 283 | }, 284 | export() { 285 | return state; 286 | }, 287 | getStoryURL() { 288 | const meta = this.meta(); 289 | let slug = 'story'; 290 | 291 | if (meta && meta.name) { 292 | slug = meta.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); 293 | } 294 | return `/s/${ this.getDemoId() }/${ slug }`; 295 | } 296 | }; 297 | 298 | window.__state = api; 299 | 300 | return api; 301 | } 302 | --------------------------------------------------------------------------------