├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build ├── codeflask.min.js ├── codeflask.module.js └── index.html ├── code.png ├── index.d.ts ├── logo.png ├── package.json ├── rollup.config.js ├── src ├── codeflask.js ├── styles │ ├── editor.js │ ├── injector.js │ └── theme-default.js └── utils │ ├── css-supports.js │ └── html-escape.js └── test ├── e2e ├── test-app.js └── test.html ├── test-server.js └── wdio.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | package-lock.json 5 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - "lts/*" 5 | addons: 6 | chrome: stable 7 | deploy: 8 | provider: npm 9 | email: $NPM_EMAIL 10 | api_key: $NPM_TOKEN 11 | on: 12 | tags: true 13 | notifications: 14 | email: false 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Claudio Holanda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/codeflask.svg)](https://www.npmjs.com/package/codeflask) 2 | [![Build Status](https://travis-ci.org/kazzkiq/CodeFlask.svg?branch=master)](https://travis-ci.org/kazzkiq/CodeFlask) 3 | 4 |

5 |
6 | CodeFlask: A micro code-editor for awesome web pages. 7 |

8 | 9 |

10 | 11 |

12 | 13 | ## Installation 14 | 15 | You can install CodeFlask via npm: 16 | 17 | ``` 18 | npm install codeflask 19 | ``` 20 | 21 | Or use it directly in browser via cdn service: 22 | 23 | ``` 24 | https://unpkg.com/codeflask/build/codeflask.min.js 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | import CodeFlask from 'codeflask'; 31 | 32 | const flask = new CodeFlask('#my-selector', { language: 'js' }); 33 | ``` 34 | You can also pass a DOM element instead of a selector: 35 | ```js 36 | import CodeFlask from 'codeflask'; 37 | 38 | const editorElem = document.getElementById('editor'); 39 | const flask = new CodeFlask(editorElem, { language: 'js' }); 40 | ``` 41 | Usage with Shadow DOM: 42 | ```js 43 | import CodeFlask from 'codeflask'; 44 | ... 45 | const shadowElem = this.shadowRoot.querySelector('#editor'); 46 | const flask = new CodeFlask(shadowElem, { language: 'js', styleParent: this.shadowRoot }); 47 | ``` 48 | ### Listening for changes in editor 49 | 50 | ```js 51 | flask.onUpdate((code) => { 52 | // do something with code here. 53 | // this will trigger whenever the code 54 | // in the editor changes. 55 | }); 56 | ``` 57 | 58 | ### Updating the editor programatically 59 | 60 | ```js 61 | // This will also trigger .onUpdate() 62 | flask.updateCode('const my_new_code_here = "Blabla"'); 63 | ``` 64 | 65 | ### Getting the current code from editor 66 | 67 | ```js 68 | const code = flask.getCode(); 69 | ``` 70 | 71 | ### Enabling line numbers 72 | 73 | ```js 74 | import CodeFlask from 'codeflask'; 75 | 76 | const flask = new CodeFlask('#my-selector', { 77 | language: 'js', 78 | lineNumbers: true 79 | }); 80 | ``` 81 | 82 | ### Enabling rtl (right to left writing) 83 | 84 | ```js 85 | import CodeFlask from 'codeflask'; 86 | 87 | const flask = new CodeFlask('#my-selector', { 88 | language: 'js', 89 | rtl: true 90 | }); 91 | ``` 92 | 93 | ### Enabling read only mode 94 | 95 | ```js 96 | import CodeFlask from 'codeflask'; 97 | 98 | const flask = new CodeFlask('#my-selector', { 99 | language: 'js', 100 | readonly: true 101 | }); 102 | ``` 103 | 104 | ### Adding other languages support: 105 | 106 | ```js 107 | flask.addLanguage('ruby', options) 108 | ``` 109 | 110 | #### For Example to add 'Ruby' 111 | 112 | ```js 113 | import Prism from 'prismjs'; 114 | import CodeFlask from 'codeflask'; 115 | 116 | const flask = new CodeFlask('#my-selector', { 117 | language: 'ruby', 118 | readonly: true 119 | }); 120 | 121 | flask.addLanguage('ruby', Prism.languages['ruby']); 122 | ``` 123 | 124 | This API is simply a proxy to add a new language to [Prism](http://prismjs.com/) itself (the code highlighter). The `options` parameter must be the same accepted in Prism. You can read more about it [here](http://prismjs.com/extending.html#language-definitions). 125 | 126 | By default, CodeFlask supports the following languages (which are also the default supported in Prism): 127 | 128 | - Markup (HTML/XML); 129 | - CSS; 130 | - C-like; 131 | - JavaScript; 132 | 133 | ### Adding your own theme to CodeFlask 134 | 135 | By default, CodeFlask comes with a simple theme made from scratch called **[CodeNoon](https://github.com/kazzkiq/CodeFlask.js/blob/master/src/styles/theme-default.js)**. 136 | 137 | You can easily override this theme with your own by writting your own CSS and adding it to your project. If that's the case, you should also disable **CodeNoon** with the `defaultTheme` option: 138 | 139 | ```js 140 | import CodeFlask from 'codeflask'; 141 | 142 | const flask = new CodeFlask('#my-selector', { 143 | language: 'js', 144 | defaultTheme: false 145 | }); 146 | ``` 147 | 148 | # Credits & Thanks 149 | 150 | CodeFlask.js was made possible by awesome open-source projects such as [Prism.js](https://github.com/PrismJS/prism) and [Rollup](https://github.com/rollup/rollup). 151 | -------------------------------------------------------------------------------- /build/codeflask.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.CodeFlask=t()}(this,function(){"use strict";var e,t,n,a='"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',s="\n .codeflask {\n position: absolute;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .codeflask, .codeflask * {\n box-sizing: border-box;\n }\n\n .codeflask__pre {\n pointer-events: none;\n z-index: 3;\n overflow: hidden;\n }\n\n .codeflask__textarea {\n background: none;\n border: none;\n color: "+(e="caret-color",t="#000",(CSS?CSS.supports(e,t):(n=(n=e).split("-").filter(function(e){return!!e}).map(function(e){return e[0].toUpperCase()+e.substr(1)}).join(""))[0].toLowerCase()+n.substr(1)in document.body.style)?"#fff":"#ccc")+";\n z-index: 1;\n resize: none;\n font-family: "+a+";\n -webkit-appearance: pre;\n caret-color: #111;\n z-index: 2;\n width: 100%;\n height: 100%;\n }\n\n .codeflask--has-line-numbers .codeflask__textarea {\n width: calc(100% - 40px);\n }\n\n .codeflask__code {\n display: block;\n font-family: "+a+";\n overflow: hidden;\n }\n\n .codeflask__flatten {\n padding: 10px;\n font-size: 13px;\n line-height: 20px;\n white-space: pre;\n position: absolute;\n top: 0;\n left: 0;\n overflow: auto;\n margin: 0 !important;\n outline: none;\n text-align: left;\n }\n\n .codeflask--has-line-numbers .codeflask__flatten {\n width: calc(100% - 40px);\n left: 40px;\n }\n\n .codeflask__line-highlight {\n position: absolute;\n top: 10px;\n left: 0;\n width: 100%;\n height: 20px;\n background: rgba(0,0,0,0.1);\n z-index: 1;\n }\n\n .codeflask__lines {\n padding: 10px 4px;\n font-size: 12px;\n line-height: 20px;\n font-family: 'Cousine', monospace;\n position: absolute;\n left: 0;\n top: 0;\n width: 40px;\n height: 100%;\n text-align: right;\n color: #999;\n z-index: 2;\n }\n\n .codeflask__lines__line {\n display: block;\n }\n\n .codeflask.codeflask--has-line-numbers {\n padding-left: 40px;\n }\n\n .codeflask.codeflask--has-line-numbers:before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n width: 40px;\n height: 100%;\n background: #eee;\n z-index: 1;\n }\n";function i(e,t,n){var a=t||"codeflask-style",s=n||document.head;if(!e)return!1;if(document.getElementById(a))return!0;var i=document.createElement("style");return i.innerHTML=e,i.id=a,s.appendChild(i),!0}var r={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function o(e){return String(e).replace(/[&<>"'`=/]/g,function(e){return r[e]})}var l="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var c,u=(function(e){var t="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},n=function(){var e=/\blang(?:uage)?-([\w-]+)\b/i,n=0,a=t.Prism={manual:t.Prism&&t.Prism.manual,disableWorkerMessageHandler:t.Prism&&t.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof s?new s(e.type,a.util.encode(e.content),e.alias):"Array"===a.util.type(e)?e.map(a.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(x instanceof l)){if(f&&k!=t.length-1){if(h.lastIndex=v,!(L=h.exec(e)))break;for(var w=L.index+(g?L[1].length:0),C=L.index+L[0].length,S=k,T=v,A=t.length;S=(T+=t[S].length)&&(++k,v=T);if(t[k]instanceof l)continue;F=S-k,x=e.slice(v,T),L.index-=v}else{h.lastIndex=0;var L=h.exec(x),F=1}if(L){g&&(m=L[1]?L[1].length:0);C=(w=L.index+m)+(L=L[0].slice(m)).length;var E=x.slice(0,w),_=x.slice(C),N=[k,F];E&&(++k,v+=E.length,N.push(E));var j=new l(c,p?a.tokenize(L,p):L,b,L,f);if(N.push(j),_&&N.push(_),Array.prototype.splice.apply(t,N),1!=F&&a.matchGrammar(e,t,n,k,v,!0,c),r)break}else if(r)break}}}}},tokenize:function(e,t,n){var s=[e],i=t.rest;if(i){for(var r in i)t[r]=i[r];delete t.rest}return a.matchGrammar(e,s,t,0,0,!1),s},hooks:{all:{},add:function(e,t){var n=a.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=a.hooks.all[e];if(n&&n.length)for(var s,i=0;s=n[i++];)s(t)}}},s=a.Token=function(e,t,n,a,s){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!s};if(s.stringify=function(e,t,n){if("string"==typeof e)return e;if("Array"===a.util.type(e))return e.map(function(n){return s.stringify(n,t,e)}).join("");var i={type:e.type,content:s.stringify(e.content,t,n),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:n};if(e.alias){var r="Array"===a.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,r)}a.hooks.run("wrap",i);var o=Object.keys(i.attributes).map(function(e){return e+'="'+(i.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+(o?" "+o:"")+">"+i.content+""},!t.document)return t.addEventListener?(a.disableWorkerMessageHandler||t.addEventListener("message",function(e){var n=JSON.parse(e.data),s=n.language,i=n.code,r=n.immediateClose;t.postMessage(a.highlight(i,a.languages[s],s)),r&&t.close()},!1),t.Prism):t.Prism;var i=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return i&&(a.filename=i.src,a.manual||i.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(a.highlightAll):window.setTimeout(a.highlightAll,16):document.addEventListener("DOMContentLoaded",a.highlightAll))),t.Prism}();e.exports&&(e.exports=n),void 0!==l&&(l.Prism=n),n.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},n.languages.markup.tag.inside["attr-value"].inside.entity=n.languages.markup.entity,n.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),n.languages.xml=n.languages.markup,n.languages.html=n.languages.markup,n.languages.mathml=n.languages.markup,n.languages.svg=n.languages.markup,n.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},n.languages.css.atrule.inside.rest=n.languages.css,n.languages.markup&&(n.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:n.languages.css,alias:"language-css",greedy:!0}}),n.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:n.languages.css}},alias:"language-css"}},n.languages.markup.tag)),n.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},n.languages.javascript=n.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,function:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),n.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"},constant:/\b[A-Z][A-Z\d_]*\b/}),n.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${[^}]+}|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\${[^}]+}/,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}}}),n.languages.javascript["template-string"].inside.interpolation.inside.rest=n.languages.javascript,n.languages.markup&&n.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:n.languages.javascript,alias:"language-javascript",greedy:!0}}),n.languages.js=n.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(){var e={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(document.querySelectorAll("pre[data-src]")).forEach(function(t){for(var a,s=t.getAttribute("data-src"),i=t,r=/\blang(?:uage)?-([\w-]+)\b/i;i&&!r.test(i.className);)i=i.parentNode;if(i&&(a=(t.className.match(r)||[,""])[1]),!a){var o=(s.match(/\.(\w+)$/)||[,""])[1];a=e[o]||o}var l=document.createElement("code");l.className="language-"+a,t.textContent="",l.textContent="Loading…",t.appendChild(l);var c=new XMLHttpRequest;c.open("GET",s,!0),c.onreadystatechange=function(){4==c.readyState&&(c.status<400&&c.responseText?(l.textContent=c.responseText,n.highlightElement(l)):c.status>=400?l.textContent="✖ Error "+c.status+" while fetching file: "+c.statusText:l.textContent="✖ Error: File does not exist or is empty")},c.send(null)}),n.plugins.toolbar&&n.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),a=document.createElement("a");return a.textContent=t.getAttribute("data-download-link-label")||"Download",a.setAttribute("download",""),a.href=n,a}})},document.addEventListener("DOMContentLoaded",self.Prism.fileHighlight))}(c={exports:{}},c.exports),c.exports),d=function(e,t){if(!e)throw Error("CodeFlask expects a parameter which is Element or a String selector");if(!t)throw Error("CodeFlask expects an object containing options as second parameter");if(e.nodeType)this.editorRoot=e;else{var n=document.querySelector(e);n&&(this.editorRoot=n)}this.opts=t,this.startEditor()};return d.prototype.startEditor=function(){if(!i(s,null,this.opts.styleParent))throw Error("Failed to inject CodeFlask CSS.");this.createWrapper(),this.createTextarea(),this.createPre(),this.createCode(),this.runOptions(),this.listenTextarea(),this.populateDefault(),this.updateCode(this.code)},d.prototype.createWrapper=function(){this.code=this.editorRoot.innerHTML,this.editorRoot.innerHTML="",this.elWrapper=this.createElement("div",this.editorRoot),this.elWrapper.classList.add("codeflask")},d.prototype.createTextarea=function(){this.elTextarea=this.createElement("textarea",this.elWrapper),this.elTextarea.classList.add("codeflask__textarea","codeflask__flatten")},d.prototype.createPre=function(){this.elPre=this.createElement("pre",this.elWrapper),this.elPre.classList.add("codeflask__pre","codeflask__flatten")},d.prototype.createCode=function(){this.elCode=this.createElement("code",this.elPre),this.elCode.classList.add("codeflask__code","language-"+(this.opts.language||"html"))},d.prototype.createLineNumbers=function(){this.elLineNumbers=this.createElement("div",this.elWrapper),this.elLineNumbers.classList.add("codeflask__lines"),this.setLineNumber()},d.prototype.createElement=function(e,t){var n=document.createElement(e);return t.appendChild(n),n},d.prototype.runOptions=function(){this.opts.rtl=this.opts.rtl||!1,this.opts.tabSize=this.opts.tabSize||2,this.opts.enableAutocorrect=this.opts.enableAutocorrect||!1,this.opts.lineNumbers=this.opts.lineNumbers||!1,this.opts.defaultTheme=!1!==this.opts.defaultTheme,this.opts.areaId=this.opts.areaId||null,this.opts.ariaLabelledby=this.opts.ariaLabelledby||null,this.opts.readonly=this.opts.readonly||null,"boolean"!=typeof this.opts.handleTabs&&(this.opts.handleTabs=!0),"boolean"!=typeof this.opts.handleSelfClosingCharacters&&(this.opts.handleSelfClosingCharacters=!0),"boolean"!=typeof this.opts.handleNewLineIndentation&&(this.opts.handleNewLineIndentation=!0),!0===this.opts.rtl&&(this.elTextarea.setAttribute("dir","rtl"),this.elPre.setAttribute("dir","rtl")),!1===this.opts.enableAutocorrect&&(this.elTextarea.setAttribute("spellcheck","false"),this.elTextarea.setAttribute("autocapitalize","off"),this.elTextarea.setAttribute("autocomplete","off"),this.elTextarea.setAttribute("autocorrect","off")),this.opts.lineNumbers&&(this.elWrapper.classList.add("codeflask--has-line-numbers"),this.createLineNumbers()),this.opts.defaultTheme&&i("\n.codeflask {\n background: #fff;\n color: #4f559c;\n}\n\n.codeflask .token.punctuation {\n color: #4a4a4a;\n}\n\n.codeflask .token.keyword {\n color: #8500ff;\n}\n\n.codeflask .token.operator {\n color: #ff5598;\n}\n\n.codeflask .token.string {\n color: #41ad8f;\n}\n\n.codeflask .token.comment {\n color: #9badb7;\n}\n\n.codeflask .token.function {\n color: #8500ff;\n}\n\n.codeflask .token.boolean {\n color: #8500ff;\n}\n\n.codeflask .token.number {\n color: #8500ff;\n}\n\n.codeflask .token.selector {\n color: #8500ff;\n}\n\n.codeflask .token.property {\n color: #8500ff;\n}\n\n.codeflask .token.tag {\n color: #8500ff;\n}\n\n.codeflask .token.attr-value {\n color: #8500ff;\n}\n","theme-default",this.opts.styleParent),this.opts.areaId&&this.elTextarea.setAttribute("id",this.opts.areaId),this.opts.ariaLabelledby&&this.elTextarea.setAttribute("aria-labelledby",this.opts.ariaLabelledby),this.opts.readonly&&this.enableReadonlyMode()},d.prototype.updateLineNumbersCount=function(){for(var e="",t=1;t<=this.lineNumber;t++)e=e+''+t+"";this.elLineNumbers.innerHTML=e},d.prototype.listenTextarea=function(){var e=this;this.elTextarea.addEventListener("input",function(t){e.code=t.target.value,e.elCode.innerHTML=o(t.target.value),e.highlight(),setTimeout(function(){e.runUpdate(),e.setLineNumber()},1)}),this.elTextarea.addEventListener("keydown",function(t){e.handleTabs(t),e.handleSelfClosingCharacters(t),e.handleNewLineIndentation(t)}),this.elTextarea.addEventListener("scroll",function(t){e.elPre.style.transform="translate3d(-"+t.target.scrollLeft+"px, -"+t.target.scrollTop+"px, 0)",e.elLineNumbers&&(e.elLineNumbers.style.transform="translate3d(0, -"+t.target.scrollTop+"px, 0)")})},d.prototype.handleTabs=function(e){if(this.opts.handleTabs){if(9!==e.keyCode)return;e.preventDefault();var t=this.elTextarea,n=t.selectionDirection,a=t.selectionStart,s=t.selectionEnd,i=t.value,r=i.substr(0,a),o=i.substring(a,s),l=i.substring(s),c=" ".repeat(this.opts.tabSize);if(a!==s&&o.length>=c.length){var u=a-r.split("\n").pop().length,d=c.length,h=c.length;if(e.shiftKey)i.substr(u,c.length)===c?(d=-d,u>a?(o=o.substring(0,u)+o.substring(u+c.length),h=0):u===a?(d=0,h=0,o=o.substring(c.length)):(h=-h,r=r.substring(0,u)+r.substring(u+c.length))):(d=0,h=0),o=o.replace(new RegExp("\n"+c.split("").join("\\"),"g"),"\n");else r=r.substr(0,u)+c+r.substring(u,a),o=o.replace(/\n/g,"\n"+c);t.value=r+o+l,t.selectionStart=a+d,t.selectionEnd=a+o.length+h,t.selectionDirection=n}else t.value=r+c+l,t.selectionStart=a+c.length,t.selectionEnd=a+c.length;var p=t.value;this.updateCode(p),this.elTextarea.selectionEnd=s+this.opts.tabSize}},d.prototype.handleSelfClosingCharacters=function(e){if(this.opts.handleSelfClosingCharacters){var t=e.key;if(["(","[","{","<","'",'"'].includes(t)||[")","]","}",">","'",'"'].includes(t))switch(t){case"(":case")":this.closeCharacter(t);break;case"[":case"]":this.closeCharacter(t);break;case"{":case"}":this.closeCharacter(t);break;case"<":case">":case"'":case'"':this.closeCharacter(t)}}},d.prototype.setLineNumber=function(){this.lineNumber=this.code.split("\n").length,this.opts.lineNumbers&&this.updateLineNumbersCount()},d.prototype.handleNewLineIndentation=function(e){if(this.opts.handleNewLineIndentation&&13===e.keyCode){e.preventDefault();var t=this.elTextarea,n=t.selectionStart,a=t.selectionEnd,s=t.value,i=s.substr(0,n),r=s.substring(a),o=s.lastIndexOf("\n",n-1),l=o+s.slice(o+1).search(/[^ ]|$/),c=l>o?l-o:0,u=i+"\n"+" ".repeat(c)+r;t.value=u,t.selectionStart=n+c+1,t.selectionEnd=n+c+1,this.updateCode(t.value)}},d.prototype.closeCharacter=function(e){var t=this.elTextarea.selectionStart,n=this.elTextarea.selectionEnd;if(this.skipCloseChar(e)){var a=this.code.substr(n,1)===e,s=a?n+1:n,i=!a&&["'",'"'].includes(e)?e:"",r=""+this.code.substring(0,t)+i+this.code.substring(s);this.updateCode(r),this.elTextarea.selectionEnd=++this.elTextarea.selectionStart}else{var o=e;switch(e){case"(":o=String.fromCharCode(e.charCodeAt()+1);break;case"<":case"{":case"[":o=String.fromCharCode(e.charCodeAt()+2)}var l=this.code.substring(t,n),c=""+this.code.substring(0,t)+l+o+this.code.substring(n);this.updateCode(c)}this.elTextarea.selectionEnd=t},d.prototype.skipCloseChar=function(e){var t=this.elTextarea.selectionStart,n=this.elTextarea.selectionEnd,a=Math.abs(n-t)>0;return[")","}","]",">"].includes(e)||["'",'"'].includes(e)&&!a},d.prototype.updateCode=function(e){this.code=e,this.elTextarea.value=e,this.elCode.innerHTML=o(e),this.highlight(),this.setLineNumber(),setTimeout(this.runUpdate.bind(this),1)},d.prototype.updateLanguage=function(e){var t=this.opts.language;this.elCode.classList.remove("language-"+t),this.elCode.classList.add("language-"+e),this.opts.language=e,this.highlight()},d.prototype.addLanguage=function(e,t){u.languages[e]=t},d.prototype.populateDefault=function(){this.updateCode(this.code)},d.prototype.highlight=function(){u.highlightElement(this.elCode,!1)},d.prototype.onUpdate=function(e){if(e&&"[object Function]"!=={}.toString.call(e))throw Error("CodeFlask expects callback of type Function");this.updateCallBack=e},d.prototype.getCode=function(){return this.code},d.prototype.runUpdate=function(){this.updateCallBack&&this.updateCallBack(this.code)},d.prototype.enableReadonlyMode=function(){this.elTextarea.setAttribute("readonly",!0)},d.prototype.disableReadonlyMode=function(){this.elTextarea.removeAttribute("readonly")},d}); 2 | -------------------------------------------------------------------------------- /build/codeflask.module.js: -------------------------------------------------------------------------------- 1 | var BACKGROUND_COLOR="#fff",LINE_HEIGHT="20px",FONT_SIZE="13px",defaultCssTheme="\n.codeflask {\n background: "+BACKGROUND_COLOR+";\n color: #4f559c;\n}\n\n.codeflask .token.punctuation {\n color: #4a4a4a;\n}\n\n.codeflask .token.keyword {\n color: #8500ff;\n}\n\n.codeflask .token.operator {\n color: #ff5598;\n}\n\n.codeflask .token.string {\n color: #41ad8f;\n}\n\n.codeflask .token.comment {\n color: #9badb7;\n}\n\n.codeflask .token.function {\n color: #8500ff;\n}\n\n.codeflask .token.boolean {\n color: #8500ff;\n}\n\n.codeflask .token.number {\n color: #8500ff;\n}\n\n.codeflask .token.selector {\n color: #8500ff;\n}\n\n.codeflask .token.property {\n color: #8500ff;\n}\n\n.codeflask .token.tag {\n color: #8500ff;\n}\n\n.codeflask .token.attr-value {\n color: #8500ff;\n}\n";function cssSupports(e,t){return CSS?CSS.supports(e,t):toCamelCase(e)in document.body.style}function toCamelCase(e){return(e=e.split("-").filter(function(e){return!!e}).map(function(e){return e[0].toUpperCase()+e.substr(1)}).join(""))[0].toLowerCase()+e.substr(1)}var FONT_FAMILY='"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',COLOR=cssSupports("caret-color","#000")?BACKGROUND_COLOR:"#ccc",LINE_NUMBER_WIDTH="40px",editorCss="\n .codeflask {\n position: absolute;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .codeflask, .codeflask * {\n box-sizing: border-box;\n }\n\n .codeflask__pre {\n pointer-events: none;\n z-index: 3;\n overflow: hidden;\n }\n\n .codeflask__textarea {\n background: none;\n border: none;\n color: "+COLOR+";\n z-index: 1;\n resize: none;\n font-family: "+FONT_FAMILY+";\n -webkit-appearance: pre;\n caret-color: #111;\n z-index: 2;\n width: 100%;\n height: 100%;\n }\n\n .codeflask--has-line-numbers .codeflask__textarea {\n width: calc(100% - "+LINE_NUMBER_WIDTH+");\n }\n\n .codeflask__code {\n display: block;\n font-family: "+FONT_FAMILY+";\n overflow: hidden;\n }\n\n .codeflask__flatten {\n padding: 10px;\n font-size: "+FONT_SIZE+";\n line-height: "+LINE_HEIGHT+";\n white-space: pre;\n position: absolute;\n top: 0;\n left: 0;\n overflow: auto;\n margin: 0 !important;\n outline: none;\n text-align: left;\n }\n\n .codeflask--has-line-numbers .codeflask__flatten {\n width: calc(100% - "+LINE_NUMBER_WIDTH+");\n left: "+LINE_NUMBER_WIDTH+";\n }\n\n .codeflask__line-highlight {\n position: absolute;\n top: 10px;\n left: 0;\n width: 100%;\n height: "+LINE_HEIGHT+";\n background: rgba(0,0,0,0.1);\n z-index: 1;\n }\n\n .codeflask__lines {\n padding: 10px 4px;\n font-size: 12px;\n line-height: "+LINE_HEIGHT+";\n font-family: 'Cousine', monospace;\n position: absolute;\n left: 0;\n top: 0;\n width: "+LINE_NUMBER_WIDTH+";\n height: 100%;\n text-align: right;\n color: #999;\n z-index: 2;\n }\n\n .codeflask__lines__line {\n display: block;\n }\n\n .codeflask.codeflask--has-line-numbers {\n padding-left: "+LINE_NUMBER_WIDTH+";\n }\n\n .codeflask.codeflask--has-line-numbers:before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n width: "+LINE_NUMBER_WIDTH+";\n height: 100%;\n background: #eee;\n z-index: 1;\n }\n";function injectCss(e,t,n){var a=t||"codeflask-style",s=n||document.head;if(!e)return!1;if(document.getElementById(a))return!0;var o=document.createElement("style");return o.innerHTML=e,o.id=a,s.appendChild(o),!0}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(e){return String(e).replace(/[&<>"'`=/]/g,function(e){return entityMap[e]})}var commonjsGlobal="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}var prism=createCommonjsModule(function(e){var t="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},n=function(){var e=/\blang(?:uage)?-([\w-]+)\b/i,n=0,a=t.Prism={manual:t.Prism&&t.Prism.manual,disableWorkerMessageHandler:t.Prism&&t.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof s?new s(e.type,a.util.encode(e.content),e.alias):"Array"===a.util.type(e)?e.map(a.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(v instanceof l)){if(f&&y!=t.length-1){if(h.lastIndex=C,!(_=h.exec(e)))break;for(var x=_.index+(g?_[1].length:0),w=_.index+_[0].length,F=y,T=C,L=t.length;F=(T+=t[F].length)&&(++y,C=T);if(t[y]instanceof l)continue;E=F-y,v=e.slice(C,T),_.index-=C}else{h.lastIndex=0;var _=h.exec(v),E=1}if(_){g&&(m=_[1]?_[1].length:0);w=(x=_.index+m)+(_=_[0].slice(m)).length;var N=v.slice(0,x),S=v.slice(w),A=[y,E];N&&(++y,C+=N.length,A.push(N));var I=new l(c,p?a.tokenize(_,p):_,b,_,f);if(A.push(I),S&&A.push(S),Array.prototype.splice.apply(t,A),1!=E&&a.matchGrammar(e,t,n,y,C,!0,c),i)break}else if(i)break}}}}},tokenize:function(e,t,n){var s=[e],o=t.rest;if(o){for(var i in o)t[i]=o[i];delete t.rest}return a.matchGrammar(e,s,t,0,0,!1),s},hooks:{all:{},add:function(e,t){var n=a.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=a.hooks.all[e];if(n&&n.length)for(var s,o=0;s=n[o++];)s(t)}}},s=a.Token=function(e,t,n,a,s){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length,this.greedy=!!s};if(s.stringify=function(e,t,n){if("string"==typeof e)return e;if("Array"===a.util.type(e))return e.map(function(n){return s.stringify(n,t,e)}).join("");var o={type:e.type,content:s.stringify(e.content,t,n),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:n};if(e.alias){var i="Array"===a.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(o.classes,i)}a.hooks.run("wrap",o);var r=Object.keys(o.attributes).map(function(e){return e+'="'+(o.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+o.tag+' class="'+o.classes.join(" ")+'"'+(r?" "+r:"")+">"+o.content+""},!t.document)return t.addEventListener?(a.disableWorkerMessageHandler||t.addEventListener("message",function(e){var n=JSON.parse(e.data),s=n.language,o=n.code,i=n.immediateClose;t.postMessage(a.highlight(o,a.languages[s],s)),i&&t.close()},!1),t.Prism):t.Prism;var o=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return o&&(a.filename=o.src,a.manual||o.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(a.highlightAll):window.setTimeout(a.highlightAll,16):document.addEventListener("DOMContentLoaded",a.highlightAll))),t.Prism}();e.exports&&(e.exports=n),void 0!==commonjsGlobal&&(commonjsGlobal.Prism=n),n.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/(^|[^\\])["']/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},n.languages.markup.tag.inside["attr-value"].inside.entity=n.languages.markup.entity,n.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),n.languages.xml=n.languages.markup,n.languages.html=n.languages.markup,n.languages.mathml=n.languages.markup,n.languages.svg=n.languages.markup,n.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(?:;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^{}\s][^{};]*?(?=\s*\{)/,string:{pattern:/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/\B!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},n.languages.css.atrule.inside.rest=n.languages.css,n.languages.markup&&(n.languages.insertBefore("markup","tag",{style:{pattern:/()[\s\S]*?(?=<\/style>)/i,lookbehind:!0,inside:n.languages.css,alias:"language-css",greedy:!0}}),n.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:n.languages.css}},alias:"language-css"}},n.languages.markup.tag)),n.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},n.languages.javascript=n.languages.extend("clike",{keyword:/\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b(?:0[xX][\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|NaN|Infinity)\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,function:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*\()/i,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),n.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[[^\]\r\n]+]|\\.|[^/\\\[\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=\s*(?:function\b|(?:\([^()]*\)|[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/i,alias:"function"},constant:/\b[A-Z][A-Z\d_]*\b/}),n.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${[^}]+}|[^\\`])*`/,greedy:!0,inside:{interpolation:{pattern:/\${[^}]+}/,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}}}),n.languages.javascript["template-string"].inside.interpolation.inside.rest=n.languages.javascript,n.languages.markup&&n.languages.insertBefore("markup","tag",{script:{pattern:/()[\s\S]*?(?=<\/script>)/i,lookbehind:!0,inside:n.languages.javascript,alias:"language-javascript",greedy:!0}}),n.languages.js=n.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(){var e={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(document.querySelectorAll("pre[data-src]")).forEach(function(t){for(var a,s=t.getAttribute("data-src"),o=t,i=/\blang(?:uage)?-([\w-]+)\b/i;o&&!i.test(o.className);)o=o.parentNode;if(o&&(a=(t.className.match(i)||[,""])[1]),!a){var r=(s.match(/\.(\w+)$/)||[,""])[1];a=e[r]||r}var l=document.createElement("code");l.className="language-"+a,t.textContent="",l.textContent="Loading…",t.appendChild(l);var c=new XMLHttpRequest;c.open("GET",s,!0),c.onreadystatechange=function(){4==c.readyState&&(c.status<400&&c.responseText?(l.textContent=c.responseText,n.highlightElement(l)):c.status>=400?l.textContent="✖ Error "+c.status+" while fetching file: "+c.statusText:l.textContent="✖ Error: File does not exist or is empty")},c.send(null)}),n.plugins.toolbar&&n.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),a=document.createElement("a");return a.textContent=t.getAttribute("data-download-link-label")||"Download",a.setAttribute("download",""),a.href=n,a}})},document.addEventListener("DOMContentLoaded",self.Prism.fileHighlight))}),CodeFlask=function(e,t){if(!e)throw Error("CodeFlask expects a parameter which is Element or a String selector");if(!t)throw Error("CodeFlask expects an object containing options as second parameter");if(e.nodeType)this.editorRoot=e;else{var n=document.querySelector(e);n&&(this.editorRoot=n)}this.opts=t,this.startEditor()};CodeFlask.prototype.startEditor=function(){if(!injectCss(editorCss,null,this.opts.styleParent))throw Error("Failed to inject CodeFlask CSS.");this.createWrapper(),this.createTextarea(),this.createPre(),this.createCode(),this.runOptions(),this.listenTextarea(),this.populateDefault(),this.updateCode(this.code)},CodeFlask.prototype.createWrapper=function(){this.code=this.editorRoot.innerHTML,this.editorRoot.innerHTML="",this.elWrapper=this.createElement("div",this.editorRoot),this.elWrapper.classList.add("codeflask")},CodeFlask.prototype.createTextarea=function(){this.elTextarea=this.createElement("textarea",this.elWrapper),this.elTextarea.classList.add("codeflask__textarea","codeflask__flatten")},CodeFlask.prototype.createPre=function(){this.elPre=this.createElement("pre",this.elWrapper),this.elPre.classList.add("codeflask__pre","codeflask__flatten")},CodeFlask.prototype.createCode=function(){this.elCode=this.createElement("code",this.elPre),this.elCode.classList.add("codeflask__code","language-"+(this.opts.language||"html"))},CodeFlask.prototype.createLineNumbers=function(){this.elLineNumbers=this.createElement("div",this.elWrapper),this.elLineNumbers.classList.add("codeflask__lines"),this.setLineNumber()},CodeFlask.prototype.createElement=function(e,t){var n=document.createElement(e);return t.appendChild(n),n},CodeFlask.prototype.runOptions=function(){this.opts.rtl=this.opts.rtl||!1,this.opts.tabSize=this.opts.tabSize||2,this.opts.enableAutocorrect=this.opts.enableAutocorrect||!1,this.opts.lineNumbers=this.opts.lineNumbers||!1,this.opts.defaultTheme=!1!==this.opts.defaultTheme,this.opts.areaId=this.opts.areaId||null,this.opts.ariaLabelledby=this.opts.ariaLabelledby||null,this.opts.readonly=this.opts.readonly||null,"boolean"!=typeof this.opts.handleTabs&&(this.opts.handleTabs=!0),"boolean"!=typeof this.opts.handleSelfClosingCharacters&&(this.opts.handleSelfClosingCharacters=!0),"boolean"!=typeof this.opts.handleNewLineIndentation&&(this.opts.handleNewLineIndentation=!0),!0===this.opts.rtl&&(this.elTextarea.setAttribute("dir","rtl"),this.elPre.setAttribute("dir","rtl")),!1===this.opts.enableAutocorrect&&(this.elTextarea.setAttribute("spellcheck","false"),this.elTextarea.setAttribute("autocapitalize","off"),this.elTextarea.setAttribute("autocomplete","off"),this.elTextarea.setAttribute("autocorrect","off")),this.opts.lineNumbers&&(this.elWrapper.classList.add("codeflask--has-line-numbers"),this.createLineNumbers()),this.opts.defaultTheme&&injectCss(defaultCssTheme,"theme-default",this.opts.styleParent),this.opts.areaId&&this.elTextarea.setAttribute("id",this.opts.areaId),this.opts.ariaLabelledby&&this.elTextarea.setAttribute("aria-labelledby",this.opts.ariaLabelledby),this.opts.readonly&&this.enableReadonlyMode()},CodeFlask.prototype.updateLineNumbersCount=function(){for(var e="",t=1;t<=this.lineNumber;t++)e=e+''+t+"";this.elLineNumbers.innerHTML=e},CodeFlask.prototype.listenTextarea=function(){var e=this;this.elTextarea.addEventListener("input",function(t){e.code=t.target.value,e.elCode.innerHTML=escapeHtml(t.target.value),e.highlight(),setTimeout(function(){e.runUpdate(),e.setLineNumber()},1)}),this.elTextarea.addEventListener("keydown",function(t){e.handleTabs(t),e.handleSelfClosingCharacters(t),e.handleNewLineIndentation(t)}),this.elTextarea.addEventListener("scroll",function(t){e.elPre.style.transform="translate3d(-"+t.target.scrollLeft+"px, -"+t.target.scrollTop+"px, 0)",e.elLineNumbers&&(e.elLineNumbers.style.transform="translate3d(0, -"+t.target.scrollTop+"px, 0)")})},CodeFlask.prototype.handleTabs=function(e){if(this.opts.handleTabs){if(9!==e.keyCode)return;e.preventDefault();var t=this.elTextarea,n=t.selectionDirection,a=t.selectionStart,s=t.selectionEnd,o=t.value,i=o.substr(0,a),r=o.substring(a,s),l=o.substring(s),c=" ".repeat(this.opts.tabSize);if(a!==s&&r.length>=c.length){var u=a-i.split("\n").pop().length,d=c.length,h=c.length;if(e.shiftKey)o.substr(u,c.length)===c?(d=-d,u>a?(r=r.substring(0,u)+r.substring(u+c.length),h=0):u===a?(d=0,h=0,r=r.substring(c.length)):(h=-h,i=i.substring(0,u)+i.substring(u+c.length))):(d=0,h=0),r=r.replace(new RegExp("\n"+c.split("").join("\\"),"g"),"\n");else i=i.substr(0,u)+c+i.substring(u,a),r=r.replace(/\n/g,"\n"+c);t.value=i+r+l,t.selectionStart=a+d,t.selectionEnd=a+r.length+h,t.selectionDirection=n}else t.value=i+c+l,t.selectionStart=a+c.length,t.selectionEnd=a+c.length;var p=t.value;this.updateCode(p),this.elTextarea.selectionEnd=s+this.opts.tabSize}},CodeFlask.prototype.handleSelfClosingCharacters=function(e){if(this.opts.handleSelfClosingCharacters){var t=e.key;if(["(","[","{","<","'",'"'].includes(t)||[")","]","}",">","'",'"'].includes(t))switch(t){case"(":case")":this.closeCharacter(t);break;case"[":case"]":this.closeCharacter(t);break;case"{":case"}":this.closeCharacter(t);break;case"<":case">":case"'":case'"':this.closeCharacter(t)}}},CodeFlask.prototype.setLineNumber=function(){this.lineNumber=this.code.split("\n").length,this.opts.lineNumbers&&this.updateLineNumbersCount()},CodeFlask.prototype.handleNewLineIndentation=function(e){if(this.opts.handleNewLineIndentation&&13===e.keyCode){e.preventDefault();var t=this.elTextarea,n=t.selectionStart,a=t.selectionEnd,s=t.value,o=s.substr(0,n),i=s.substring(a),r=s.lastIndexOf("\n",n-1),l=r+s.slice(r+1).search(/[^ ]|$/),c=l>r?l-r:0,u=o+"\n"+" ".repeat(c)+i;t.value=u,t.selectionStart=n+c+1,t.selectionEnd=n+c+1,this.updateCode(t.value)}},CodeFlask.prototype.closeCharacter=function(e){var t=this.elTextarea.selectionStart,n=this.elTextarea.selectionEnd;if(this.skipCloseChar(e)){var a=this.code.substr(n,1)===e,s=a?n+1:n,o=!a&&["'",'"'].includes(e)?e:"",i=""+this.code.substring(0,t)+o+this.code.substring(s);this.updateCode(i),this.elTextarea.selectionEnd=++this.elTextarea.selectionStart}else{var r=e;switch(e){case"(":r=String.fromCharCode(e.charCodeAt()+1);break;case"<":case"{":case"[":r=String.fromCharCode(e.charCodeAt()+2)}var l=this.code.substring(t,n),c=""+this.code.substring(0,t)+l+r+this.code.substring(n);this.updateCode(c)}this.elTextarea.selectionEnd=t},CodeFlask.prototype.skipCloseChar=function(e){var t=this.elTextarea.selectionStart,n=this.elTextarea.selectionEnd,a=Math.abs(n-t)>0;return[")","}","]",">"].includes(e)||["'",'"'].includes(e)&&!a},CodeFlask.prototype.updateCode=function(e){this.code=e,this.elTextarea.value=e,this.elCode.innerHTML=escapeHtml(e),this.highlight(),this.setLineNumber(),setTimeout(this.runUpdate.bind(this),1)},CodeFlask.prototype.updateLanguage=function(e){var t=this.opts.language;this.elCode.classList.remove("language-"+t),this.elCode.classList.add("language-"+e),this.opts.language=e,this.highlight()},CodeFlask.prototype.addLanguage=function(e,t){prism.languages[e]=t},CodeFlask.prototype.populateDefault=function(){this.updateCode(this.code)},CodeFlask.prototype.highlight=function(){prism.highlightElement(this.elCode,!1)},CodeFlask.prototype.onUpdate=function(e){if(e&&"[object Function]"!=={}.toString.call(e))throw Error("CodeFlask expects callback of type Function");this.updateCallBack=e},CodeFlask.prototype.getCode=function(){return this.code},CodeFlask.prototype.runUpdate=function(){this.updateCallBack&&this.updateCallBack(this.code)},CodeFlask.prototype.enableReadonlyMode=function(){this.elTextarea.setAttribute("readonly",!0)},CodeFlask.prototype.disableReadonlyMode=function(){this.elTextarea.removeAttribute("readonly")};export default CodeFlask; 2 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CodeFlask Test Page 8 | 9 | 19 | 20 | 21 |

Testing below:

22 | 23 |
// Feel free to play with the code. :) 24 | 25 | function Hey(phrase) { 26 | phrase = phrase || "Hey"; 27 | return console.log(phrase); 28 | } 29 | 30 | window.onLoad = function() { 31 | Hey(); 32 | Hey("Jude"); 33 | Hey("Don't make it bad."); 34 | }
35 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazzkiq/CodeFlask/2805db74727a2741824e19e65d68b9d4eea7655f/code.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as prism from 'prismjs'; 2 | 3 | export as namespace codeflask 4 | 5 | export type LanguageDefinition = { 6 | [token: string]: prism.LanguageDefinition | RegExp 7 | } 8 | 9 | export interface CodeFlaskOptions { 10 | language?: string 11 | rtl?: boolean 12 | tabSize?: number 13 | enableAutocorrect?: boolean 14 | lineNumbers?: boolean 15 | defaultTheme?: boolean 16 | areaId?: string 17 | ariaLabelledby?: string 18 | readonly?: boolean 19 | } 20 | 21 | export default class CodeFlask { 22 | constructor(selectorOrElement: Element | string, opts: CodeFlaskOptions) 23 | 24 | updateCode(newCode: string): void 25 | updateLanguage(newLanguage: string): void 26 | addLanguage(name: string, options: LanguageDefinition): void 27 | 28 | getCode(): string 29 | onUpdate(callback: (code: string) => void): void 30 | 31 | disableReadonlyMode(): void 32 | enableReadonlyMode(): void 33 | } 34 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazzkiq/CodeFlask/2805db74727a2741824e19e65d68b9d4eea7655f/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeflask", 3 | "version": "1.4.1", 4 | "description": "A micro code-editor for awesome web pages", 5 | "main": "build/codeflask.min.js", 6 | "module": "build/codeflask.module.js", 7 | "types": "index.d.ts", 8 | "files": [ 9 | "build/codeflask.min.js", 10 | "build/codeflask.module.js", 11 | "index.d.ts", 12 | "README.md", 13 | "LICENSE" 14 | ], 15 | "scripts": { 16 | "build": "rollup -c", 17 | "dev": "serve build & rollup -c -w", 18 | "start": "serve public", 19 | "pretest": "npm run build", 20 | "test": "wdio test/wdio.conf.js", 21 | "prepublishOnly": "npm install && npm run build" 22 | }, 23 | "dependencies": { 24 | "@types/prismjs": "^1.9.1", 25 | "prismjs": "^1.14.0" 26 | }, 27 | "devDependencies": { 28 | "@wdio/cli": "^6.1.15", 29 | "@wdio/local-runner": "^6.1.14", 30 | "@wdio/mocha-framework": "^6.1.14", 31 | "@wdio/spec-reporter": "^6.1.14", 32 | "@wdio/sync": "^6.1.14", 33 | "chai": "^4.1.2", 34 | "chromedriver": "^83.0.0", 35 | "micro": "^9.3.0", 36 | "mocha": "^5.1.1", 37 | "rollup": "^0.58.1", 38 | "rollup-plugin-buble": "^0.19.2", 39 | "rollup-plugin-commonjs": "^9.1.0", 40 | "rollup-plugin-node-resolve": "^3.0.3", 41 | "rollup-plugin-uglify": "^3.0.0", 42 | "serve": "^7.0.0", 43 | "wdio-chromedriver-service": "^6.0.3" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/kazzkiq/CodeFlask.git" 48 | }, 49 | "keywords": [ 50 | "code-editor", 51 | "code", 52 | "syntax-highlight", 53 | "highlight" 54 | ], 55 | "author": "Claudio Holanda", 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/kazzkiq/CodeFlask/issues" 59 | }, 60 | "homepage": "https://kazzkiq.github.io/CodeFlask/" 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import buble from 'rollup-plugin-buble'; 4 | import uglify from 'rollup-plugin-uglify'; 5 | 6 | const production = !process.env.ROLLUP_WATCH; 7 | 8 | export default { 9 | input: 'src/codeflask.js', 10 | output: [ 11 | { 12 | sourcemap: false, 13 | format: 'umd', 14 | name: 'CodeFlask', 15 | file: 'build/codeflask.min.js' 16 | }, 17 | { 18 | sourcemap: false, 19 | format: 'es', 20 | name: 'CodeFlask', 21 | file: 'build/codeflask.module.js' 22 | }, 23 | ], 24 | plugins: [ 25 | // If you have external dependencies installed from 26 | // npm, you'll most likely need these plugins. In 27 | // some cases you'll need additional configuration — 28 | // consult the documentation for details: 29 | // https://github.com/rollup/rollup-plugin-commonjs 30 | resolve(), 31 | commonjs(), 32 | 33 | // If we're building for production (npm run build 34 | // instead of npm run dev), transpile and minify 35 | production && buble({ exclude: 'node_modules/**' }), 36 | production && uglify() 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /src/codeflask.js: -------------------------------------------------------------------------------- 1 | import { editorCss } from './styles/editor' 2 | import { injectCss } from './styles/injector' 3 | import { defaultCssTheme } from './styles/theme-default' 4 | import { escapeHtml } from './utils/html-escape' 5 | import Prism from 'prismjs' 6 | 7 | export default class CodeFlask { 8 | constructor (selectorOrElement, opts) { 9 | if (!selectorOrElement) { 10 | // If no selector or element is passed to CodeFlask, 11 | // stop execution and throw error. 12 | throw Error('CodeFlask expects a parameter which is Element or a String selector') 13 | } 14 | 15 | if (!opts) { 16 | // If no selector or element is passed to CodeFlask, 17 | // stop execution and throw error. 18 | throw Error('CodeFlask expects an object containing options as second parameter') 19 | } 20 | 21 | if (selectorOrElement.nodeType) { 22 | // If it is an element, assign it directly 23 | this.editorRoot = selectorOrElement 24 | } else { 25 | // If it is a selector, tries to find element 26 | const editorRoot = document.querySelector(selectorOrElement) 27 | 28 | // If an element is found using this selector, 29 | // assign this element as the root element 30 | if (editorRoot) { 31 | this.editorRoot = editorRoot 32 | } 33 | } 34 | 35 | this.opts = opts 36 | this.startEditor() 37 | } 38 | 39 | startEditor () { 40 | const isCSSInjected = injectCss(editorCss, null, this.opts.styleParent) 41 | 42 | if (!isCSSInjected) { 43 | throw Error('Failed to inject CodeFlask CSS.') 44 | } 45 | 46 | // The order matters (pre > code). Don't change it 47 | // or things are going to break. 48 | this.createWrapper() 49 | this.createTextarea() 50 | this.createPre() 51 | this.createCode() 52 | 53 | this.runOptions() 54 | this.listenTextarea() 55 | this.populateDefault() 56 | this.updateCode(this.code) 57 | } 58 | 59 | createWrapper () { 60 | this.code = this.editorRoot.innerHTML 61 | this.editorRoot.innerHTML = '' 62 | this.elWrapper = this.createElement('div', this.editorRoot) 63 | this.elWrapper.classList.add('codeflask') 64 | } 65 | 66 | createTextarea () { 67 | this.elTextarea = this.createElement('textarea', this.elWrapper) 68 | this.elTextarea.classList.add('codeflask__textarea', 'codeflask__flatten') 69 | } 70 | 71 | createPre () { 72 | this.elPre = this.createElement('pre', this.elWrapper) 73 | this.elPre.classList.add('codeflask__pre', 'codeflask__flatten') 74 | } 75 | 76 | createCode () { 77 | this.elCode = this.createElement('code', this.elPre) 78 | this.elCode.classList.add('codeflask__code', `language-${this.opts.language || 'html'}`) 79 | } 80 | 81 | createLineNumbers () { 82 | this.elLineNumbers = this.createElement('div', this.elWrapper) 83 | this.elLineNumbers.classList.add('codeflask__lines') 84 | this.setLineNumber() 85 | } 86 | 87 | createElement (elementTag, whereToAppend) { 88 | const element = document.createElement(elementTag) 89 | whereToAppend.appendChild(element) 90 | 91 | return element 92 | } 93 | 94 | runOptions () { 95 | this.opts.rtl = this.opts.rtl || false 96 | this.opts.tabSize = this.opts.tabSize || 2 97 | this.opts.enableAutocorrect = this.opts.enableAutocorrect || false 98 | this.opts.lineNumbers = this.opts.lineNumbers || false 99 | this.opts.defaultTheme = this.opts.defaultTheme !== false 100 | this.opts.areaId = this.opts.areaId || null 101 | this.opts.ariaLabelledby = this.opts.ariaLabelledby || null 102 | this.opts.readonly = this.opts.readonly || null 103 | 104 | // if handleTabs is not either true or false, make it true by default 105 | if (typeof this.opts.handleTabs !== 'boolean') { 106 | this.opts.handleTabs = true 107 | } 108 | // if handleTabs is not either true or false, make it true by default 109 | if (typeof this.opts.handleSelfClosingCharacters !== 'boolean') { 110 | this.opts.handleSelfClosingCharacters = true 111 | } 112 | // if handleTabs is not either true or false, make it true by default 113 | if (typeof this.opts.handleNewLineIndentation !== 'boolean') { 114 | this.opts.handleNewLineIndentation = true 115 | } 116 | 117 | if (this.opts.rtl === true) { 118 | this.elTextarea.setAttribute('dir', 'rtl') 119 | this.elPre.setAttribute('dir', 'rtl') 120 | } 121 | 122 | if (this.opts.enableAutocorrect === false) { 123 | this.elTextarea.setAttribute('spellcheck', 'false') 124 | this.elTextarea.setAttribute('autocapitalize', 'off') 125 | this.elTextarea.setAttribute('autocomplete', 'off') 126 | this.elTextarea.setAttribute('autocorrect', 'off') 127 | } 128 | 129 | if (this.opts.lineNumbers) { 130 | this.elWrapper.classList.add('codeflask--has-line-numbers') 131 | this.createLineNumbers() 132 | } 133 | 134 | if (this.opts.defaultTheme) { 135 | injectCss(defaultCssTheme, 'theme-default', this.opts.styleParent) 136 | } 137 | 138 | if (this.opts.areaId) { 139 | this.elTextarea.setAttribute('id', this.opts.areaId) 140 | } 141 | 142 | if (this.opts.ariaLabelledby) { 143 | this.elTextarea.setAttribute('aria-labelledby', this.opts.ariaLabelledby) 144 | } 145 | 146 | if (this.opts.readonly) { 147 | this.enableReadonlyMode() 148 | } 149 | } 150 | 151 | updateLineNumbersCount () { 152 | let numberList = '' 153 | 154 | for (let i = 1; i <= this.lineNumber; i++) { 155 | numberList = numberList + `${i}` 156 | } 157 | 158 | this.elLineNumbers.innerHTML = numberList 159 | } 160 | 161 | listenTextarea () { 162 | this.elTextarea.addEventListener('input', (e) => { 163 | this.code = e.target.value 164 | this.elCode.innerHTML = escapeHtml(e.target.value) 165 | this.highlight() 166 | setTimeout(() => { 167 | this.runUpdate() 168 | this.setLineNumber() 169 | }, 1) 170 | }) 171 | 172 | this.elTextarea.addEventListener('keydown', (e) => { 173 | if (this.opts.readonly) { 174 | return; 175 | } 176 | this.handleTabs(e) 177 | this.handleSelfClosingCharacters(e) 178 | this.handleNewLineIndentation(e) 179 | }) 180 | 181 | this.elTextarea.addEventListener('scroll', (e) => { 182 | this.elPre.style.transform = `translate3d(-${e.target.scrollLeft}px, -${e.target.scrollTop}px, 0)` 183 | if (this.elLineNumbers) { 184 | this.elLineNumbers.style.transform = `translate3d(0, -${e.target.scrollTop}px, 0)` 185 | } 186 | }) 187 | } 188 | 189 | handleTabs (e) { 190 | if (this.opts.handleTabs) { 191 | if (e.keyCode !== 9) { 192 | return 193 | } 194 | e.preventDefault() 195 | 196 | var input = this.elTextarea 197 | var selectionDir = input.selectionDirection 198 | var selStartPos = input.selectionStart 199 | var selEndPos = input.selectionEnd 200 | var inputVal = input.value 201 | 202 | var beforeSelection = inputVal.substr(0, selStartPos) 203 | var selectionVal = inputVal.substring(selStartPos, selEndPos) 204 | var afterSelection = inputVal.substring(selEndPos) 205 | const indent = ' '.repeat(this.opts.tabSize) 206 | 207 | if (selStartPos !== selEndPos && selectionVal.length >= indent.length) { 208 | var currentLineStart = selStartPos - beforeSelection.split('\n').pop().length 209 | var startIndentLen = indent.length 210 | var endIndentLen = indent.length 211 | 212 | // Unindent 213 | if (e.shiftKey) { 214 | var currentLineStartStr = inputVal.substr(currentLineStart, indent.length) 215 | // Line start whit indent 216 | if (currentLineStartStr === indent) { 217 | startIndentLen = -startIndentLen 218 | 219 | if (currentLineStart > selStartPos) { 220 | // Indent is in selection 221 | selectionVal = selectionVal.substring(0, currentLineStart) + selectionVal.substring(currentLineStart + indent.length) 222 | endIndentLen = 0 223 | } else if (currentLineStart === selStartPos) { 224 | // Indent is in start of selection 225 | startIndentLen = 0 226 | endIndentLen = 0 227 | selectionVal = selectionVal.substring(indent.length) 228 | } else { 229 | // Indent is before selection 230 | endIndentLen = -endIndentLen 231 | beforeSelection = beforeSelection.substring(0, currentLineStart) + beforeSelection.substring(currentLineStart + indent.length) 232 | } 233 | } else { 234 | startIndentLen = 0 235 | endIndentLen = 0 236 | } 237 | 238 | selectionVal = selectionVal.replace(new RegExp('\n' + indent.split('').join('\\'), 'g'), '\n') 239 | } else { 240 | // Indent 241 | beforeSelection = beforeSelection.substr(0, currentLineStart) + indent + beforeSelection.substring(currentLineStart, selStartPos) 242 | selectionVal = selectionVal.replace(/\n/g, '\n' + indent) 243 | } 244 | 245 | // Set new indented value 246 | input.value = beforeSelection + selectionVal + afterSelection 247 | 248 | input.selectionStart = selStartPos + startIndentLen 249 | input.selectionEnd = selStartPos + selectionVal.length + endIndentLen 250 | input.selectionDirection = selectionDir 251 | } else { 252 | input.value = beforeSelection + indent + afterSelection 253 | input.selectionStart = selStartPos + indent.length 254 | input.selectionEnd = selStartPos + indent.length 255 | } 256 | 257 | var newCode = input.value 258 | this.updateCode(newCode) 259 | this.elTextarea.selectionEnd = selEndPos + this.opts.tabSize 260 | } 261 | } 262 | 263 | handleSelfClosingCharacters (e) { 264 | if (!this.opts.handleSelfClosingCharacters) return 265 | const openChars = ['(', '[', '{', '<', '\'', '"'] 266 | const closeChars = [')', ']', '}', '>', '\'', '"'] 267 | const key = e.key 268 | 269 | if (!openChars.includes(key) && !closeChars.includes(key)) { 270 | return 271 | } 272 | 273 | switch (key) { 274 | case '(': 275 | case ')': 276 | this.closeCharacter(key) 277 | break 278 | 279 | case '[': 280 | case ']': 281 | this.closeCharacter(key) 282 | break 283 | 284 | case '{': 285 | case '}': 286 | this.closeCharacter(key) 287 | break 288 | 289 | case '<': 290 | case '>': 291 | this.closeCharacter(key) 292 | break 293 | 294 | case '\'': 295 | this.closeCharacter(key) 296 | break 297 | 298 | case '"': 299 | this.closeCharacter(key) 300 | break 301 | } 302 | } 303 | 304 | setLineNumber () { 305 | this.lineNumber = this.code.split('\n').length 306 | 307 | if (this.opts.lineNumbers) { 308 | this.updateLineNumbersCount() 309 | } 310 | } 311 | 312 | handleNewLineIndentation (e) { 313 | if (!this.opts.handleNewLineIndentation) return 314 | if (e.keyCode !== 13) { 315 | return 316 | } 317 | 318 | e.preventDefault() 319 | var input = this.elTextarea 320 | var selStartPos = input.selectionStart 321 | var selEndPos = input.selectionEnd 322 | var inputVal = input.value 323 | 324 | var beforeSelection = inputVal.substr(0, selStartPos) 325 | var afterSelection = inputVal.substring(selEndPos) 326 | 327 | var lineStart = inputVal.lastIndexOf('\n', selStartPos - 1) 328 | var spaceLast = lineStart + inputVal.slice(lineStart + 1).search(/[^ ]|$/) 329 | var indent = (spaceLast > lineStart) ? (spaceLast - lineStart) : 0 330 | var newCode = beforeSelection + '\n' + ' '.repeat(indent) + afterSelection 331 | 332 | input.value = newCode 333 | input.selectionStart = selStartPos + indent + 1 334 | input.selectionEnd = selStartPos + indent + 1 335 | 336 | this.updateCode(input.value) 337 | } 338 | 339 | closeCharacter (char) { 340 | const selectionStart = this.elTextarea.selectionStart 341 | const selectionEnd = this.elTextarea.selectionEnd 342 | 343 | if (!this.skipCloseChar(char)) { 344 | let closeChar = char 345 | switch (char) { 346 | case '(': 347 | closeChar = String.fromCharCode(char.charCodeAt() + 1) 348 | break 349 | case '<': 350 | case '{': 351 | case '[': 352 | closeChar = String.fromCharCode(char.charCodeAt() + 2) 353 | break 354 | } 355 | const selectionText = this.code.substring(selectionStart, selectionEnd) 356 | const newCode = `${this.code.substring(0, selectionStart)}${selectionText}${closeChar}${this.code.substring(selectionEnd)}` 357 | this.updateCode(newCode) 358 | } else { 359 | const skipChar = this.code.substr(selectionEnd, 1) === char 360 | const newSelectionEnd = skipChar ? selectionEnd + 1 : selectionEnd 361 | const closeChar = !skipChar && ['\'', '"'].includes(char) ? char : '' 362 | const newCode = `${this.code.substring(0, selectionStart)}${closeChar}${this.code.substring(newSelectionEnd)}` 363 | this.updateCode(newCode) 364 | this.elTextarea.selectionEnd = ++this.elTextarea.selectionStart 365 | } 366 | 367 | this.elTextarea.selectionEnd = selectionStart 368 | } 369 | 370 | skipCloseChar (char) { 371 | const selectionStart = this.elTextarea.selectionStart 372 | const selectionEnd = this.elTextarea.selectionEnd 373 | const hasSelection = Math.abs(selectionEnd - selectionStart) > 0 374 | return [')', '}', ']', '>'].includes(char) || (['\'', '"'].includes(char) && !hasSelection) 375 | } 376 | 377 | updateCode (newCode) { 378 | this.code = newCode 379 | this.elTextarea.value = newCode 380 | this.elCode.innerHTML = escapeHtml(newCode) 381 | this.highlight() 382 | this.setLineNumber() 383 | setTimeout(this.runUpdate.bind(this), 1) 384 | } 385 | 386 | updateLanguage (newLanguage) { 387 | const oldLanguage = this.opts.language 388 | this.elCode.classList.remove(`language-${oldLanguage}`) 389 | this.elCode.classList.add(`language-${newLanguage}`) 390 | this.opts.language = newLanguage 391 | this.highlight() 392 | } 393 | 394 | addLanguage (name, options) { 395 | Prism.languages[name] = options 396 | } 397 | 398 | populateDefault () { 399 | this.updateCode(this.code) 400 | } 401 | 402 | highlight () { 403 | Prism.highlightElement(this.elCode, false) 404 | } 405 | 406 | onUpdate (callback) { 407 | if (callback && {}.toString.call(callback) !== '[object Function]') { 408 | throw Error('CodeFlask expects callback of type Function') 409 | } 410 | 411 | this.updateCallBack = callback 412 | } 413 | 414 | getCode () { 415 | return this.code 416 | } 417 | 418 | runUpdate () { 419 | if (this.updateCallBack) { 420 | this.updateCallBack(this.code) 421 | } 422 | } 423 | 424 | enableReadonlyMode () { 425 | this.elTextarea.setAttribute('readonly', true) 426 | } 427 | 428 | disableReadonlyMode () { 429 | this.elTextarea.removeAttribute('readonly') 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/styles/editor.js: -------------------------------------------------------------------------------- 1 | import { BACKGROUND_COLOR, LINE_HEIGHT, FONT_SIZE } from './theme-default' 2 | import { cssSupports } from '../utils/css-supports' 3 | 4 | const FONT_FAMILY = `"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace` 5 | const COLOR = (cssSupports('caret-color', '#000')) ? BACKGROUND_COLOR : '#ccc' 6 | const LINE_NUMBER_WIDTH = '40px' 7 | 8 | export const editorCss = ` 9 | .codeflask { 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | overflow: hidden; 14 | } 15 | 16 | .codeflask, .codeflask * { 17 | box-sizing: border-box; 18 | } 19 | 20 | .codeflask__pre { 21 | pointer-events: none; 22 | z-index: 3; 23 | overflow: hidden; 24 | } 25 | 26 | .codeflask__textarea { 27 | background: none; 28 | border: none; 29 | color: ${COLOR}; 30 | z-index: 1; 31 | resize: none; 32 | font-family: ${FONT_FAMILY}; 33 | -webkit-appearance: pre; 34 | caret-color: #111; 35 | z-index: 2; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .codeflask--has-line-numbers .codeflask__textarea { 41 | width: calc(100% - ${LINE_NUMBER_WIDTH}); 42 | } 43 | 44 | .codeflask__code { 45 | display: block; 46 | font-family: ${FONT_FAMILY}; 47 | overflow: hidden; 48 | } 49 | 50 | .codeflask__flatten { 51 | padding: 10px; 52 | font-size: ${FONT_SIZE}; 53 | line-height: ${LINE_HEIGHT}; 54 | white-space: pre; 55 | position: absolute; 56 | top: 0; 57 | left: 0; 58 | overflow: auto; 59 | margin: 0 !important; 60 | outline: none; 61 | text-align: left; 62 | } 63 | 64 | .codeflask--has-line-numbers .codeflask__flatten { 65 | width: calc(100% - ${LINE_NUMBER_WIDTH}); 66 | left: ${LINE_NUMBER_WIDTH}; 67 | } 68 | 69 | .codeflask__line-highlight { 70 | position: absolute; 71 | top: 10px; 72 | left: 0; 73 | width: 100%; 74 | height: ${LINE_HEIGHT}; 75 | background: rgba(0,0,0,0.1); 76 | z-index: 1; 77 | } 78 | 79 | .codeflask__lines { 80 | padding: 10px 4px; 81 | font-size: 12px; 82 | line-height: ${LINE_HEIGHT}; 83 | font-family: 'Cousine', monospace; 84 | position: absolute; 85 | left: 0; 86 | top: 0; 87 | width: ${LINE_NUMBER_WIDTH}; 88 | height: 100%; 89 | text-align: right; 90 | color: #999; 91 | z-index: 2; 92 | } 93 | 94 | .codeflask__lines__line { 95 | display: block; 96 | } 97 | 98 | .codeflask.codeflask--has-line-numbers { 99 | padding-left: ${LINE_NUMBER_WIDTH}; 100 | } 101 | 102 | .codeflask.codeflask--has-line-numbers:before { 103 | content: ''; 104 | position: absolute; 105 | left: 0; 106 | top: 0; 107 | width: ${LINE_NUMBER_WIDTH}; 108 | height: 100%; 109 | background: #eee; 110 | z-index: 1; 111 | } 112 | ` -------------------------------------------------------------------------------- /src/styles/injector.js: -------------------------------------------------------------------------------- 1 | export function injectCss (css, styleName, parent) { 2 | const CSS_ID = styleName || 'codeflask-style' 3 | const PARENT = parent || document.head 4 | 5 | if (!css) { 6 | return false 7 | } 8 | 9 | if (document.getElementById(CSS_ID)) { 10 | return true 11 | } 12 | 13 | const style = document.createElement('style') 14 | 15 | style.innerHTML = css 16 | style.id = CSS_ID 17 | PARENT.appendChild(style) 18 | 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/theme-default.js: -------------------------------------------------------------------------------- 1 | export const BACKGROUND_COLOR = '#fff' 2 | export const LINE_HEIGHT = '20px' 3 | export const FONT_SIZE = '13px' 4 | 5 | export const defaultCssTheme = ` 6 | .codeflask { 7 | background: ${BACKGROUND_COLOR}; 8 | color: #4f559c; 9 | } 10 | 11 | .codeflask .token.punctuation { 12 | color: #4a4a4a; 13 | } 14 | 15 | .codeflask .token.keyword { 16 | color: #8500ff; 17 | } 18 | 19 | .codeflask .token.operator { 20 | color: #ff5598; 21 | } 22 | 23 | .codeflask .token.string { 24 | color: #41ad8f; 25 | } 26 | 27 | .codeflask .token.comment { 28 | color: #9badb7; 29 | } 30 | 31 | .codeflask .token.function { 32 | color: #8500ff; 33 | } 34 | 35 | .codeflask .token.boolean { 36 | color: #8500ff; 37 | } 38 | 39 | .codeflask .token.number { 40 | color: #8500ff; 41 | } 42 | 43 | .codeflask .token.selector { 44 | color: #8500ff; 45 | } 46 | 47 | .codeflask .token.property { 48 | color: #8500ff; 49 | } 50 | 51 | .codeflask .token.tag { 52 | color: #8500ff; 53 | } 54 | 55 | .codeflask .token.attr-value { 56 | color: #8500ff; 57 | } 58 | ` 59 | -------------------------------------------------------------------------------- /src/utils/css-supports.js: -------------------------------------------------------------------------------- 1 | export function cssSupports (property, value) { 2 | if (typeof CSS !== 'undefined') { 3 | return CSS.supports(property, value) 4 | } 5 | 6 | if (typeof document === 'undefined') { 7 | return false; 8 | } 9 | 10 | return toCamelCase(property) in document.body.style 11 | } 12 | 13 | export function toCamelCase (cssProperty) { 14 | cssProperty = cssProperty 15 | .split('-') 16 | .filter(word => !!word) 17 | .map(word => word[0].toUpperCase() + word.substr(1)) 18 | .join('') 19 | 20 | return cssProperty[0].toLowerCase() + cssProperty.substr(1) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/html-escape.js: -------------------------------------------------------------------------------- 1 | const entityMap = { 2 | '&': '&', 3 | '<': '<', 4 | '>': '>', 5 | '"': '"', 6 | "'": ''', 7 | '/': '/', 8 | '`': '`', 9 | '=': '=' 10 | } 11 | 12 | export function escapeHtml (string) { 13 | return String(string).replace(/[&<>"'`=/]/g, function (s) { 14 | return entityMap[s] 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /test/e2e/test-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const expect = require('chai').expect; 4 | 5 | const server = require('../test-server'); 6 | 7 | describe('CodeFlask Tests', () => { 8 | before(() => { 9 | browser.url('http://localhost:8888/'); 10 | }); 11 | 12 | after(() => { 13 | server.close(); 14 | }); 15 | 16 | it('should open page', () => { 17 | const title = browser.getTitle(); 18 | const url = browser.getUrl(); 19 | expect(title).to.be.equals('CodeFlask Test Page'); 20 | expect(url).to.be.equals('http://localhost:8888/'); 21 | }); 22 | 23 | it('should create editor elements', function () { 24 | expect($('.codeflask').isExisting()).to.be.true; 25 | expect($('.codeflask__pre').isExisting()).to.be.true; 26 | expect($('.codeflask__textarea').isExisting()).to.be.true; 27 | expect($('.codeflask__code').isExisting()).to.be.true; 28 | expect($('.codeflask__flatten').isExisting()).to.be.true; 29 | expect($('.codeflask__flatten').isExisting()).to.be.true; 30 | }); 31 | 32 | it('should enable syntax highlight', function () { 33 | expect($('.codeflask .token.punctuation').isExisting()).to.be.true; 34 | }); 35 | 36 | it('should render lineNumbers', function () { 37 | expect($('.codeflask .codeflask__lines').isExisting()).to.be.true; 38 | expect($('.codeflask .codeflask__lines__line').isExisting()).to.be.true; 39 | }); 40 | 41 | it('should have same lineNumbers as lines of code', function () { 42 | $('.codeflask__textarea').setValue('let it = "go";\nconst parrot = "bird";'); 43 | expect($('.codeflask .codeflask__lines').isExisting()).to.be.true; 44 | const lines = $$('.codeflask .codeflask__lines__line'); 45 | expect(lines.length).to.equal(2); 46 | }); 47 | 48 | it('should update editor upon update', function () { 49 | $('.codeflask__textarea').setValue('let it = "go";'); 50 | expect($('.codeflask .token.keyword').isExisting()).to.be.true; 51 | expect($('.codeflask .token.operator').isExisting()).to.be.true; 52 | expect($('.codeflask .token.string').isExisting()).to.be.true; 53 | expect($('.codeflask .token.punctuation').isExisting()).to.be.true; 54 | }); 55 | 56 | it('should be instance of CodeFlask', function () { 57 | const isInstance = browser.execute(() => { 58 | return flask instanceof CodeFlask 59 | }); 60 | expect(isInstance).to.be.true; 61 | }); 62 | 63 | it('.updateCode(): should update lineNumbers', function () { 64 | browser.execute(() => { flask.updateCode("let age = 20"); }); 65 | const lines = $$('.codeflask .codeflask__lines__line'); 66 | expect(lines.length).to.equal(1); 67 | }); 68 | 69 | it('.updateCode(): should update lineNumbers for multiple lines', function () { 70 | browser.execute(() => { flask.updateCode("let age = 20\nlet lines = 2"); }); 71 | const lines = $$('.codeflask .codeflask__lines__line'); 72 | expect(lines.length).to.equal(2); 73 | }); 74 | 75 | it('.onUpdate(): should execute callback upon user interaction', function () { 76 | $('.codeflask__textarea').setValue(''); 77 | browser.execute(() => { flask.onUpdate(code => document.title = code) }); 78 | $('.codeflask__textarea').setValue('let it = "go";'); 79 | const title = browser.getTitle(); 80 | expect(title).to.be.equals('let it = "go";') 81 | }); 82 | 83 | it('should enable rtl when rtl: true', function () { 84 | browser.execute(() => { 85 | const test_div = document.createElement('div'); 86 | document.body.appendChild(test_div); 87 | const flask_test = new CodeFlask(test_div, { rtl: true }); 88 | }); 89 | expect($('.codeflask__textarea[dir="rtl"]').isExisting()).to.be.true; 90 | expect($('.codeflask__pre[dir="rtl"]').isExisting()).to.be.true; 91 | }); 92 | 93 | it('should NOT enable rtl when rtl: false', function () { 94 | browser.execute(() => { 95 | const test_div = document.createElement('div'); 96 | document.body.appendChild(test_div); 97 | const flask_test = new CodeFlask(test_div, { rtl: false }); 98 | }); 99 | expect($('.codeflask__textarea:not([dir="rtl"])').isExisting()).to.be.true; 100 | expect($('.codeflask__pre:not([dir="rtl"])').isExisting()).to.be.true; 101 | }); 102 | 103 | it('should NOT enable rtl when rtl not set', function () { 104 | browser.execute(() => { 105 | const test_div = document.createElement('div'); 106 | document.body.appendChild(test_div); 107 | const flask_test = new CodeFlask(test_div, { language: 'js' }); 108 | }); 109 | expect($('.codeflask__textarea:not([dir="rtl"])').isExisting()).to.be.true; 110 | expect($('.codeflask__pre:not([dir="rtl"])').isExisting()).to.be.true; 111 | }); 112 | 113 | it('.getCode(): should return current code', function () { 114 | $('.codeflask__textarea').setValue('return "my code here"'); 115 | const code = browser.execute(() => { return flask.getCode(); }); 116 | expect(code).to.be.equals('return "my code here"'); 117 | }); 118 | 119 | it('should add an ID attribute with option', function () { 120 | browser.execute(() => { 121 | const test_div = document.createElement('div'); 122 | document.body.appendChild(test_div); 123 | const flask_test = new CodeFlask(test_div, { areaId: 'thing1' }); 124 | }); 125 | expect($('.codeflask__textarea#thing1').isExisting()).to.be.true; 126 | }); 127 | 128 | it('should add an aria-labelledby attribute with option', function () { 129 | browser.execute(() => { 130 | const test_div = document.createElement('div'); 131 | document.body.appendChild(test_div); 132 | const flask_test = new CodeFlask(test_div, { ariaLabelledby: 'thing2' }); 133 | }); 134 | expect($('.codeflask__textarea[aria-labelledby="thing2"]').isExisting()).to.be.true; 135 | }); 136 | 137 | it('should add a readonly attribute with option', function () { 138 | browser.execute(() => { 139 | const test_div = document.createElement('div'); 140 | document.body.appendChild(test_div); 141 | const flask_test = new CodeFlask(test_div, { readonly: true }); 142 | }); 143 | 144 | expect($('.codeflask__textarea[readonly]').isExisting()).to.be.true; 145 | }); 146 | 147 | it('should not add a readonly attribute with option if it is set to false', function () { 148 | browser.execute(() => { 149 | const test_div = document.createElement('div'); 150 | document.body.appendChild(test_div); 151 | const flask_test = new CodeFlask(test_div, { readonly: false }); 152 | }); 153 | 154 | expect($('.codeflask__textarea:not([readonly])').isExisting()).to.be.true; 155 | }); 156 | 157 | it('should not add a readonly attribute with option if it is not set', function () { 158 | browser.execute(() => { 159 | const test_div = document.createElement('div'); 160 | document.body.appendChild(test_div); 161 | const flask_test = new CodeFlask(test_div, { }); 162 | }); 163 | 164 | expect($('.codeflask__textarea:not([readonly])').isExisting()).to.be.true; 165 | }); 166 | 167 | it('should add a readonly attribute from a function call', function () { 168 | browser.execute(() => { 169 | const test_div = document.createElement('div'); 170 | document.body.appendChild(test_div); 171 | const flask_test = new CodeFlask(test_div, {}); 172 | flask_test.enableReadonlyMode(); 173 | }); 174 | 175 | expect($('.codeflask__textarea[readonly]').isExisting()).to.be.true; 176 | }); 177 | 178 | it('should remove a readonly attribute from a function call', function () { 179 | browser.execute(() => { 180 | const test_div = document.createElement('div'); 181 | document.body.appendChild(test_div); 182 | const flask_test = new CodeFlask(test_div, { readonly: true }); 183 | flask_test.disableReadonlyMode(); 184 | }); 185 | 186 | expect($('.codeflask__textarea:not([readonly])').isExisting()).to.be.true; 187 | }); 188 | 189 | it('should not add line by press enter when editor is readonly', function () { 190 | browser.execute(() => { 191 | // remove all existing editors 192 | document.body.innerHTML = ''; 193 | const rootEl = document.createElement('div'); 194 | document.body.appendChild(rootEl); 195 | 196 | new CodeFlask(rootEl, { readonly: true, lineNumbers: true }); 197 | rootEl.querySelector('.codeflask .codeflask__textarea').dispatchEvent(new KeyboardEvent('keydown',{ key: 'Enter', keyCode: 13 })); 198 | }); 199 | 200 | const lines = $$('.codeflask .codeflask__lines__line'); 201 | expect(lines.length).to.equal(1); 202 | }); 203 | 204 | xit('should handle the tab key in the editor', function () { 205 | let flask_test 206 | browser.execute(() => { 207 | const test_div = document.createElement('div'); 208 | document.body.appendChild(test_div); 209 | flask_test = new CodeFlask(test_div, { handleTabs: true }); 210 | }); 211 | $('.codeflask__textarea').setValue('hi\thello after'); 212 | const code = browser.execute(() => { return flask.getCode(); }); 213 | expect(code).to.be.equals('hi\thello after'); 214 | }); 215 | 216 | xit('should not handle the tab key in the editor with handleTabs=false option', function () { 217 | let flask_test 218 | browser.execute(() => { 219 | const test_div = document.createElement('div'); 220 | document.body.appendChild(test_div); 221 | flask_test = new CodeFlask(test_div, { handleTabs: false }); 222 | }); 223 | $('.codeflask__textarea').setValue('hi before tab\thello after'); 224 | const code = browser.execute(() => { return flask.getCode(); }); 225 | expect(code).to.be.equals('hi before tab'); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /test/e2e/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CodeFlask Test Page 8 | 9 | 19 | 20 | 21 |

