├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── demo └── src │ ├── App.js │ └── index.js ├── nwb.config.js ├── package-lock.json ├── package.json ├── src ├── getPrismSource.js ├── index.js ├── prism-copy-to-clipboard.js └── utils │ ├── constant.js │ ├── getIndent.js │ ├── htmlToPlain.js │ ├── languages.js │ ├── normalizeHtml.js │ ├── plugins.js │ ├── prism.js │ ├── selection-range.js │ └── themes.js ├── tests ├── .eslintrc └── index.test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 10 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= 10 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Prism Editor 2 | 3 | 4 | > A dead simple code editor with theme,syntax highlighting,line numbers. 5 | 6 | ## Demo 7 | 8 | [react-prism-editor](https://lumia2046.github.io/react-prism-editor/) 9 | 10 | 11 | ## Features 12 | - Theme 13 | - Copy to dashboard 14 | - Code Editing ^^ 15 | - Syntax highlighting 16 | - Undo / Redo 17 | - Copy / Paste 18 | - The spaces/tabs of the previous line is preserved when a new line is added 19 | - Works on mobile (thanks to contenteditable) 20 | - Resize to parent width and height 21 | - Support for line numbers 22 | - Support for autosizing the editor 23 | - Autostyling the linenumbers 24 | 25 | ## Install 26 | 27 | ```sh 28 | npm install react-prism-editor 29 | ``` 30 | 31 | or 32 | 33 | ```sh 34 | yarn add react-prism-editor 35 | ``` 36 | 37 | ## Usage 38 | 39 | 40 | 41 | ```js 42 | import ReactPrismEditor from "react-prism-editor"; 43 | 44 | { 52 | this.code = code 53 | console.log(code) 54 | }} 55 | /> 56 | 57 | 58 | ``` 59 | ## Props 60 | 61 | | Name | Type | Default | Options | Description | 62 | | -------------------- | --------- | ------- | ------------------------------------ | ------------------------------------------------ | 63 | | code | `string` | `""` | - | the code | 64 | | language | `String` |`"javascript"`| `json,javascript,jsx,tsx,typescript`
`html,vue,angular,css,sass,markup`
`java,php,csharp,c,cpp,sql,xml` | language of the code | 65 | | lineNumbers | `Boolean` | `false` | - | Whether to show line numbers or not | 66 | | readonly | `Boolean` | `false` | - | Indicates if the editor is read only or not | 67 | | clipboard | `Boolean` | `false` | - | Whether to show clipboard or not | 68 | | showLanguage | `Boolean` | `false` | - | Whether to show language or not | 69 | 70 | 71 | 72 | ## Events 73 | 74 | | Name | Parameters | Description | 75 | | ---------- | ---------- | ------------------------------- | 76 | | changeCode | `(code)` | Fires when the code is changed. | 77 | 78 | 79 | ## Thanks 80 | 81 | inspired by [react-live](https://github.com/FormidableLabs/react-live). 82 | 83 | ## License 84 | 85 | MIT -------------------------------------------------------------------------------- /demo/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Editor from '../../src' 3 | import { themes } from '../../src/utils/themes' 4 | import { languages } from '../../src/utils/languages' 5 | 6 | 7 | 8 | class App extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | const { lineNumber, readOnly, code, theme, language, showLanguage, clipboard } = this.props 12 | this.state = { 13 | language, 14 | theme, 15 | lineNumber, 16 | readOnly, 17 | showLanguage, 18 | clipboard, 19 | code 20 | } 21 | } 22 | 23 | 24 | render() { 25 | const { lineNumber, readOnly, code, theme, language, showLanguage, clipboard } = this.state 26 | return
27 |
28 | this.setState({ lineNumber: !lineNumber })} > 29 | Line Numbers 30 | { }} /> 31 | 32 | this.setState({ readOnly: !readOnly })}> 33 | Readonly 34 | { }} /> 35 | 36 | 37 | Theme 38 | 43 | 44 | 45 | Language 46 | 51 | 52 |
53 | { 62 | this.code = code 63 | console.log(code) 64 | }} 65 | /> 66 |
67 | } 68 | } 69 | 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | 6 | 7 | ReactDOM.render(
8 |
9 |
10 |

React Prism Code Editor

11 |

12 | A dead simple code editor with theme、 syntax highlighting 、 line numbers. 13 |

14 |
15 |
16 | Documentation on 17 | Github 18 |
19 |
20 |
21 | { 57 | this.code = code 58 | console.log(code) 59 | }} 60 | /> 61 | } 62 | } 63 | 64 | export default App; 65 | `} 66 | /> 67 | `} 123 | /> 124 |
125 |
, document.querySelector('#demo')); 126 | 127 | // If you want your app to work offline and load faster, you can change 128 | // unregister() to register() below. Note this comes with some pitfalls. 129 | // Learn more about service workers: https://bit.ly/CRA-PWA 130 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: false 6 | }, 7 | babel: { 8 | // plugins: [ 9 | // [ 10 | // "prismjs", 11 | // { 12 | // "languages": [ 13 | // "javascript", 14 | // "json", 15 | // "jsx", 16 | // "tsx", 17 | // "typescript", 18 | // "markup", 19 | // "html", 20 | // "css", 21 | // "sass", 22 | // "xml", 23 | // "java", 24 | // "php", 25 | // "csharp", 26 | // "c", 27 | // "cpp", 28 | // "sql" 29 | // ], 30 | // "plugins": [ 31 | // "line-numbers", 32 | // "show-language", 33 | // "copy-to-clipboard", 34 | // "custom-class" 35 | // ], 36 | // "css": true 37 | // } 38 | // ]] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-prism-editor", 3 | "version": "1.1.1", 4 | "description": "React prism editor with theme,line-numbers,copy to dashboard", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "css", 8 | "es", 9 | "lib", 10 | "umd" 11 | ], 12 | "scripts": { 13 | "build": "nwb build-react-component", 14 | "clean": "nwb clean-module && nwb clean-demo", 15 | "prepublishOnly": "npm run build", 16 | "start": "nwb serve-react-demo", 17 | "test": "nwb test-react", 18 | "test:coverage": "nwb test-react --coverage", 19 | "test:watch": "nwb test-react --server" 20 | }, 21 | "dependencies": { 22 | "clipboard": "^2.0.8", 23 | "dom-iterator": "^1.0.0", 24 | "escape-html": "^1.0.3", 25 | "prismjs": "^1.20.0", 26 | "raw-loader": "^4.0.1", 27 | "unescape": "^1.0.1" 28 | }, 29 | "peerDependencies": { 30 | "react": ">=16.0.0", 31 | "react-dom": ">=16.0.0" 32 | }, 33 | "devDependencies": { 34 | "babel-plugin-prismjs": "^2.0.1", 35 | "nwb": "0.25.x", 36 | "react": "^16.13.1", 37 | "react-dom": "^16.13.1" 38 | }, 39 | "author": "zhang yue", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/lumia2046/react-prism-editor.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/lumia2046/react-prism-editor/issues" 46 | }, 47 | "homepage": "https://lumia2046.github.io/react-prism-editor/", 48 | "license": "MIT", 49 | "keywords": [ 50 | "prismjs", 51 | "editor", 52 | "code", 53 | "code editor", 54 | "react component", 55 | "react prismjs" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/getPrismSource.js: -------------------------------------------------------------------------------- 1 | import 'clipboard' 2 | import 'prismjs/plugins/toolbar/prism-toolbar.css' 3 | import { themes, basicThemes } from './utils/themes' 4 | import { languages, basicLanguages } from './utils/languages' 5 | import { plugins, basicPlugins } from './utils/plugins' 6 | 7 | export const addCssParent = (parentSelector, cssStr) => { 8 | cssStr = cssStr.replace(/:not\(pre\) > code\[class\*="language-"\]/g, `${parentSelector} not(pre)code`) 9 | cssStr = cssStr.replace(/\.language-css \.token\.string/g, `${parentSelector} language-token`) 10 | cssStr = cssStr.replace(/\.style \.token\.string/g, `${parentSelector} style.string`) 11 | const keyArray = ['code\\[class\\*="language"\\]', 'code\\[class\\*="language-"\\]', 'pre\\[class\\*="language-"\\]', '\\.token\\.'] 12 | keyArray.forEach(item => { 13 | const name = item.replace(/\\/g, '') 14 | cssStr = cssStr.replace(new RegExp(item, 'g'), `${parentSelector} ${name}`) 15 | }) 16 | cssStr = cssStr.replace(new RegExp(`not\\(pre\\)code`, 'g'), ':not(pre) > code[class*="language-"]') 17 | cssStr = cssStr.replace(new RegExp(`language-token`, 'g'), '.language-css .token.string') 18 | cssStr = cssStr.replace(new RegExp(`style\\.string`, 'g'), '.style .token.string') 19 | return cssStr 20 | } 21 | 22 | const themesCss = {} 23 | themes.forEach(({ title, srcName }) => { 24 | themesCss[title] = require(`!!raw-loader!prismjs/themes/${srcName}.css`).default 25 | }) 26 | 27 | const languagesJs = {} 28 | basicLanguages.forEach(item => { 29 | languagesJs[item] = require(`!!raw-loader!prismjs/components/prism-${['html', 'vue', 'angular', 'xml'].includes(item) ? 'markup' : item}.min.js`).default 30 | }) 31 | 32 | const pluginsJs = {} 33 | // const pluginsCss = [] 34 | basicPlugins.forEach(item => { 35 | if (item === 'copy-to-clipboard') { 36 | //修改逻辑,让每次修改后都能拿到最新的值 37 | pluginsJs[item] = require(`!!raw-loader!./prism-${item}.js`).default 38 | //pluginsJs[item] = require(`!!raw-loader!prismjs/plugins/${item}/prism-${item}.js`).default 39 | } else { 40 | pluginsJs[item] = require(`!!raw-loader!prismjs/plugins/${item}/prism-${item}.min.js`).default 41 | } 42 | }) 43 | 44 | 45 | const lineNumbersCss = require(`!!raw-loader!prismjs/plugins/line-numbers/prism-line-numbers.css`).default 46 | const prismJs = require(`!!raw-loader!prismjs/prism.js`).default 47 | 48 | 49 | export { themesCss, lineNumbersCss, languagesJs, pluginsJs, prismJs } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import prism from "./utils/prism"; 4 | import escapeHtml from "escape-html"; 5 | import normalizeHtml from "./utils/normalizeHtml.js"; 6 | import htmlToPlain from "./utils/htmlToPlain.js"; 7 | import selectionRange from "./utils/selection-range.js"; 8 | import { getIndent, getDeindentLevel } from "./utils/getIndent"; 9 | import { FORBIDDEN_KEYS } from "./utils/constant"; 10 | import { basicThemes } from './utils/themes' 11 | import { basicLanguages } from './utils/languages' 12 | import { plugins } from './utils/plugins' 13 | import { themesCss, lineNumbersCss, languagesJs, pluginsJs, prismJs, addCssParent } from './getPrismSource' 14 | 15 | 16 | 17 | 18 | 19 | 20 | class Editor extends React.Component { 21 | constructor(props) { 22 | super(props) 23 | this.state = { 24 | lineNumbersHeight: '20px', 25 | selection: undefined, 26 | codeData: '', 27 | content: '', 28 | id: `${Math.random()}`.split('0.')[1] 29 | } 30 | } 31 | 32 | getLanguage(language) { 33 | if (basicLanguages.includes(language)) { 34 | return language 35 | } else { 36 | console.warn(`Provided languages:${basicLanguages}`) 37 | console.warn('Your input language is not support, use default language javascript') 38 | return 'javascript' 39 | } 40 | } 41 | 42 | getTheme(theme) { 43 | if (basicThemes.includes(theme)) { 44 | return theme 45 | } else { 46 | console.warn(`Provided themes:${basicThemes}`) 47 | console.warn('Your input theme is not support, use default theme') 48 | return 'default' 49 | } 50 | } 51 | 52 | 53 | 54 | handleClick(evt) { 55 | this.undoTimestamp = 0 56 | this.selection = selectionRange(this.pre); 57 | } 58 | getPlain() { 59 | if (this._innerHTML === this.pre.innerHTML) { 60 | return this._plain; 61 | } 62 | const plain = htmlToPlain(normalizeHtml(this.pre.innerHTML)); 63 | this._innerHTML = this.pre.innerHTML; 64 | this._plain = plain; 65 | return this._plain; 66 | } 67 | 68 | undoStack = [] 69 | undoOffset = 0 70 | lastPos = 0 71 | undoTimestamp = 0 72 | composing = false 73 | 74 | recordChange(plain, selection) { 75 | if (plain === this.undoStack[this.undoStack.length - 1]) { 76 | return; 77 | } 78 | 79 | if (this.undoOffset > 0) { 80 | this.undoStack = this.undoStack.slice(0, -this.undoOffset); 81 | this.undoOffset = 0; 82 | } 83 | 84 | const timestamp = Date.now(); 85 | const record = { plain, selection }; 86 | 87 | // Overwrite last record if threshold is not crossed 88 | if (timestamp - this.undoTimestamp < 3000) { 89 | this.undoStack[this.undoStack.length - 1] = record; 90 | } else { 91 | this.undoStack.push(record); 92 | 93 | if (this.undoStack.length > 50) { 94 | this.undoStack.shift(); 95 | } 96 | } 97 | 98 | this.undoTimestamp = timestamp; 99 | } 100 | 101 | restoreStackState(offset) { 102 | console.log(this.undoStack) 103 | const { plain, selection } = this.undoStack[ 104 | this.undoStack.length - 1 - offset 105 | ]; 106 | 107 | this.selection = selection; 108 | this.undoOffset = offset; 109 | this.updateContent(plain); 110 | } 111 | undo() { 112 | const offset = this.undoOffset + 1; 113 | if (offset >= this.undoStack.length) { 114 | return; 115 | } 116 | this.restoreStackState(offset); 117 | } 118 | redo() { 119 | const offset = this.undoOffset - 1; 120 | if (offset < 0) { 121 | return; 122 | } 123 | 124 | this.restoreStackState(offset); 125 | } 126 | handleKeyDown(evt) { 127 | if (evt.keyCode === 9 && !this.ignoreTabKey) { 128 | document.execCommand("insertHTML", false, " "); 129 | evt.preventDefault(); 130 | } else if (evt.keyCode === 8) { 131 | // Backspace Key 132 | const { start: cursorPos, end: cursorEndPos } = selectionRange( 133 | this.pre 134 | ); 135 | if (cursorPos !== cursorEndPos) { 136 | return; // Bail on selections 137 | } 138 | 139 | const deindent = getDeindentLevel(this.pre.innerText, cursorPos); 140 | if (deindent <= 0) { 141 | return; // Bail when deindent level defaults to 0 142 | } 143 | 144 | // Delete chars `deindent` times 145 | for (let i = 0; i < deindent; i++) { 146 | document.execCommand("delete", false); 147 | } 148 | 149 | evt.preventDefault(); 150 | } else if (evt.keyCode === 13) { 151 | // Enter Key 152 | const { start: cursorPos } = selectionRange(this.pre); 153 | const indentation = getIndent(this.pre.innerText, cursorPos); 154 | 155 | // https://stackoverflow.com/questions/35585421 156 | // add a space and remove it. it works :/ 157 | document.execCommand("insertHTML", false, "\n " + indentation); 158 | document.execCommand("delete", false); 159 | 160 | evt.preventDefault(); 161 | } else if ( 162 | // Undo / Redo 163 | evt.keyCode === 90 && 164 | evt.metaKey !== evt.ctrlKey && 165 | !evt.altKey 166 | ) { 167 | if (evt.shiftKey) { 168 | this.redo(); 169 | } else { 170 | this.undo(); 171 | } 172 | 173 | evt.preventDefault(); 174 | } 175 | } 176 | handleKeyUp(evt) { 177 | const keyupCode = evt.which; 178 | if (this.composing) { 179 | if (keyupCode === 13) { 180 | // finish inputting via IM. 181 | this.composing = false; 182 | } else { 183 | // now inputting words using IM. 184 | // must not update view. 185 | return; 186 | } 187 | } 188 | 189 | if (!this.code) { 190 | this.codeData = evt.target.innerText; 191 | } 192 | 193 | // if (this.emitEvents) { 194 | // this.$emit("keyup", evt); 195 | // } 196 | if ( 197 | evt.keyCode === 91 || // left cmd 198 | evt.keyCode === 93 || // right cmd 199 | evt.ctrlKey || 200 | evt.metaKey 201 | ) { 202 | return; 203 | } 204 | 205 | // Enter key 206 | if (evt.keyCode === 13) { 207 | this.undoTimestamp = 0; 208 | } 209 | 210 | this.selection = selectionRange(this.pre); 211 | 212 | if (!Object.values(FORBIDDEN_KEYS).includes(evt.keyCode)) { 213 | const plain = this.getPlain(); 214 | 215 | this.recordChange(plain, this.selection); 216 | this.updateContent(plain); 217 | } else { 218 | this.undoTimestamp = 0; 219 | } 220 | } 221 | 222 | getContent(codeData, language) { 223 | return prism(this.Prism, codeData || "", language); 224 | } 225 | 226 | onPaste = e => { 227 | e.preventDefault(); 228 | const currentCursorPos = selectionRange(this.pre); 229 | 230 | // get text representation of clipboard 231 | var text = (e.originalEvent || e).clipboardData.getData("Text"); 232 | // insert text manually 233 | document.execCommand("insertHTML", false, escapeHtml(text)); 234 | 235 | const newCursorPos = currentCursorPos.end + text.length; 236 | this.selection = { start: newCursorPos, end: newCursorPos }; 237 | 238 | const plain = this.getPlain(); 239 | this.recordChange(plain, this.selection); 240 | this.updateContent(plain); 241 | this.styleLineNumbers(); 242 | } 243 | 244 | componentWillUnmount() { 245 | this.pre.removeEventListener("paste", this.onPaste); 246 | } 247 | 248 | shouldComponentUpdate(nextProps, nextState) { 249 | if (this.props.code !== nextProps.code || this.props.language !== nextProps.language) { 250 | this.setPrismContent(nextProps.code, nextProps.language) 251 | } 252 | if (this.props.theme !== nextProps.theme) { 253 | this.setThemeStyle(nextProps.theme) 254 | this.setLineNumbersStyle(nextProps.theme) 255 | this.setState({}, this.styleLineNumbers) 256 | } 257 | return true 258 | } 259 | 260 | componentDidMount() { 261 | // document.addEventListener('DOMContentLoaded',()=>{ 262 | // console.log('DOMContentLoaded') 263 | // }) 264 | // const event = new CustomEvent("DOMContentLoaded", { 265 | // detail: { 266 | // hazcheeseburger: true 267 | // } 268 | // }) 269 | // setInterval(() => { 270 | // document.dispatchEvent(event) 271 | // }, 1000); 272 | 273 | const { theme, code, language } = this.props 274 | this.setThemeStyle(theme) 275 | this.setLineNumbersStyle(theme) 276 | this.setPrismScript() 277 | this.setPluginsScript() 278 | //php等语言需要先引入这个 279 | this.setLanguageScript('markup-templating') 280 | 281 | this.Prism = window.Prism 282 | this.setPrismContent(code, language) 283 | this.setState({}, () => { 284 | this.recordChange(this.getPlain()) 285 | }) 286 | 287 | 288 | 289 | 290 | this.undoTimestamp = 0; // Reset timestamp 291 | const $pre = this.pre; 292 | $pre.addEventListener("paste", this.onPaste); 293 | 294 | $pre.addEventListener("compositionstart", () => { 295 | this.composing = true; 296 | }); 297 | $pre.addEventListener("compositionend", () => { 298 | // for canceling input. 299 | this.composing = false; 300 | }); 301 | } 302 | 303 | componentWillUnmount() { 304 | this.addedDomNames.forEach(domName => { 305 | this[domName] && document.body.removeChild(this[domName]) 306 | }) 307 | } 308 | 309 | updateContent(plain) { 310 | const { changeCode, language } = this.props 311 | if (changeCode) { 312 | changeCode(plain) 313 | } 314 | this.setPrismContent(plain, language) 315 | // }, () => this.styleLineNumbers()) 316 | 317 | } 318 | 319 | addedDomNames = [] 320 | 321 | addElement(type, name, content) { 322 | const domName = `${name}-${type}-dom` 323 | if (document.querySelector(`#${domName}`)) { 324 | // if (this[domName] || window[domName]) { 325 | return 326 | //script必须每次通过appendChild的方式才会重新执行,所以先要remove掉 327 | document.body.removeChild(this[domName]) 328 | } else { 329 | this.addedDomNames.push(domName) 330 | } 331 | // window[domName] = true 332 | this[domName] = document.createElement(type) 333 | this[domName].id = domName 334 | this[domName].innerHTML = content 335 | document.body.appendChild(this[domName]) 336 | } 337 | 338 | setPrismContent(code, language) { 339 | // debugger 340 | // this.Prism = null 341 | // this.setPrismScript() 342 | this.setLanguageScript(language) 343 | 344 | // const event = new CustomEvent("DOMContentLoaded", { 345 | // detail: { 346 | // hazcheeseburger: true 347 | // } 348 | // }) 349 | // document.dispatchEvent(event) 350 | // setInterval(() => { 351 | // 352 | // }, 1000); 353 | // 354 | this.setState({ 355 | codeData: code || '', 356 | content: this.getContent(code, language) 357 | }, () => { 358 | // const container = this.pre.parentNode 359 | //去掉 toolbar,但是不能改container的className,否则每次keyUp光变都要消失 360 | // container.className = container.className.replace(/code-toolbar/g, '') 361 | //const toolbar = container.querySelector('.toolbar') 362 | // toolbar && container.removeChild(toolbar) 363 | 364 | //重新执行Prism的complete操作,linenumber,toobar等插件都在complete里挂载了回调方法 365 | this.Prism.hooks.run('complete', { language, code, element: this.pre.querySelector('code') }) 366 | // this.styleLineNumbers() 367 | }) 368 | 369 | } 370 | 371 | setThemeStyle(theme) { 372 | this.addElement('style', theme, addCssParent(`.module-theme-${theme}`, themesCss[theme])) 373 | } 374 | 375 | setLineNumbersStyle(theme) { 376 | this.addElement('style', `${theme}-lineNumbers`, addCssParent(`.module-theme-${theme}`, lineNumbersCss)) 377 | } 378 | 379 | setPrismScript() { 380 | this.addElement('script', 'prism', prismJs) 381 | } 382 | 383 | setPluginsScript() { 384 | Array.from(new Set(plugins.map(item => item.value))).forEach(item => { 385 | this.addElement('script', item, pluginsJs[item]) 386 | }) 387 | } 388 | 389 | 390 | setLanguageScript(language) { 391 | this.addElement('script', `${language}-language`, languagesJs[language]) 392 | // this.Prism.language = window.Prism.language 393 | // console.log(this.Prism) 394 | 395 | } 396 | 397 | componentDidUpdate() { 398 | if (this.selection) { 399 | selectionRange(this.pre, this.selection); 400 | } 401 | } 402 | 403 | // getLineNumbers() { 404 | // let totalLines = this.state.codeData.split(/\r\n|\n/).length; 405 | // // TODO: Find a better way of doing this - ignore last line break (os spesific etc.) 406 | // if (this.state.codeData.endsWith("\n")) { 407 | // totalLines--; 408 | // } 409 | // const lineNumbers = [] 410 | // for (let i = 0; i < totalLines; i++) { 411 | // lineNumbers.push(i) 412 | // } 413 | // return lineNumbers 414 | // } 415 | 416 | deletePx = (str = '') => parseInt(str.split('px')[0]) 417 | 418 | styleLineNumbers() { 419 | // clearTimeout(this.timeout) 420 | // if (this.props.lineNumber) { 421 | // this.timeout = setTimeout(() => { 422 | // const lineNumbers = this.getLineNumbers() 423 | // const reactLineNumbers = lineNumbers.map((item, i) => ) 424 | // const $editor = this.pre; 425 | // const $code = $editor.querySelector('code') 426 | // const editorStyles = window.getComputedStyle($editor); 427 | // let $lineNumbers = $code.querySelector('.line-numbers-rows') 428 | // if (!$lineNumbers) { 429 | // $lineNumbers = document.createElement('div') 430 | // $code.appendChild($lineNumbers) 431 | // $lineNumbers.className = "line-numbers-rows" 432 | // const btlr = "border-top-left-radius"; 433 | // const bblr = "border-bottom-left-radius"; 434 | // $lineNumbers.style[btlr] = editorStyles[btlr]; 435 | // $lineNumbers.style[bblr] = editorStyles[bblr]; 436 | // $editor.style[btlr] = 0; 437 | // $editor.style[bblr] = 0; 438 | // const stylesList = [ 439 | // // "background-color", 440 | // "font-family", 441 | // "font-size", 442 | // "line-height", 443 | // // "padding-top", 444 | // // "padding-bottom", 445 | // ]; 446 | // stylesList.forEach(style => { 447 | // $lineNumbers.style[style] = editorStyles[style]; 448 | // }); 449 | // } 450 | // $lineNumbers.innerHTML = renderToString(reactLineNumbers) 451 | // $editor.style['height'] = $lineNumbers.offsetHeight + 452 | // // this.deletePx(editorStyles["padding-top"]) + 453 | // // this.deletePx(editorStyles["padding-bottom"]) + 454 | // // this.deletePx(editorStyles["border-top-width"]) + 455 | // // this.deletePx(editorStyles["border-bottom-width"]) + 456 | // 'px' 457 | // }, 20) 458 | // } 459 | 460 | } 461 | 462 | 463 | 464 | 465 | render() { 466 | const { language, readOnly, theme, lineNumber, clipboard, showLanguage } = this.props 467 | const { content, lineNumbersHeight, id } = this.state 468 | return
469 | {/* {lineNumber &&
this.lineNumbersDom = ref} 472 | > 473 | {lineNumbers.map((item, i) => )} 477 |
} */} 478 |
 this.pre = ref}
