├── .gitignore ├── .postcssrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── build └── lit-code.mjs ├── logo.svg ├── package-lock.json ├── package.json ├── preview_dark.svg ├── preview_white.svg ├── rollup.config.dev.mjs ├── rollup.config.prod.mjs └── src ├── index.mjs ├── lit-code.css └── lit-code.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .eslintcache 4 | Thumbs.db 5 | dist 6 | *.bak 7 | *~ 8 | github\.key* 9 | .firebase* 10 | .firebase/** 11 | firebase.json 12 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-preset-env": { 4 | "stage": 3, 5 | "features": { 6 | "nesting-rules": true 7 | } 8 | }, 9 | "postcss-input-range": {}, 10 | "cssnano": { 11 | "preset": "default" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.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 | Copyright 2021 John Kamel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/lit-code.svg)](https://badge.fury.io/js/lit-code) 2 | [![Build Status](https://travis-ci.com/Demiler/lit-code.svg?branch=master)](https://travis-ci.com/Demiler/lit-code) 3 | [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/lit-code) 4 | [![Bundlephobia gzip size](https://img.shields.io/bundlephobia/minzip/lit-code?label=size)](https://bundlephobia.com/result?p=lit-code) 5 | 6 | ![logo](logo.svg) 7 | 8 | # lit-code 9 | Simple browser code editor for small code chunks. 10 | Written with web-components and [lit](https://lit.dev/) library. 11 | Inspired by [CodeFlask](https://github.com/kazzkiq/CodeFlask). 12 | 13 | [DEMO](https://demiler.github.io/lit-code/) 14 | 15 | ![preview](preview_dark.svg) 16 | 17 | # Features 18 | + Web component 19 | + Keeps your last line indentetion 20 | + Auto closing brackets, quotes 21 | + Indents line with the Tab key 22 | 23 | # Installation 24 | ``` 25 | npm i lit-code 26 | ``` 27 | Requires lit library and if you want highlight prismjs aswell. 28 | 29 | # Usage 30 | Import it like this 31 | ```js 32 | import 'prismjs'; //to enable code highlight 33 | //or import './my-version-of-prism.js' 34 | import 'lit-code'; //component it self 35 | ``` 36 | Use it like any other custom element! 37 | ```html 38 | 39 | ``` 40 | 41 | ### options 42 | + `linenumbers` - add line numbers 43 | + `noshadow` - disables element's shadow-dom so you can sepcify your own colorscheme 44 | + `mycolors` - disables buildin theme for highlight 45 | + `code` - set pre existing code 46 | + `language` - set language (must exists in Prism package) 47 | + `grammar` - grammar for you language (sets automaticaly with any change of `language`); 48 | 49 | That's how you can use them: 50 | ```html 51 | 59 | ``` 60 | 61 | # API 62 | To get any code updates use `@update` as event listener. That will proved you with latests changes in code: 63 | ```html 64 | console.log('Hey, I\'ve got some new code:', code) 67 | } 68 | > 69 | ``` 70 | Or you can grab code with `.getCode()` 71 | 72 | To set some code at runtime use `.setCode()`. 73 | 74 | # Styling 75 | `lit-code` by default (as css vars) support `js`, `clike`, `html` and `css` hightlight. 76 | Also `lit-code` keeps it self safe in comfy shadom-dom but you can still 77 | specify various colors to it via css variables: 78 | ```css 79 | --font-family: monospace; 80 | --font-size: 12pt; 81 | --line-height: 14pt; 82 | --lines-width: 40px; 83 | 84 | --editor-bg-color: white; 85 | --editor-text-color: black; 86 | --editor-caret-color: var(--editor-text-color); 87 | --editor-sel-color: #b9ecff; 88 | 89 | --lines-bg-color: #eee; 90 | --lines-text-color: black; 91 | --scroll-track-color: #aaa; 92 | --scroll-thumb-color: #eee; 93 | 94 | /*lit-theme colors for default highlight tokens */ 95 | --hl-color-string: #00ae22; 96 | --hl-color-function: #004eff; 97 | --hl-color-number: #dd9031; 98 | --hl-color-operator: #5a5a5a; 99 | --hl-color-class-name: #78c3ca; 100 | --hl-color-punctuation: #4a4a4a; 101 | --hl-color-keyword: #8500ff; 102 | --hl-color-comment: #aaa; 103 | ``` 104 | 105 | These are default editor and highlight colors but you can spice things up 106 | by adding your own highlight with your `Prism` pacakge, disabling shadow-dom and 107 | creating new highlight colorscheme: 108 | ```js 109 | import './my-version-of-prism-with-cpp.js'; 110 | import 'lit-code'; 111 | ``` 112 | ```html 113 | 114 | ``` 115 | ```css 116 | .litcode { 117 | --editor-bg-color: black; 118 | --editor-text-color: white; 119 | } 120 | .litcode .token.type { color: red; } 121 | .litcode .token.template { color: yellow; } 122 | ``` 123 | 124 | ## Pro tip 125 | For easy access to parsed by prismjs words hold `ctrl` + `shift` 126 | while inspecting highlight with dev tools 127 | 128 | # Example 129 | ```js 130 | import { html, css, LitElement } from 'lit'; 131 | import 'prismjs'; 132 | import 'lit-code'; 133 | 134 | class JsCodePlayground extends LitElement { 135 | static styles = css` 136 | pre, lit-code { 137 | max-height: 300px; 138 | border-radius: 8px; 139 | border: 2px solid #eee; 140 | } 141 | `; 142 | 143 | static properties = { 144 | output: { type: String } 145 | }; 146 | 147 | render() { 148 | return html` 149 | 150 | 151 |
${this.output}
152 | `; 153 | } 154 | 155 | runCode() { 156 | const oldLog = console.log; 157 | console.log = (...args) => { this.output += args.join(' ') + '\n'; } 158 | this.output = ''; 159 | const code = this.shadowRoot.querySelector('lit-code').getCode(); 160 | eval(code); //eval is used only for demonstration purposes 161 | console.log = oldLog; 162 | } 163 | }; 164 | 165 | customElements.define('js-code-playground', JsCodePlayground); 166 | ``` 167 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rray-code" 3 | } 4 | -------------------------------------------------------------------------------- /build/lit-code.mjs: -------------------------------------------------------------------------------- 1 | import{css as e,LitElement as t,html as o}from"lit";var i=e`:host{display:block;width:100%;--font-family:monospace;--font-size:12pt;--line-height:14pt;--lines-width:40px;--editor-bg-color:#fff;--editor-text-color:#000;--editor-caret-color:var(--editor-text-color);--editor-sel-color:#b9ecff;--lines-bg-color:#eee;--lines-text-color:#000;--scroll-track-color:#aaa;--scroll-thumb-color:#eee;--hl-color-string:#00ae22;--hl-color-function:#004eff;--hl-color-number:#dd9031;--hl-color-operator:#5a5a5a;--hl-color-class-name:#3cabb6;--hl-color-punctuation:#4a4a4a;--hl-color-keyword:#8500ff;--hl-color-comment:#aaa;--hl-color-tag:#3a9bca;--hl-color-selector:#3a9bca;--hl-color-property:#713aca}.litcode,:host{height:100%;max-height:inherit;max-width:inherit}.litcode{border-radius:inherit;overflow:auto;display:grid;grid-template-columns:var(--lines-width) auto;position:relative;line-height:var(--line-height);font-family:var(--font-family);font-size:var(--font-size)}.litcode :is(.litcode_textarea,.litcode_highlight){grid-column:1/3;grid-row:1;box-sizing:border-box;height:100%;width:100%;height:calc(var(--height) + 100% - var(--line-height)*4)}.litcode .litcode_linenumbers~:is(.litcode_textarea,.litcode_highlight){grid-column:2;grid-row:1}.litcode :is(.litcode_textarea,.litcode_linenumbers,.litcode_highlight){padding:4px}.litcode .litcode_linenumbers{position:sticky;left:0;padding-right:1px;text-align:right;background-color:var(--lines-bg-color);color:var(--lines-text-color);height:100%;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.litcode .litcode_textarea{font:inherit;resize:none;border:none;outline:none;margin:0;white-space:pre;color:transparent;height:100%;caret-color:var(--editor-caret-color);background-color:var(--editor-bg-color)}.litcode .litcode_textarea::-moz-selection{background-color:var(--editor-sel-color)}.litcode .litcode_textarea::selection{background-color:var(--editor-sel-color)}.litcode .litcode_highlight{width:100%;height:100%;color:var(--editor-text-color);pointer-events:none}.litcode .litcode_highlight>pre{margin:0}.litcode::-webkit-scrollbar{width:10px;height:10px}.litcode::-webkit-scrollbar-track{background-color:var(--scroll-track-color);border-radius:8px}.litcode::-webkit-scrollbar-thumb{background-color:var(--scroll-thumb-color);border-radius:8px}.litcode::-webkit-scrollbar-corner{background-color:var(--scroll-track-color)}.litcode[default] .token.string{color:var(--hl-color-string)}.litcode[default] .token.function{color:var(--hl-color-function)}.litcode[default] .token.number{color:var(--hl-color-number)}.litcode[default] .token.operator{color:var(--hl-color-operator)}.litcode[default] .token.class-name{color:var(--hl-color-class-name)}.litcode[default] .token.punctuation{color:var(--hl-color-punctuation)}.litcode[default] .token.keyword{color:var(--hl-color-keyword)}.litcode[default] .token.comment{color:var(--hl-color-comment)}.litcode[default] .token.tag{color:var(--hl-color-tag)}.litcode[default] .token.selector{color:var(--hl-color-selector)}.litcode[default] .token.property{color:var(--hl-color-property)}`;const r="undefined"!=typeof Prism;function l(e){return"string"==typeof e?o`${e}`:o`${Array.isArray(e.content)?e.content.map(l):o`${e.content}`}`}customElements.define("lit-code",class extends t{static get styles(){return i}static get properties(){return{code:{type:String},grammar:{type:Object},language:{type:String},noshadow:{attribute:!0},linenumbers:{attribute:!0}}}get shadowDom(){return!this.hasAttribute("noshadow")}constructor(){super(),this.opening=["(","{","[","'",'"'],this.closing=[")","}","]","'",'"'],this.code="",this.indent=" ",this.language="clike",r&&(this.grammar=Prism.languages[this.language])}update(e){if(super.update(e),r&&e.has("language")){const e=Prism.languages[this.language.toLowerCase()];if(void 0===e)throw new Error("Unsupported Prism language");this.grammar=e}}_getElement(e){return this.shadowDom?this.shadowRoot.querySelector(`.litcode_${e}`):this.querySelector(`.litcode_${e}`)}firstUpdated(){this.elTextarea=this._getElement("textarea"),this.elContainer=this._getElement("litcode"),this.updateTextarea()}render(){return o` ${this.shadowDom?o``:o``}
${this.hasAttribute("linenumbers")?o`
1
${(this.code.match(/\r?\n/g)||[]).map(((e,t)=>o`
${t+2}
`))}
`:o``}
${r?Prism.tokenize(this.code,this.grammar).map(l):o`${this.code}`}
`}setCode(e){this.code=e,this.updateTextarea()}getCode(){return this.code}createRenderRoot(){return this.shadowDom?super.createRenderRoot():this}setCursor(e){this.elTextarea.setSelectionRange(e,e)}setSelect(e,t){this.elTextarea.setSelectionRange(e,t)}getCurrentLineIndent(){const e=this.elTextarea.selectionStart,t=this.elTextarea.selectionEnd,o=this.code.lastIndexOf("\n",e-1)+1,i=(()=>{let e=o;for(;" "===this.code[e]&&e 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-code", 3 | "version": "0.1.10", 4 | "description": "Simple web editor created with web components", 5 | "license": "MIT", 6 | "repository": "demiler/lit-code", 7 | "homepage": "https://demiler.github.io/lit-code/", 8 | "author": "demiler", 9 | "main": "build/lit-code.mjs", 10 | "files": [ 11 | "build/lit-code.mjs" 12 | ], 13 | "keywords": [ 14 | "editor", 15 | "web components", 16 | "highlight", 17 | "code", 18 | "lit element" 19 | ], 20 | "devDependencies": { 21 | "@rollup/plugin-node-resolve": "^13.0.0", 22 | "chokidar": "^3.5.2", 23 | "glob": "^7.1.7", 24 | "postcss": "^8.3.5", 25 | "postcss-input-range": "^4.0.0", 26 | "postcss-preset-env": "^6.7.0", 27 | "rollup": "^2.52.2", 28 | "rollup-plugin-livereload": "^2.0.5", 29 | "rollup-plugin-minify-html-literals": "^1.2.6", 30 | "rollup-plugin-postcss": "^4.0.0", 31 | "rollup-plugin-postcss-lit": "^1.1.0", 32 | "rollup-plugin-serve": "^1.1.0", 33 | "rollup-plugin-svg": "^2.0.0", 34 | "rollup-plugin-terser": "^7.0.2", 35 | "prismjs": "^1.24.1" 36 | }, 37 | "scripts": { 38 | "dev": "rollup --watch --config rollup.config.dev.mjs", 39 | "build": "rollup --config rollup.config.prod.mjs", 40 | "release": "npm run build; git add build/lit-code*; git commit -m \"build\"; npm version patch; npm publish" 41 | }, 42 | "dependencies": { 43 | "lit": "^2.0.0-rc.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /preview_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /rollup.config.dev.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import postcss from 'rollup-plugin-postcss'; 3 | import postcssLit from 'rollup-plugin-postcss-lit'; 4 | import serve from 'rollup-plugin-serve'; 5 | import livereload from 'rollup-plugin-livereload'; 6 | import glob from 'glob'; 7 | import { resolve, dirname } from 'path'; 8 | import { fileURLToPath } from 'url'; 9 | import svg from 'rollup-plugin-svg' 10 | 11 | export default { 12 | input: 'src/index.mjs', 13 | output: { 14 | file: 'dist/index.mjs', 15 | format: 'es' 16 | }, 17 | plugins: [ 18 | serve({ 19 | open: false, 20 | contentBase: 'dist', 21 | host: '0.0.0.0', 22 | port: 8080, 23 | }), 24 | postcss({ 25 | inject: false, 26 | }), 27 | postcssLit.default({ 28 | importPackage: 'lit', 29 | }), 30 | nodeResolve({ 31 | browser: true, 32 | }), 33 | svg(), 34 | { 35 | name: 'watch-external', 36 | buildStart() { 37 | const dir = dirname(fileURLToPath(import.meta.url)); 38 | glob('src/**/*.{html,css,woff2,svg,png,webmanifest}', {}, (err, files) => { 39 | if (err) throw err; 40 | for (const file of files) this.addWatchFile(resolve(dir, file)); 41 | }); 42 | } 43 | } 44 | ], 45 | watch: { 46 | include: './src/**', 47 | chokidar: true, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /rollup.config.prod.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import postcss from 'rollup-plugin-postcss'; 3 | import postcssLit from 'rollup-plugin-postcss-lit'; 4 | import minifyHTML from 'rollup-plugin-minify-html-literals'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | export default { 8 | input: 'src/lit-code.mjs', 9 | output: { 10 | sourcemap: false, 11 | format: 'es', 12 | name: 'lit-code', 13 | file: 'build/lit-code.mjs', 14 | }, 15 | plugins: [ 16 | postcss({ 17 | inject: false, 18 | }), 19 | postcssLit.default({ 20 | importPackage: 'lit', 21 | }), 22 | minifyHTML.default({ 23 | options: { 24 | minifyOptions: { 25 | conservativeCollapse: true, 26 | minifyCSS: false, // broken for template strings 27 | minifyJS: false, 28 | } 29 | } 30 | }), 31 | terser(), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | import 'prismjs'; 2 | import './lit-code.mjs'; 3 | -------------------------------------------------------------------------------- /src/lit-code.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | 5 | :host { 6 | height: 100%; 7 | width: 100%; 8 | max-height: inherit; 9 | max-width: inherit; 10 | 11 | --font-family: monospace; 12 | --font-size: 12pt; 13 | --line-height: 14pt; 14 | --lines-width: 40px; 15 | 16 | --editor-bg-color: white; 17 | --editor-text-color: black; 18 | --editor-caret-color: var(--editor-text-color); 19 | --editor-sel-color: #b9ecff; 20 | 21 | --lines-bg-color: #eee; 22 | --lines-text-color: black; 23 | --scroll-track-color: #aaa; 24 | --scroll-thumb-color: #eee; 25 | 26 | --hl-color-string: #00ae22; 27 | --hl-color-function: #004eff; 28 | --hl-color-number: #dd9031; 29 | --hl-color-operator: #5a5a5a; 30 | --hl-color-class-name: #3cabb6; 31 | --hl-color-punctuation: #4a4a4a; 32 | --hl-color-keyword: #8500ff; 33 | --hl-color-comment: #aaa; 34 | --hl-color-tag: #3a9bca; 35 | --hl-color-selector: #3a9bca; 36 | --hl-color-property: #713aca; 37 | 38 | } 39 | 40 | .litcode { 41 | border-radius: inherit; 42 | height: 100%; 43 | max-height: inherit; 44 | max-width: inherit; 45 | overflow: auto; 46 | 47 | display: grid; 48 | grid-template-columns: var(--lines-width) auto; 49 | position: relative; 50 | 51 | line-height: var(--line-height); 52 | font-family: var(--font-family); 53 | font-size: var(--font-size); 54 | 55 | & :is(.litcode_textarea, .litcode_highlight) { 56 | grid-column: 1/3; 57 | grid-row: 1; 58 | box-sizing: border-box; 59 | 60 | height: 100%; 61 | width: 100%; 62 | height: calc(var(--height) + 100% - 4 * var(--line-height)); 63 | } 64 | 65 | & .litcode_linenumbers ~ :is(.litcode_textarea, .litcode_highlight) { 66 | grid-column: 2; 67 | grid-row: 1; 68 | } 69 | 70 | & :is(.litcode_textarea, .litcode_linenumbers, .litcode_highlight) { 71 | padding: 4px; 72 | } 73 | 74 | & .litcode_linenumbers { 75 | position: sticky; 76 | left: 0; 77 | 78 | padding-right: 1px; 79 | text-align: right; 80 | background-color: var(--lines-bg-color); 81 | color: var(--lines-text-color); 82 | height: 100%; 83 | box-sizing: border-box; 84 | user-select: none; 85 | } 86 | 87 | & .litcode_textarea { 88 | font: inherit; 89 | resize: none; 90 | border: none; 91 | outline: none; 92 | margin: 0; 93 | white-space: pre; 94 | color: transparent; 95 | height: 100%; 96 | caret-color: var(--editor-caret-color); 97 | background-color: var(--editor-bg-color); 98 | } 99 | 100 | & .litcode_textarea::selection { 101 | background-color: var(--editor-sel-color); 102 | } 103 | 104 | & .litcode_highlight { 105 | width: 100%; 106 | height: 100%; 107 | color: var(--editor-text-color); 108 | 109 | pointer-events: none; 110 | 111 | & > pre { 112 | margin: 0; 113 | } 114 | } 115 | 116 | &::-webkit-scrollbar { 117 | width: 10px; 118 | height: 10px; 119 | } 120 | 121 | &::-webkit-scrollbar-track { 122 | background-color: var(--scroll-track-color); 123 | border-radius: 8px; 124 | } 125 | 126 | &::-webkit-scrollbar-thumb { 127 | background-color: var(--scroll-thumb-color); 128 | border-radius: 8px; 129 | } 130 | 131 | &::-webkit-scrollbar-corner { 132 | background-color: var(--scroll-track-color); 133 | } 134 | } 135 | 136 | .litcode[default] .token.string { color: var(--hl-color-string); } 137 | .litcode[default] .token.function { color: var(--hl-color-function); } 138 | .litcode[default] .token.number { color: var(--hl-color-number); } 139 | .litcode[default] .token.operator { color: var(--hl-color-operator); } 140 | .litcode[default] .token.class-name { color: var(--hl-color-class-name); } 141 | .litcode[default] .token.punctuation { color: var(--hl-color-punctuation); } 142 | .litcode[default] .token.keyword { color: var(--hl-color-keyword); } 143 | .litcode[default] .token.comment { color: var(--hl-color-comment); } 144 | .litcode[default] .token.tag { color: var(--hl-color-tag); } 145 | .litcode[default] .token.selector { color: var(--hl-color-selector); } 146 | .litcode[default] .token.property { color: var(--hl-color-property); } 147 | -------------------------------------------------------------------------------- /src/lit-code.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Experimental editor on web components 3 | * @module components/lit-editor 4 | * @param { attribute } noshadow Removes shadow dom from web component 5 | * @param { attribute } linenumber Adds linenumber 6 | * @param { attribute } mycolors Disable included highlight colors for prism 7 | * @param { string } [code] Code inside of editor 8 | * @param { string } [language='clike'] Used lanaguage 9 | * @param { object } [grammar] Prism language object 10 | */ 11 | 12 | import { html, LitElement } from 'lit'; 13 | import style from './lit-code.css'; 14 | 15 | const IS_PRISM = (typeof Prism !== "undefined"); 16 | 17 | function htmlize(el) { 18 | if (typeof el === 'string') return html`${el}`; 19 | 20 | return html`${ 21 | Array.isArray(el.content) 22 | ? el.content.map(htmlize) 23 | : html`${el.content}` 24 | }`; 25 | } 26 | 27 | class LitCode extends LitElement { 28 | static get styles() { 29 | return style; 30 | } 31 | 32 | static get properties() { 33 | return { 34 | code: { type: String }, 35 | grammar: { type: Object }, 36 | language: { type: String }, 37 | noshadow: { attribute: true }, 38 | linenumbers: { attribute: true }, 39 | }; 40 | }; 41 | 42 | get shadowDom() { return !this.hasAttribute('noshadow'); } 43 | 44 | constructor() { 45 | super(); 46 | this.opening = [ '(', '{', '[', '\'', '"' ]; 47 | this.closing = [ ')', '}', ']', '\'', '"' ]; 48 | 49 | this.code = ''; 50 | this.indent = ' '; 51 | 52 | this.language = 'clike'; 53 | if (IS_PRISM) 54 | this.grammar = Prism.languages[this.language]; 55 | } 56 | 57 | update(params) { 58 | super.update(params); 59 | if (IS_PRISM && params.has('language')) { 60 | const newGrammar = Prism.languages[this.language.toLowerCase()]; 61 | if (newGrammar === undefined) { 62 | throw new Error('Unsupported Prism language'); 63 | this.language = params.get('language'); 64 | } 65 | else this.grammar = newGrammar; 66 | } 67 | } 68 | 69 | _getElement(id) { 70 | return this.shadowDom 71 | ? this.shadowRoot.querySelector(`.litcode_${id}`) 72 | : this.querySelector(`.litcode_${id}`); 73 | } 74 | 75 | firstUpdated() { 76 | this.elTextarea = this._getElement('textarea'); 77 | this.elContainer = this._getElement('litcode'); 78 | this.updateTextarea(); 79 | } 80 | 81 | render() { 82 | return html` 83 | ${this.shadowDom ? html`` : html``} 84 | 85 |
86 | ${!this.hasAttribute('linenumbers') ? html`` : html` 87 |
88 |
1
89 | ${(this.code.match(/\r?\n/g) || []).map((_, i) => html` 90 |
${i + 2}
91 | `)} 92 |
93 | `} 94 | 95 | 100 | 101 |
${
102 |           IS_PRISM ? Prism.tokenize(this.code, this.grammar).map(htmlize) : html`${this.code}`
103 |         }
104 |
105 | `; 106 | } 107 | 108 | setCode(code) { 109 | this.code = code; 110 | this.updateTextarea(); 111 | } 112 | 113 | getCode() { 114 | return this.code; 115 | } 116 | 117 | createRenderRoot() { 118 | return this.shadowDom ? super.createRenderRoot() : this; 119 | } 120 | 121 | setCursor(pos) { 122 | this.elTextarea.setSelectionRange(pos, pos); 123 | } 124 | 125 | setSelect(from, to) { 126 | this.elTextarea.setSelectionRange(from, to); 127 | } 128 | 129 | getCurrentLineIndent() { 130 | const selStart = this.elTextarea.selectionStart; 131 | const selEnd = this.elTextarea.selectionEnd; 132 | 133 | const indentStart = this.code.lastIndexOf('\n', selStart - 1) + 1; 134 | const spaces = (() => { 135 | let pos = indentStart; 136 | while (this.code[pos] === ' ' && pos < selEnd) pos++; 137 | return pos - indentStart; 138 | })(); 139 | return ' '.repeat(spaces); 140 | } 141 | 142 | updateTextarea() { 143 | if (!this.elTextarea) return; 144 | this.elTextarea.value = this.code; 145 | 146 | this.dispatchEvent(new CustomEvent('update', { detail: this.code })); 147 | } 148 | 149 | insertCode(pos, text, placeCursor = true) { 150 | this.code = 151 | this.code.substring(0, pos) + text + this.code.substring(pos) 152 | this.updateTextarea(); 153 | if (placeCursor) this.setCursor(pos + text.length); 154 | } 155 | 156 | replaceCode(posFrom, posTo, text = '', placeCursor = true) { 157 | this.code = 158 | this.code.substring(0, posFrom) + text + this.code.substring(posTo); 159 | this.updateTextarea(); 160 | if (placeCursor) this.setCursor(posFrom + text.length); 161 | } 162 | 163 | handleKeys(e) { 164 | switch (e.code) { 165 | case 'Tab': this.handleTabs(e); break; 166 | case 'Enter': this.handleNewLine(e); break; 167 | case 'Backspace': this.handleBackspace(e); break; 168 | default: 169 | if (this.opening.includes(e.key)) 170 | this.handleAutoClose(e); 171 | else if (this.closing.includes(e.key)) 172 | this.handleAutoSkip(e); 173 | } 174 | } 175 | 176 | handleInput({ target }) { 177 | this.code = target.value; 178 | this.dispatchEvent(new CustomEvent('update', { detail: this.code })); 179 | } 180 | 181 | handleTabs(e) { 182 | e.preventDefault(); 183 | const selStart = this.elTextarea.selectionStart; 184 | const selEnd = this.elTextarea.selectionEnd; 185 | 186 | if (selStart !== selEnd) { //multiline indent 187 | const selLineStart = Math.max(0, this.code.lastIndexOf('\n', selStart - 1)); 188 | const selLineEnd = Math.max(this.code.indexOf('\n', selEnd), selEnd); 189 | 190 | let linesInChunk = 0; 191 | let codeChunk = this.code.substring(selLineStart, selLineEnd); 192 | let lenShift = this.indent.length; 193 | if (selLineStart === 0) codeChunk = '\n' + codeChunk; 194 | 195 | if (e.shiftKey) { //Unindent 196 | lenShift = -lenShift; 197 | linesInChunk = (codeChunk.match(new RegExp('\n' + this.indent, 'g')) || []).length; 198 | codeChunk = codeChunk.replaceAll('\n' + this.indent, '\n'); 199 | } 200 | else { //Indent 201 | linesInChunk = (codeChunk.match(/\n/g) || []).length; 202 | codeChunk = codeChunk.replaceAll('\n', '\n' + this.indent); 203 | } 204 | 205 | if (selLineStart === 0) codeChunk = codeChunk.replace(/^\n/, ''); 206 | this.replaceCode(selLineStart, selLineEnd, codeChunk, false); 207 | 208 | const newStart = Math.max(selLineStart + 1, selStart + lenShift); 209 | const newEnd = selEnd + linesInChunk * lenShift; 210 | this.setSelect(newStart, newEnd); 211 | } 212 | else { 213 | this.insertCode(selStart, this.indent, true); 214 | } 215 | } 216 | 217 | handleBackspace(e) { 218 | const selStart = this.elTextarea.selectionStart; 219 | const selEnd = this.elTextarea.selectionEnd; 220 | if (e.ctrlKey || selStart !== selEnd) return; 221 | 222 | e.preventDefault(); 223 | 224 | //remove brackets pairs 225 | const prevSymbol = this.code[selStart - 1]; 226 | const curSymbol = this.code[selStart]; 227 | const isInPairs = this.opening.includes(prevSymbol) && this.closing.includes(curSymbol); 228 | const isPair = this.closing[this.opening.indexOf(prevSymbol)] === curSymbol; 229 | 230 | if (isInPairs && isPair) { 231 | this.replaceCode(selStart - 1, selStart + 1); 232 | } 233 | else { //remove indent 234 | const chunkStart = selStart - this.indent.length; 235 | const chunkEnd = selStart; 236 | const chunk = this.code.substring(chunkStart, chunkEnd); 237 | 238 | if (chunk === this.indent) this.replaceCode(chunkStart, chunkEnd); 239 | else this.replaceCode(selStart - 1, selStart); 240 | } 241 | } 242 | 243 | handleAutoClose(e) { 244 | const selStart = this.elTextarea.selectionStart; 245 | const selEnd = this.elTextarea.selectionEnd; 246 | if (this.code[selStart] === '\'' || this.code[selStart] === '"') { 247 | return this.handleAutoSkip(e); 248 | } 249 | e.preventDefault(); 250 | 251 | if (selStart === selEnd) { 252 | const opening = e.key; 253 | const closing = this.closing[this.opening.indexOf(opening)]; 254 | 255 | if (opening === '{' 256 | && (this.code[selStart] === '\n' || this.code.length === selStart) 257 | ) { 258 | const lineShift = '\n' + this.getCurrentLineIndent(); 259 | this.insertCode(selStart, opening + lineShift + this.indent + lineShift + closing); 260 | this.setCursor(selStart + lineShift.length + this.indent.length + 1); 261 | } 262 | else { 263 | this.insertCode(selStart, opening + closing); 264 | this.setCursor(selStart + 1); 265 | } 266 | } 267 | } 268 | 269 | handleAutoSkip(e) { 270 | const selStart = this.elTextarea.selectionStart; 271 | 272 | if (this.code[selStart] === e.key) { 273 | e.preventDefault(); 274 | this.setCursor(selStart + 1); 275 | } 276 | } 277 | 278 | handleNewLine(e) { 279 | e.preventDefault(); 280 | this.insertCode(this.elTextarea.selectionStart, '\n' + this.getCurrentLineIndent()); 281 | if (this.elTextarea.selectionStart === this.code.length) { 282 | this.elContainer.scrollTop = this.elContainer.scrollHeight; 283 | } 284 | } 285 | } 286 | 287 | customElements.define('lit-code', LitCode); 288 | --------------------------------------------------------------------------------