├── .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 | [](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 `
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 |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 |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 |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 | --------------------------------------------------------------------------------