├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs └── index.html ├── index.js ├── package-lock.json ├── package.json ├── public └── index.html ├── rollup.config.js ├── src ├── button.js ├── constants.js ├── editor.js ├── icon.js ├── index.js ├── input.js ├── select.js ├── style.css └── toolbar.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "forceAllTransforms": true 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: fvilers 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | demo 64 | dist 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | docs 3 | public 4 | src 5 | .babelrc 6 | .gitignore 7 | .npmignore 8 | rollup.config.js 9 | webpack.*.js 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "node_modules": true, 4 | "package-lock.json": true, 5 | "demo": true, 6 | "dist": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fabian Vilers 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 | # tiny-editor 2 | 3 | A tiny HTML rich text editor written in vanilla JavaScript 4 | 5 | ## Goal 6 | 7 | Create a less than 5 Kb (compressed) library that enables a HTML element to be used as a rich text editor in plain old vanilla JavaScript. 8 | 9 | ## Support 10 | 11 | If you use and like this library, feel free to support my Open Source projects. 12 | 13 | [![donate](https://www.paypalobjects.com/en_US/BE/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=JZ26X897M9V9L¤cy_code=EUR) 14 | 15 | ## How to install 16 | 17 | ``` 18 | npm install tiny-editor 19 | ``` 20 | 21 | or load the bundle file directly at the end of your HTML document. 22 | 23 | ``` 24 | 25 | ``` 26 | 27 | ## How to use 28 | 29 | 1. Reference the editor library in your HTML document 30 | 2. Add a `data-tiny-editor` attribute to the HTML element you want to transform into an editor 31 | 32 | ## How to dynamically create an editor 33 | 34 | Use the exported function `window.__tinyEditor.transformToEditor()` which take as the first argument the DOM element (usually a `
`) that you want to transform to an editor. Refer to the `/public/index.html` for an example. 35 | 36 | ## How to extract the formatted text 37 | 38 | Listen for the `input` event on the editor HTML element. 39 | 40 | ``` 41 | document 42 | .querySelectorAll('[data-tiny-editor]') 43 | .forEach(editor => 44 | editor.addEventListener('input', e => console.log(e.target.innerHTML) 45 | ) 46 | ); 47 | ``` 48 | 49 | ## How to customize 50 | 51 | There are various options that can be used to customize how the Tiny Editor will be rendered. By default, every options are enabled. You can disable an option using data attributes. 52 | 53 | For example, you can remove the bold format button using the following attribute: 54 | 55 | ``` 56 |
57 | ``` 58 | 59 | ### Options 60 | 61 | - `data-formatblock="no"`: remove the styles drop down list 62 | - `data-bold="no"`: remove the bold button 63 | - `data-italic="no"`: : remove the italic button 64 | - `data-underline="no"`: remove the underline button 65 | - `data-fontname="no"`: remove the font drop down list 66 | - `data-forecolor="no"`: : remove the text color button 67 | - `data-justifyleft="no"`: remove the left align button 68 | - `data-justifycenter="no"`: remove the center align button 69 | - `data-justifyright="no"`: remove the right align button 70 | - `data-insertorderedlist="no"`: remove the numbered list button 71 | - `data-insertunorderedlist="no"`: remove the bulleted list button 72 | - `data-outdent="no"`: remove the decrease indent button 73 | - `data-indent="no"`: remove the increase indent button 74 | - `data-remove-format="no"`: remove the clear formatting button 75 | - `data-autofocus="no"`: remove autofocus from the editor 76 | 77 | ## Supported browsers 78 | 79 | Modern browser (Chrome, Firefox, Edge,...) are supported. Tiny Editor doesn't work on Internet Explorer. 80 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Tiny Editor 10 | 15 | 21 | 22 | 23 | 24 |

Tiny Editor demo

25 |
26 |

27 | Lorem ipsum dolor sit amet, no dictas mollis definiebas cum, duo dolor 28 | quodsi ei. Usu veniam honestatis eu. Iracundia instructior ad mea, eu 29 | eos nostro corrumpit cotidieque. Iracundia urbanitas signiferumque id 30 | usu, ex adversarium consequuntur definitionem quo. An per vituperata 31 | suscipiantur, graece persecuti eum in. 32 |

33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/bundle.js'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-editor", 3 | "version": "0.5.0", 4 | "description": "A tiny HTML rich text editor written in vanilla JavaScript", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "start": "webpack-dev-server --open", 8 | "analyze": "npm run build && source-map-explorer dist/bundle.js", 9 | "version": "npm run build", 10 | "postversion": "git push && git push --tags && npm publish" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/fvilers/tiny-editor.git" 15 | }, 16 | "keywords": [ 17 | "tiny", 18 | "editor", 19 | "wysiwyg", 20 | "html", 21 | "vanilla", 22 | "javascript" 23 | ], 24 | "author": "Fabian Vilers (https://www.dev-one.com/)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/fvilers/tiny-editor/issues" 28 | }, 29 | "homepage": "https://fvilers.github.io/tiny-editor", 30 | "devDependencies": { 31 | "@babel/core": "^7.10.5", 32 | "@babel/preset-env": "^7.10.4", 33 | "babel-plugin-transform-import-styles": "0.0.11", 34 | "babel-template": "^6.26.0", 35 | "clean-webpack-plugin": "0.1.19", 36 | "css-loader": "6.7.3", 37 | "html-webpack-plugin": "5.5.0", 38 | "load-styles": "^2.0.0", 39 | "rollup": "^3.29.5", 40 | "rollup-plugin-babel": "^4.4.0", 41 | "rollup-plugin-postcss": "^4.0.2", 42 | "rollup-plugin-uglify": "^6.0.4", 43 | "source-map-explorer": "^1.6.0", 44 | "style-loader": "3.3.1", 45 | "webpack": "^5.75.0", 46 | "webpack-cli": "^5.0.1", 47 | "webpack-dev-server": "^5.2.1", 48 | "webpack-merge": "4.1.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Tiny Editor 10 | 15 | 16 | 17 | 18 |

Tiny Editor demo

19 |

Complete toolbar

20 |
21 |

22 | Lorem ipsum dolor sit amet, no dictas mollis 23 | definiebas cum, duo dolor quodsi ei. Usu veniam honestatis 24 | eu. Iracundia instructior ad mea, eu eos nostro corrumpit cotidieque. 25 | Iracundia urbanitas signiferumque id usu, ex adversarium consequuntur 26 | definitionem quo. An per vituperata suscipiantur, graece persecuti eum 27 | in. 28 |

29 |
30 | 31 |

Custom toolbar

32 |
47 |

48 | Lorem ipsum dolor sit amet, no dictas mollis 49 | definiebas cum, duo dolor quodsi ei. Usu veniam honestatis 50 | eu. Iracundia instructior ad mea, eu eos nostro corrumpit cotidieque. 51 | Iracundia urbanitas signiferumque id usu, ex adversarium consequuntur 52 | definitionem quo. An per vituperata suscipiantur, graece persecuti eum 53 | in. 54 |

55 |
56 | 57 |

Dynamically created

58 | 59 | 60 | 61 | 62 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const postcss = require('rollup-plugin-postcss'); 2 | const babel = require('rollup-plugin-babel'); 3 | const uglify = require('rollup-plugin-uglify'); 4 | 5 | module.exports = { 6 | input: 'src/index.js', 7 | output: { 8 | file: 'dist/bundle.js', 9 | format: 'iife', 10 | sourcemap: true 11 | }, 12 | plugins: [ 13 | postcss({ 14 | plugins: [] 15 | }), 16 | babel({ 17 | exclude: 'node_modules/**' 18 | }), 19 | uglify.uglify() 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /src/button.js: -------------------------------------------------------------------------------- 1 | import { BEFORE_END, TOOLBAR_ITEM } from "./constants"; 2 | 3 | export const createButton = (commandId, title, children, execCommand) => { 4 | const button = document.createElement("button"); 5 | button.dataset.commandId = commandId; 6 | button.className = TOOLBAR_ITEM; 7 | button.title = title; 8 | button.type = "button"; 9 | button.insertAdjacentElement(BEFORE_END, children); 10 | button.addEventListener("click", () => execCommand(commandId)); 11 | 12 | return button; 13 | }; 14 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const BEFORE_BEGIN = "beforebegin"; 2 | export const BEFORE_END = "beforeend"; 3 | export const TOOLBAR_ITEM = "__toolbar-item"; 4 | export const NO = "no"; 5 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | import { BEFORE_BEGIN, NO } from "./constants"; 2 | import { createToolbar } from "./toolbar"; 3 | 4 | const rgbToHex = (color) => { 5 | const digits = /(.*?)rgb\((\d+), (\d+), (\d+)\)/.exec(color); 6 | const red = parseInt(digits[2]); 7 | const green = parseInt(digits[3]); 8 | const blue = parseInt(digits[4]); 9 | const rgb = blue | (green << 8) | (red << 16); 10 | 11 | return digits[1] + "#" + rgb.toString(16).padStart(6, "0"); 12 | }; 13 | 14 | export const transformToEditor = (editor) => { 15 | // Indicate that the element is editable 16 | editor.setAttribute("contentEditable", true); 17 | 18 | // Add a custom class 19 | editor.className = "__editor"; 20 | 21 | // Create an exec command function 22 | const execCommand = (commandId, value) => { 23 | document.execCommand(commandId, false, value); 24 | }; 25 | 26 | // Set default paragraph to

27 | execCommand("defaultParagraphSeparator", "p"); 28 | 29 | // Create a toolbar 30 | const toolbar = createToolbar(editor.dataset, execCommand); 31 | editor.insertAdjacentElement(BEFORE_BEGIN, toolbar); 32 | 33 | // Listen for events to detect where the caret is 34 | const updateActiveState = () => { 35 | const toolbarSelects = toolbar.querySelectorAll("select[data-command-id]"); 36 | for (const select of toolbarSelects) { 37 | const value = document.queryCommandValue(select.dataset.commandId); 38 | const option = Array.from(select.options).find( 39 | (option) => option.value === value 40 | ); 41 | select.selectedIndex = option ? option.index : -1; 42 | } 43 | 44 | const toolbarButtons = toolbar.querySelectorAll("button[data-command-id]"); 45 | for (const button of toolbarButtons) { 46 | const active = document.queryCommandState(button.dataset.commandId); 47 | button.classList.toggle("active", active); 48 | } 49 | 50 | const inputButtons = toolbar.querySelectorAll("input[data-command-id]"); 51 | for (const input of inputButtons) { 52 | const value = document.queryCommandValue(input.dataset.commandId); 53 | input.value = rgbToHex(value); 54 | } 55 | }; 56 | 57 | // Give focus 58 | if (editor.dataset.autofocus !== NO) { 59 | editor.focus(); 60 | } 61 | 62 | editor.addEventListener("keydown", updateActiveState); 63 | editor.addEventListener("keyup", updateActiveState); 64 | editor.addEventListener("click", updateActiveState); 65 | toolbar.addEventListener("click", updateActiveState); 66 | }; 67 | -------------------------------------------------------------------------------- /src/icon.js: -------------------------------------------------------------------------------- 1 | export const createIcon = (className) => { 2 | const icon = document.createElement("i"); 3 | icon.className = className; 4 | 5 | return icon; 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { transformToEditor } from "./editor"; 2 | import "./style.css"; 3 | 4 | document.querySelectorAll("[data-tiny-editor]").forEach(transformToEditor); 5 | 6 | window.__tinyEditor = { 7 | transformToEditor, 8 | }; 9 | -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | import { TOOLBAR_ITEM } from "./constants"; 2 | 3 | export const createInput = (commandId, title, type, execCommand) => { 4 | const input = document.createElement("input"); 5 | input.dataset.commandId = commandId; 6 | input.className = TOOLBAR_ITEM; 7 | input.title = title; 8 | input.type = type; 9 | input.addEventListener("change", (e) => 10 | execCommand(commandId, e.target.value) 11 | ); 12 | 13 | return input; 14 | }; 15 | -------------------------------------------------------------------------------- /src/select.js: -------------------------------------------------------------------------------- 1 | import { BEFORE_END, TOOLBAR_ITEM } from "./constants"; 2 | 3 | const createOption = (value, text, selected) => { 4 | const option = document.createElement("option"); 5 | option.innerText = text; 6 | 7 | if (value) { 8 | option.setAttribute("value", value); 9 | } 10 | 11 | if (selected) { 12 | option.setAttribute("selected", selected); 13 | } 14 | 15 | return option; 16 | }; 17 | 18 | export const createSelect = (commandId, title, options, execCommand) => { 19 | const select = document.createElement("select"); 20 | select.dataset.commandId = commandId; 21 | select.className = TOOLBAR_ITEM; 22 | select.title = title; 23 | select.addEventListener("change", (e) => 24 | execCommand(commandId, e.target.options[e.target.selectedIndex].value) 25 | ); 26 | 27 | for (const option of options) { 28 | select.insertAdjacentElement( 29 | BEFORE_END, 30 | createOption(option.value, option.text, option.selected) 31 | ); 32 | } 33 | 34 | return select; 35 | }; 36 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .__editor { 2 | background: #ffffff; 3 | border: solid 1px #e0e0e0; 4 | color: #000000; 5 | font-family: "serif"; 6 | margin-top: 10px; 7 | overflow: scroll; 8 | padding: 10px; 9 | } 10 | 11 | .__editor:focus { 12 | outline: none; 13 | } 14 | 15 | .__toolbar { 16 | display: flex; 17 | flex-wrap: wrap; 18 | padding: 5px; 19 | } 20 | 21 | .__toolbar-item { 22 | background: #ffffff; 23 | border: 0; 24 | border-radius: 3px; 25 | cursor: pointer; 26 | margin-right: 7px; 27 | min-width: 30px; 28 | padding: 5px; 29 | } 30 | 31 | .__toolbar-item:hover, 32 | .__toolbar-item.active { 33 | background: #f0f0f0; 34 | } 35 | 36 | .__toolbar-separator { 37 | border-left: solid 1px #e0e0e0; 38 | margin-right: 7px; 39 | } 40 | 41 | .__toolbar-separator:last-child { 42 | display: none; 43 | } 44 | 45 | .tes { 46 | font-style: normal; 47 | } 48 | 49 | .te-bold::before { 50 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M6.8 19V5h5.525q1.625 0 3 1T16.7 8.775q0 1.275-.575 1.963t-1.075.987q.625.275 1.388 1.025T17.2 15q0 2.225-1.625 3.113t-3.05.887zm3.025-2.8h2.6q1.2 0 1.463-.612t.262-.888t-.262-.887t-1.538-.613H9.825zm0-5.7h2.325q.825 0 1.2-.425t.375-.95q0-.6-.425-.975t-1.1-.375H9.825z'/%3E%3C/svg%3E"); 51 | font-weight: bold; 52 | } 53 | 54 | .te-italic::before { 55 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M5 19v-2.5h4l3-9H8V5h10v2.5h-3.5l-3 9H15V19z'/%3E%3C/svg%3E"); 56 | font-style: italic; 57 | } 58 | 59 | .te-underline::before { 60 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M5 21v-2h14v2zm7-4q-2.525 0-3.925-1.575t-1.4-4.175V3H9.25v8.4q0 1.4.7 2.275t2.05.875t2.05-.875t.7-2.275V3h2.575v8.25q0 2.6-1.4 4.175T12 17'/%3E%3C/svg%3E"); 61 | text-decoration: underline; 62 | } 63 | 64 | .te-align-left::before { 65 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M3 21v-2h18v2zm0-4v-2h12v2zm0-4v-2h18v2zm0-4V7h12v2zm0-4V3h18v2z'/%3E%3C/svg%3E"); 66 | } 67 | 68 | .te-align-center::before { 69 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M3 21v-2h18v2zm4-4v-2h10v2zm-4-4v-2h18v2zm4-4V7h10v2zM3 5V3h18v2z'/%3E%3C/svg%3E"); 70 | } 71 | 72 | .te-align-right::before { 73 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M3 5V3h18v2zm6 4V7h12v2zm-6 4v-2h18v2zm6 4v-2h12v2zm-6 4v-2h18v2z'/%3E%3C/svg%3E"); 74 | } 75 | 76 | .te-list-ol::before { 77 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M3 22v-1.5h2.5v-.75H4v-1.5h1.5v-.75H3V16h3q.425 0 .713.288T7 17v1q0 .425-.288.713T6 19q.425 0 .713.288T7 20v1q0 .425-.288.713T6 22zm0-7v-2.75q0-.425.288-.712T4 11.25h1.5v-.75H3V9h3q.425 0 .713.288T7 10v1.75q0 .425-.288.713T6 12.75H4.5v.75H7V15zm1.5-7V3.5H3V2h3v6zM9 19v-2h12v2zm0-6v-2h12v2zm0-6V5h12v2z'/%3E%3C/svg%3E"); 78 | } 79 | 80 | .te-list-ul::before { 81 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M9 19v-2h12v2zm0-6v-2h12v2zm0-6V5h12v2zM5 20q-.825 0-1.412-.587T3 18t.588-1.412T5 16t1.413.588T7 18t-.587 1.413T5 20m0-6q-.825 0-1.412-.587T3 12t.588-1.412T5 10t1.413.588T7 12t-.587 1.413T5 14m0-6q-.825 0-1.412-.587T3 6t.588-1.412T5 4t1.413.588T7 6t-.587 1.413T5 8'/%3E%3C/svg%3E"); 82 | white-space: pre-wrap; 83 | display: inline-block; 84 | line-height: 0.5; 85 | } 86 | 87 | .te-indent::before { 88 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M3 21v-2h18v2zm8-4v-2h10v2zm0-4v-2h10v2zm0-4V7h10v2zM3 5V3h18v2zm0 11V8l4 4z'/%3E%3C/svg%3E"); 89 | white-space: pre-wrap; 90 | display: inline-block; 91 | line-height: 0.5; 92 | } 93 | 94 | .te-indent.te-flip-horizontal::before { 95 | transform: rotate(180deg); 96 | } 97 | 98 | .te-eraser::before { 99 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='m13.2 10.35l-2.325-2.325L7.85 5H20v3h-5.8zm6.6 12.25l-8.3-8.3l-2 4.7H6.225L9.2 12L1.4 4.2l1.4-1.4l18.4 18.4z'/%3E%3C/svg%3E"); 100 | white-space: pre-wrap; 101 | display: inline-block; 102 | line-height: 0.5; 103 | } 104 | -------------------------------------------------------------------------------- /src/toolbar.js: -------------------------------------------------------------------------------- 1 | import { createButton } from "./button"; 2 | import { BEFORE_END, NO } from "./constants"; 3 | import { createIcon } from "./icon"; 4 | import { createInput } from "./input"; 5 | import { createSelect } from "./select"; 6 | 7 | const createSeparator = () => { 8 | const separator = document.createElement("span"); 9 | separator.className = "__toolbar-separator"; 10 | 11 | return separator; 12 | }; 13 | 14 | export const createToolbar = (options, execCommand) => { 15 | const toolbar = document.createElement("div"); 16 | toolbar.className = "__toolbar"; 17 | 18 | // Styles 19 | if (options.formatblock !== NO) { 20 | toolbar.insertAdjacentElement( 21 | BEFORE_END, 22 | createSelect( 23 | "formatblock", 24 | "Styles", 25 | [ 26 | { value: "h1", text: "Title 1" }, 27 | { value: "h2", text: "Title 2" }, 28 | { value: "h3", text: "Title 3" }, 29 | { value: "h4", text: "Title 4" }, 30 | { value: "h5", text: "Title 5" }, 31 | { value: "h6", text: "Title 6" }, 32 | { value: "p", text: "Paragraph", selected: true }, 33 | { value: "pre", text: "Preformatted" }, 34 | ], 35 | execCommand 36 | ) 37 | ); 38 | } 39 | 40 | // Font 41 | if (options.fontname !== NO) { 42 | toolbar.insertAdjacentElement( 43 | BEFORE_END, 44 | createSelect( 45 | "fontname", 46 | "Font", 47 | [ 48 | { value: "serif", text: "Serif", selected: true }, 49 | { value: "sans-serif", text: "Sans Serif" }, 50 | { value: "monospace", text: "Monospace" }, 51 | { value: "cursive", text: "Cursive" }, 52 | { value: "fantasy", text: "Fantasy" }, 53 | ], 54 | execCommand 55 | ) 56 | ); 57 | } 58 | 59 | // Bold 60 | if (options.bold !== NO) { 61 | toolbar.insertAdjacentElement( 62 | BEFORE_END, 63 | createButton("bold", "Bold", createIcon("tes te-bold"), execCommand) 64 | ); 65 | } 66 | 67 | // Italic 68 | if (options.italic !== NO) { 69 | toolbar.insertAdjacentElement( 70 | BEFORE_END, 71 | createButton("italic", "Italic", createIcon("tes te-italic"), execCommand) 72 | ); 73 | } 74 | 75 | // Underline 76 | if (options.underline !== NO) { 77 | toolbar.insertAdjacentElement( 78 | BEFORE_END, 79 | createButton( 80 | "underline", 81 | "Underline", 82 | createIcon("tes te-underline"), 83 | execCommand 84 | ) 85 | ); 86 | } 87 | 88 | // Text color 89 | if (options.forecolor !== NO) { 90 | toolbar.insertAdjacentElement( 91 | BEFORE_END, 92 | createInput("forecolor", "Text color", "color", execCommand) 93 | ); 94 | } 95 | 96 | // Separator 97 | toolbar.insertAdjacentElement(BEFORE_END, createSeparator()); 98 | 99 | // Left align 100 | if (options.justifyleft !== NO) { 101 | toolbar.insertAdjacentElement( 102 | BEFORE_END, 103 | createButton( 104 | "justifyleft", 105 | "Left align", 106 | createIcon("tes te-align-left"), 107 | execCommand 108 | ) 109 | ); 110 | } 111 | 112 | // Center align 113 | if (options.justifycenter !== NO) { 114 | toolbar.insertAdjacentElement( 115 | BEFORE_END, 116 | createButton( 117 | "justifycenter", 118 | "Center align", 119 | createIcon("tes te-align-center"), 120 | execCommand 121 | ) 122 | ); 123 | } 124 | 125 | // Right align 126 | if (options.justifyright !== NO) { 127 | toolbar.insertAdjacentElement( 128 | BEFORE_END, 129 | createButton( 130 | "justifyright", 131 | "Right align", 132 | createIcon("tes te-align-right"), 133 | execCommand 134 | ) 135 | ); 136 | } 137 | 138 | // Separator 139 | toolbar.insertAdjacentElement(BEFORE_END, createSeparator()); 140 | 141 | // Numbered list 142 | if (options.insertorderedlist !== NO) { 143 | toolbar.insertAdjacentElement( 144 | BEFORE_END, 145 | createButton( 146 | "insertorderedlist", 147 | "Numbered list", 148 | createIcon("tes te-list-ol"), 149 | execCommand 150 | ) 151 | ); 152 | } 153 | 154 | // Bulleted list 155 | if (options.insertunorderedlist !== NO) { 156 | toolbar.insertAdjacentElement( 157 | BEFORE_END, 158 | createButton( 159 | "insertunorderedlist", 160 | "Bulleted list", 161 | createIcon("tes te-list-ul"), 162 | execCommand 163 | ) 164 | ); 165 | } 166 | 167 | // Decrease indent 168 | if (options.outdent !== NO) { 169 | toolbar.insertAdjacentElement( 170 | BEFORE_END, 171 | createButton( 172 | "outdent", 173 | "Decrease indent", 174 | createIcon("tes te-indent fa-flip-horizontal"), 175 | execCommand 176 | ) 177 | ); 178 | } 179 | 180 | // Increase indent 181 | if (options.indent !== NO) { 182 | toolbar.insertAdjacentElement( 183 | BEFORE_END, 184 | createButton( 185 | "indent", 186 | "Increase indent", 187 | createIcon("tes te-indent"), 188 | execCommand 189 | ) 190 | ); 191 | } 192 | 193 | // Separator 194 | toolbar.insertAdjacentElement(BEFORE_END, createSeparator()); 195 | 196 | // Clear formatting 197 | if (options.removeFormat !== NO) { 198 | toolbar.insertAdjacentElement( 199 | BEFORE_END, 200 | createButton( 201 | "removeFormat", 202 | "Clear formatting", 203 | createIcon("tes te-eraser"), 204 | execCommand 205 | ) 206 | ); 207 | } 208 | 209 | return toolbar; 210 | }; 211 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 4 | const webpack = require("webpack"); 5 | 6 | module.exports = { 7 | entry: "./src/index.js", 8 | plugins: [ 9 | new CleanWebpackPlugin(["demo"]), 10 | new HtmlWebpackPlugin({ 11 | template: "public/index.html", 12 | }), 13 | new webpack.HotModuleReplacementPlugin(), 14 | ], 15 | output: { 16 | filename: "main.js", 17 | path: path.resolve(__dirname, "demo"), 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/, 23 | use: ["style-loader", "css-loader"], 24 | }, 25 | ], 26 | }, 27 | devtool: "inline-source-map", 28 | devServer: { 29 | hot: true, 30 | }, 31 | mode: "development", 32 | }; 33 | --------------------------------------------------------------------------------