481 |                 style={{ marginTop: 0 }}
482 |                 dangerouslySetInnerHTML={{ __html: content }}
483 |                 contentEditable={!readOnly}
484 |                 onKeyDown={this.handleKeyDown.bind(this)}
485 |                 onKeyUp={this.handleKeyUp.bind(this)}
486 |                 onClick={this.handleClick.bind(this)}
487 |                 spellCheck="false"
488 |                 autoCapitalize="off"
489 |                 autoComplete="off"
490 |                 autoCorrect="off"
491 |                 data-gramm="false"
492 |             />
493 |             
504 |             {/*  */}
505 |             {/*  */}
506 | 
507 |             {/*  */}
550 |         
551 | } 552 | } 553 | 554 | 555 | export default Editor; 556 | -------------------------------------------------------------------------------- /src/prism-copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof self === 'undefined' || !self.Prism || !self.document) { 3 | return; 4 | } 5 | 6 | if (!Prism.plugins.toolbar) { 7 | console.warn('Copy to Clipboard plugin loaded before Toolbar plugin.'); 8 | 9 | return; 10 | } 11 | 12 | var ClipboardJS = window.ClipboardJS || undefined; 13 | 14 | if (!ClipboardJS && typeof require === 'function') { 15 | ClipboardJS = require('clipboard'); 16 | } 17 | 18 | var callbacks = []; 19 | 20 | if (!ClipboardJS) { 21 | var script = document.createElement('script'); 22 | var head = document.querySelector('head'); 23 | 24 | script.onload = function () { 25 | ClipboardJS = window.ClipboardJS; 26 | 27 | if (ClipboardJS) { 28 | while (callbacks.length) { 29 | callbacks.pop()(); 30 | } 31 | } 32 | }; 33 | 34 | script.src = 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js'; 35 | head.appendChild(script); 36 | } 37 | 38 | Prism.plugins.toolbar.registerButton('copy-to-clipboard', function (env) { 39 | var linkCopy = document.createElement('button'); 40 | var clip = null 41 | linkCopy.textContent = 'Copy'; 42 | //add 43 | Prism.hooks.add("complete", function (event) { ClipboardJS && registerClipboard(event) }); 44 | 45 | if (!ClipboardJS) { 46 | callbacks.push(registerClipboard); 47 | } else { 48 | registerClipboard(); 49 | } 50 | 51 | return linkCopy; 52 | 53 | function registerClipboard(event) { 54 | try { clip && clip.destroy() } catch (e) { console.log(e) } 55 | clip = new ClipboardJS(linkCopy, { 56 | 'text': function () { 57 | return (event || env).code; 58 | } 59 | }); 60 | 61 | clip.on('success', function () { 62 | linkCopy.textContent = 'Copied!'; 63 | 64 | resetText(); 65 | }); 66 | clip.on('error', function () { 67 | linkCopy.textContent = 'Press Ctrl+C to copy'; 68 | 69 | resetText(); 70 | }); 71 | } 72 | 73 | function resetText() { 74 | setTimeout(function () { 75 | linkCopy.textContent = 'Copy'; 76 | }, 5000); 77 | } 78 | }); 79 | })(); 80 | -------------------------------------------------------------------------------- /src/utils/constant.js: -------------------------------------------------------------------------------- 1 | export const FORBIDDEN_KEYS = { 2 | shift: 16, 3 | ctrl: 17, 4 | alt: 18, 5 | pauseBreak: 19, 6 | capsLock: 20, 7 | esc: 27, 8 | pageUp: 33, 9 | pageDown: 34, 10 | end: 35, 11 | home: 36, 12 | arrowLeft: 37, 13 | arrowUp: 38, 14 | arrowRight: 39, 15 | arrowDown: 40, 16 | printScreen: 44, 17 | meta: 91, 18 | f1: 112, 19 | f2: 113, 20 | f3: 114, 21 | f4: 115, 22 | f5: 116, 23 | f6: 117, 24 | f7: 118, 25 | f8: 119, 26 | f9: 120, 27 | f10: 121, 28 | f11: 122, 29 | f12: 123, 30 | numLock: 144, 31 | scrollLock: 145 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/getIndent.js: -------------------------------------------------------------------------------- 1 | const getLine = (plain, cursorPos) => { 2 | const startSlice = plain.slice(0, cursorPos); 3 | const lastNewline = startSlice.lastIndexOf("\n") + 1; 4 | const lineSlice = startSlice.slice(lastNewline); 5 | return lineSlice; 6 | }; 7 | 8 | const indentRe = /^\s+/; 9 | 10 | export const getIndent = (plain, cursorPos) => { 11 | const line = getLine(plain, cursorPos); 12 | const matches = line.match(indentRe); 13 | if (matches === null) { 14 | return ""; 15 | } 16 | 17 | return matches[0] || ""; 18 | }; 19 | 20 | const deindentSpacesRe = /^(\t| {2})* {2}$/; 21 | 22 | export const getDeindentLevel = (plain, cursorPos) => { 23 | const line = getLine(plain, cursorPos); 24 | if (!deindentSpacesRe.test(line)) { 25 | return 0; // Doesn't match regex, so normal behaviour can apply 26 | } 27 | 28 | // The line contains only whitespace indentation 29 | // thus two characters must be deleted 30 | return 2; 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/htmlToPlain.js: -------------------------------------------------------------------------------- 1 | import unescape from "unescape"; 2 | 3 | const htmlToPlain = html => 4 | unescape(html.replace(/
/gm, "\n").replace(/<\/?[^>]*>/gm, "")); 5 | 6 | export default htmlToPlain; 7 | -------------------------------------------------------------------------------- /src/utils/languages.js: -------------------------------------------------------------------------------- 1 | export const basicLanguages = [ 2 | 'markup-templating', 3 | 'javascript', 'json', 'jsx', 'tsx', 'typescript', 'markup', 'html', 'vue', 4 | 'angular', 'css', 'sass', 'xml', 'java', 'php', 'csharp', 'c', 'cpp', 'sql','apex' 5 | ] 6 | 7 | export const languages = [ 8 | { title: 'MARKUP-TEM', value: 'markup-templating' }, 9 | { title: 'JS', value: 'javascript' }, 10 | { title: 'JSON', value: 'json' }, 11 | { title: 'JSX', value: 'jsx' }, 12 | { title: 'TXS', value: 'tsx' }, 13 | { title: 'TS', value: 'typescript' }, 14 | { title: 'MARKUP', value: 'markup' }, 15 | { title: 'HTML', value: 'html' }, 16 | { title: 'Vue', value: 'vue' }, 17 | { title: 'Angular', value: 'angular' }, 18 | { title: 'CSS', value: 'css' }, 19 | { title: 'SASS', value: 'sass' }, 20 | { title: 'XML', value: 'xml' }, 21 | { title: 'JAVA', value: 'java' }, 22 | { title: 'PHP', value: 'php' }, 23 | { title: 'C#', value: 'csharp' }, 24 | { title: 'C', value: 'c' }, 25 | { title: 'C++', value: 'cpp' }, 26 | { title: 'SQL', value: 'sql' }, 27 | { title: 'Apex', value: 'apex' }, 28 | ] 29 | -------------------------------------------------------------------------------- /src/utils/normalizeHtml.js: -------------------------------------------------------------------------------- 1 | const normalizeHtml = html => html.replace("\n", "
"); 2 | 3 | export default normalizeHtml; 4 | -------------------------------------------------------------------------------- /src/utils/plugins.js: -------------------------------------------------------------------------------- 1 | 2 | export const plugins = [ 3 | { title: 'toolbar', value: 'toolbar' }, 4 | { title: 'lineNumbers', value: 'line-numbers' }, 5 | { title: 'showLanguage', value: 'show-language' }, 6 | { title: 'clipboard', value: 'copy-to-clipboard' }, 7 | 8 | ] 9 | 10 | export const basicPlugins = ['toolbar', 'line-numbers', 'show-language', 'copy-to-clipboard'] -------------------------------------------------------------------------------- /src/utils/prism.js: -------------------------------------------------------------------------------- 1 | import escapeHtml from "escape-html"; 2 | // import 'prismjs' 3 | 4 | 5 | 6 | 7 | function wrap(code, lang, langPrism) { 8 | if (lang === "text") { 9 | code = escapeHtml(code); 10 | } 11 | return `${code}`; 12 | } 13 | 14 | export default (Prism, str, lang) => { 15 | // const Prism = window.Prism 16 | if (!lang) { 17 | return wrap(str, "text", "text"); 18 | } 19 | lang = lang.toLowerCase(); 20 | const rawLang = lang; 21 | if (Prism.languages[lang]) { 22 | // var myEvent = new Event('DOMContentLoaded'); 23 | // window.dispatchEvent(myEvent) 24 | // console.log(Prism) 25 | const code = Prism.highlight(str, Prism.languages[lang], lang); 26 | // console.log(code) 27 | // Prism.highlightAll() 28 | // {highlightedCode} 29 | 30 | return wrap(code, rawLang, lang); 31 | } 32 | return wrap(str, "text", "text"); 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/selection-range.js: -------------------------------------------------------------------------------- 1 | import iterator from "dom-iterator"; 2 | 3 | function position(el, pos) { 4 | if (document.activeElement !== el) return; 5 | var selection = window.getSelection(); 6 | 7 | if (1 == arguments.length) { 8 | if (!selection.rangeCount) return; 9 | var indexes = {}; 10 | var range = selection.getRangeAt(0); 11 | var clone = range.cloneRange(); 12 | clone.selectNodeContents(el); 13 | clone.setEnd(range.endContainer, range.endOffset); 14 | indexes.end = clone.toString().length; 15 | clone.setStart(range.startContainer, range.startOffset); 16 | indexes.start = indexes.end - clone.toString().length; 17 | indexes.atStart = clone.startOffset === 0; 18 | indexes.commonAncestorContainer = clone.commonAncestorContainer; 19 | indexes.endContainer = clone.endContainer; 20 | indexes.startContainer = clone.startContainer; 21 | return indexes; 22 | } 23 | 24 | var setSelection = pos.end && pos.end !== pos.start; 25 | var length = 0; 26 | // eslint-disable-next-line 27 | var range = document.createRange(); 28 | var it = iterator(el) 29 | .select(Node.TEXT_NODE) 30 | .revisit(false); 31 | var next; 32 | var startindex; 33 | var start = 34 | pos.start > el.textContent.length ? el.textContent.length : pos.start; 35 | var end = pos.end > el.textContent.length ? el.textContent.length : pos.end; 36 | var atStart = pos.atStart; 37 | 38 | while ((next = it.next())) { 39 | var olen = length; 40 | length += next.textContent.length; 41 | 42 | // Set start point of selection 43 | var atLength = atStart ? length > start : length >= start; 44 | if (!startindex && atLength) { 45 | startindex = true; 46 | range.setStart(next, start - olen); 47 | if (!setSelection) { 48 | range.collapse(true); 49 | makeSelection(el, range); 50 | break; 51 | } 52 | } 53 | 54 | // Set end point of selection 55 | if (setSelection && length >= end) { 56 | range.setEnd(next, end - olen); 57 | makeSelection(el, range); 58 | break; 59 | } 60 | } 61 | } 62 | 63 | function makeSelection(el, range) { 64 | var selection = window.getSelection(); 65 | el.focus(); 66 | selection.removeAllRanges(); 67 | selection.addRange(range); 68 | } 69 | 70 | export default position; 71 | -------------------------------------------------------------------------------- /src/utils/themes.js: -------------------------------------------------------------------------------- 1 | export const basicThemes = ['default', 'coy', 'dark', 'funky', 'okaidia', 'solarizedlight', 'tomorrow', 'twilight'] 2 | 3 | export const themes = basicThemes 4 | .map(item => { 5 | const result = { title: item, srcName: `prism-${item}` } 6 | if (item === 'default') { 7 | result.srcName = 'prism' 8 | } 9 | return result 10 | }) -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import {render, unmountComponentAtNode} from 'react-dom' 4 | 5 | import Component from 'src/' 6 | 7 | describe('Component', () => { 8 | let node 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(() => { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('displays a welcome message', () => { 19 | render(, node, () => { 20 | expect(node.innerHTML).toContain('Welcome to React components') 21 | }) 22 | }) 23 | }) 24 | --------------------------------------------------------------------------------