Testing below:

22 | 23 |
// Feel free to play with the code. :) 24 | 25 | function Hey(phrase) { 26 | phrase = phrase || "Hey"; 27 | return console.log(phrase); 28 | } 29 | 30 | window.onLoad = function() { 31 | Hey(); 32 | Hey("Jude"); 33 | Hey("Don't make it bad."); 34 | }
35 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/test-server.js: -------------------------------------------------------------------------------- 1 | const micro = require('micro'); 2 | const fs = require('fs'); 3 | 4 | const server = micro(async (req, res) => { 5 | const url = req.url; 6 | switch (url) { 7 | case '/': 8 | return fs.readFileSync(__dirname + '/e2e/test.html', 'utf8'); 9 | break; 10 | 11 | case '/codeflask.min.js': 12 | return fs.readFileSync(__dirname + '/../build/codeflask.min.js', 'utf8'); 13 | break; 14 | 15 | case 'codeflask.min.js': 16 | return fs.readFileSync(__dirname + '/../build/codeflask.min.js', 'utf8'); 17 | break; 18 | 19 | case './codeflask.min.js': 20 | return fs.readFileSync(__dirname + '/../build/codeflask.min.js', 'utf8'); 21 | break; 22 | 23 | default: 24 | return '404'; 25 | break; 26 | } 27 | }); 28 | 29 | module.exports = server; 30 | 31 | server.listen(8888); 32 | -------------------------------------------------------------------------------- /test/wdio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.config = { 4 | port: 9515, 5 | path: '/', 6 | specs: [ 7 | './test/e2e/*.js' 8 | ], 9 | exclude: [], 10 | maxInstances: 2, // it depends on the plan of the cloud service 11 | sync: true, 12 | logLevel: 'error', 13 | coloredLogs: true, 14 | waitforTimeout: 20000, 15 | connectionRetryTimeout: 90000, 16 | connectionRetryCount: 3, 17 | framework: 'mocha', 18 | reporters: ['spec'], 19 | mochaOpts: { 20 | ui: 'bdd', 21 | timeout: 30000 22 | }, 23 | capabilities: [ 24 | { 25 | browserName: 'chrome', 26 | "goog:chromeOptions": { 27 | args: ['--headless', '--disable-gpu', '--window-size=1280,800'] 28 | } 29 | } 30 | ], 31 | services: ['chromedriver'] 32 | } 33 | --------------------------------------------------------------------------------