├── .github └── workflows │ ├── demos.yml │ └── deploy.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── demos ├── build.sh ├── demos-index.html ├── header-text.js ├── javascript │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ └── index.js │ └── webpack.config.js ├── package.json ├── react │ ├── .babelrc │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── app.jsx │ │ └── index.jsx │ └── webpack.config.js ├── script-tags │ ├── demo-quill-1.x.html │ └── demo-quill-2.x.html ├── typescript │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.ts │ │ └── setup.js │ ├── tsconfig.json │ └── webpack.config.js └── vue │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── app.vue │ └── index.js │ └── webpack.config.js ├── jest.config.js ├── package.json ├── src ├── html-formatter.ts ├── html-parser.spec.ts ├── html-parser.ts ├── index.ts ├── logger.ts ├── options.ts ├── quill.htmlEditButton.ts └── styles.css ├── tsconfig.json └── webpack.config.js /.github/workflows/demos.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Build and Publish Demos 7 | jobs: 8 | demo: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 20.12.1 15 | - name: Cache node_modules 16 | id: cache-modules 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: ${{ runner.OS }}-build-${{ hashFiles('package.json') }} 21 | - name: Install Dependencies 22 | if: steps.cache-modules.outputs.cache-hit != 'true' 23 | run: yarn 24 | - name: Build lib 25 | run: yarn build 26 | - name: Build demo folder 27 | run: | 28 | cd demos 29 | yarn 30 | ./build.sh 31 | find ./dist 32 | - name: Deploy 🚀 33 | uses: JamesIves/github-pages-deploy-action@releases/v3 34 | with: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | BRANCH: gh-pages 37 | FOLDER: demos/dist 38 | CLEAN: true 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Build and Publish Library 7 | jobs: 8 | build: 9 | if: "!contains(github.event.head_commit.message, 'no-ci')" 10 | name: Deploy Library 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 20.12.1 17 | - name: Cache node_modules 18 | id: cache-modules 19 | uses: actions/cache@v1 20 | with: 21 | path: node_modules 22 | key: ${{ runner.OS }}-build-${{ hashFiles('package.json') }} 23 | - name: Install Dependencies 24 | if: steps.cache-modules.outputs.cache-hit != 'true' 25 | run: yarn 26 | - name: Test Library 27 | run: yarn test 28 | - name: Build Library 29 | run: yarn build 30 | - name: Publish 31 | uses: Github-Actions-Community/merge-release@22d66d3f7750d57b2b8c05db6077205332527df8 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | dist 4 | types 5 | *.DS_STORE 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .github 3 | node_modules 4 | yarn.lock 5 | webpack.config.js 6 | tsconfig.json 7 | jest.config.js 8 | demos 9 | *.tgz 10 | 11 | !dist 12 | !types 13 | !src 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ben Winding 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quill-html-edit-button 2 | 3 | 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/quill-html-edit-button.svg)](https://www.npmjs.com/package/quill-html-edit-button) 6 | [![License](https://img.shields.io/npm/l/quill-html-edit-button.svg)](https://github.com/benwinding/quill-html-edit-button/blob/master/LICENSE) 7 | [![Downloads/week](https://img.shields.io/npm/dm/quill-html-edit-button.svg)](https://www.npmjs.com/package/quill-html-edit-button) 8 | [![Github Issues](https://img.shields.io/github/issues/benwinding/quill-html-edit-button.svg)](https://github.com/benwinding/quill-html-edit-button) 9 | ![Build and Publish](https://github.com/benwinding/quill-html-edit-button/actions/workflows/deploy.yml/badge.svg) 10 | 11 | 12 | 13 | Quill.js Module which allows you to quickly view/edit the HTML in the editor 14 | 15 | ![Demo](https://user-images.githubusercontent.com/664714/93285035-f7f44e80-f7a1-11ea-83c7-59e151c53c06.gif) 16 | 17 | - [Live Demo (webpack javascript)](https://benwinding.github.io/quill-html-edit-button/javascript/) - also > [Source Code](https://github.com/benwinding/quill-html-edit-button/tree/master/demos/javascript) 18 | - [Live Demo (webpack typescript)](https://benwinding.github.io/quill-html-edit-button/typescript/) - also > [Source Code](https://github.com/benwinding/quill-html-edit-button/tree/master/demos/typescript) 19 | - [Live Demo (webpack vue)](https://benwinding.github.io/quill-html-edit-button/vue/) - also > [Source Code](https://github.com/benwinding/quill-html-edit-button/tree/master/demos/vue) 20 | - [Live Demo (script tags quill-1.x)](https://benwinding.github.io/quill-html-edit-button/script-tags/demo-quill-1.x.html) - also > [Source Code](https://github.com/benwinding/quill-html-edit-button/tree/master/demos/script-tags/demo-quill-1.x.html) 21 | - [Live Demo (script tags quill-2.x) With Tables!](https://benwinding.github.io/quill-html-edit-button/script-tags/demo-quill-2.x.html) - also > [Source Code](https://github.com/benwinding/quill-html-edit-button/tree/master/demos/script-tags/demo-quill-2.x.html) 22 | 23 | ## Install 24 | 25 | `yarn add quill-html-edit-button` 26 | 27 | ## Quickstart (Javascript) 28 | 29 | ``` js 30 | import Quill from 'quill/core'; 31 | import Toolbar from 'quill/modules/toolbar'; 32 | import Snow from 'quill/themes/snow'; 33 | 34 | import htmlEditButton from "quill-html-edit-button"; 35 | 36 | Quill.register({ 37 | 'modules/toolbar': Toolbar, 38 | 'themes/snow': Snow, 39 | "modules/htmlEditButton": htmlEditButton 40 | }) 41 | 42 | const quill = new Quill(editor, { 43 | // ... 44 | modules: { 45 | // ... 46 | htmlEditButton: {} 47 | } 48 | }); 49 | ``` 50 | 51 | ## Quickstart (typescript) 52 | 53 | Due to Quill's implementation, typescript integration is not trivial: 54 | 55 | - Follow the demo example here [`demos/typescript/src/index.ts`](https://github.com/benwinding/quill-html-edit-button/blob/master/demos/typescript/src/index.ts) 56 | - The file [`setup.js`](https://github.com/benwinding/quill-html-edit-button/blob/master/demos/typescript/src/setup.js`) is to use the library without types (as they aren't implemented with quill modules). 57 | - Your `tsconfig.json` needs the following properties, to prevent errors: 58 | ``` json 59 | "compilerOptions": { 60 | "allowJs": true, 61 | "checkJs": false 62 | } 63 | ``` 64 | 65 | 66 | ## Quickstart (script tag) 67 | 68 | ``` html 69 | 70 | 71 | 81 | ``` 82 | 83 | ## Options 84 | 85 | ``` js 86 | modules: { 87 | // ... 88 | htmlEditButton: { 89 | debug: true, // logging, default:false 90 | msg: "Edit the content in HTML format", //Custom message to display in the editor, default: Edit HTML here, when you click "OK" the quill editor's contents will be replaced 91 | okText: "Ok", // Text to display in the OK button, default: Ok, 92 | cancelText: "Cancel", // Text to display in the cancel button, default: Cancel 93 | buttonHTML: "<>", // Text to display in the toolbar button, default: <> 94 | buttonTitle: "Show HTML source", // Text to display as the tooltip for the toolbar button, default: Show HTML source 95 | syntax: false, // Show the HTML with syntax highlighting. Requires highlightjs on window.hljs (similar to Quill itself), default: false 96 | prependSelector: 'div#myelement', // a string used to select where you want to insert the overlayContainer, default: null (appends to body), 97 | editorModules: {} // The default mod 98 | } 99 | } 100 | ``` 101 | 102 | ### Syntax Highlighting 103 | 104 | By default syntax highlighting is off, if you want to enable it use `syntax: true` in the options (as shown above) and make sure you add the following script tags: 105 | 106 | ``` html 107 | 111 | 112 | 116 | ``` 117 | 118 | Alternatively, include these scripts in your package bundler, as long as highlightjs is available in the global space at `window.hljs`. 119 | 120 | ## Customising The HTML Editor 121 | The editor itself is actually a Quill Editor instance too! So you can pass in custom modules like this: 122 | 123 | ``` ts 124 | // options 125 | htmlEditButton: { 126 | // Flags 127 | debug?: boolean; // default: false 128 | syntax?: boolean; // default: false 129 | // Overlay 130 | closeOnClickOverlay: boolean; // default: true 131 | prependSelector: string; // default: null 132 | // Labels 133 | buttonHTML?: string; // default: "<>" 134 | buttonTitle?: string; // default: "Show HTML source" 135 | msg: string; // default: 'Edit HTML here, when you click "OK" the quill editor\'s contents will be replaced' 136 | okText: string; // default: "Ok" 137 | cancelText: string; // default: "Cancel" 138 | // Quill Modules (for the HTML editor) 139 | editorModules?: { // default: null 140 | // Any modules here will be declared in HTML quill editor instance 141 | keyboard: { 142 | bindings: { 143 | custom: { 144 | key: 'a', 145 | handler: function(range, context) { 146 | console.log('A KEY PRESSED!'); 147 | } 148 | }, 149 | }, 150 | }, 151 | }, 152 | }, 153 | ``` 154 | 155 | ## Thanks 156 | 157 | This project is based on [quill-image-uploader](https://github.com/NoelOConnell/quill-image-uploader), thanks mate! 158 | -------------------------------------------------------------------------------- /demos/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | rm -rf ./dist 4 | mkdir dist 5 | 6 | # Common Header 7 | cp ./header-text.js ./dist 8 | # Demo list 9 | cp ./demos-index.html ./dist/index.html 10 | 11 | # Build typescript 12 | rm -rf ./typescript/dist 13 | yarn build:typescript 14 | cp -r ./typescript/dist ./dist/typescript 15 | 16 | # Build javascript 17 | rm -rf ./javascript/dist 18 | yarn build:javascript 19 | cp -r ./javascript/dist ./dist/javascript 20 | 21 | # Copy script tags 22 | mkdir ./dist/script-tags 23 | cp ./script-tags/* ./dist/script-tags 24 | 25 | # Build vue 26 | rm -rf ./vue/dist 27 | yarn build:vue 28 | cp -r ./vue/dist ./dist/vue 29 | 30 | # Build react 31 | rm -rf ./react/dist 32 | yarn build:react 33 | cp -r ./react/dist ./dist/react 34 | -------------------------------------------------------------------------------- /demos/demos-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | quill-html-edit-button (Demos) 7 | 8 | 9 | 10 |

quill-html-edit-button

11 |

12 | This is a quill.js module 13 | which allows you to quickly view/edit the HTML in the editor. 14 |

15 |

16 | Demo 22 |

23 |

Demos

24 |

25 | Below are a few demos for the 26 | quill-html-edit-button 29 | library. 30 |

31 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /demos/header-text.js: -------------------------------------------------------------------------------- 1 | const demoTitle = document.title; 2 | const demoSourceCodeLink = document 3 | .getElementById("src-code") 4 | .getAttribute("data"); 5 | 6 | // HEADER TEXT 7 | const headerHtml = `
8 |

All Demos

9 |
>
10 |

${document.title}

11 |

Demo Source Code

12 |
13 | `; 14 | const header = document.createElement("div"); 15 | header.innerHTML = headerHtml; 16 | 17 | // GITHUB CORNER 18 | const cornerHTML = ` 19 | 22 | 23 | `; 26 | const corner = document.createElement("div"); 27 | corner.innerHTML = cornerHTML; 28 | 29 | // Add to DOM after first JS frame 30 | window.addEventListener("load", function () { 31 | document.body.prepend(header); 32 | document.body.prepend(corner); 33 | }); 34 | -------------------------------------------------------------------------------- /demos/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "webpack serve --mode development --hot", 9 | "build": "webpack --mode production" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demos/javascript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (javascript) 7 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 |
33 |

Select the HTML button from the toolbar

34 |


35 |

Try editing this sentence!

36 |
37 | 38 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demos/javascript/src/index.js: -------------------------------------------------------------------------------- 1 | import htmlEditButton from "../../../src/quill.htmlEditButton"; 2 | 3 | import Quill from "quill/core"; 4 | 5 | import Toolbar from "quill/modules/toolbar"; 6 | import Snow from "quill/themes/snow"; 7 | 8 | Quill.register({ 9 | "modules/toolbar": Toolbar, 10 | "themes/snow": Snow, 11 | "modules/htmlEditButton": htmlEditButton, 12 | }); 13 | 14 | const fullToolbarOptions = [ 15 | [{ header: [1, 2, 3, false] }], 16 | ["bold", "italic"], 17 | ["clean"], 18 | ["image"], 19 | [{ list: "ordered" }, { list: "bullet" }], 20 | ]; 21 | 22 | console.log("Demo loaded..."); 23 | 24 | var quill = new Quill("#editor", { 25 | theme: "snow", 26 | modules: { 27 | toolbar: { 28 | container: fullToolbarOptions, 29 | }, 30 | htmlEditButton: { debug: true, syntax: true }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /demos/javascript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | const isProd = process.argv.includes("production"); 6 | 7 | /** 8 | * @type {import('webpack').Configuration} 9 | */ 10 | module.exports = { 11 | entry: { 12 | index: "./src/index.js", 13 | }, 14 | output: { 15 | filename: "[name].min.js", 16 | libraryTarget: "umd", 17 | path: path.resolve(__dirname, "dist"), 18 | publicPath: "/dist", 19 | }, 20 | devServer: { 21 | port: 8001, 22 | static: { 23 | directory: path.resolve(__dirname, "dist"), 24 | }, 25 | devMiddleware: { 26 | writeToDisk: true, 27 | }, 28 | }, 29 | plugins: [ 30 | new CopyWebpackPlugin({ 31 | patterns: [ 32 | { from: "public", to: "." }, 33 | { from: "../header-text.js", to: "." }, 34 | ], 35 | }), 36 | ], 37 | devtool: isProd ? "source-map" : "inline-source-map", 38 | optimization: { 39 | minimize: true, 40 | minimizer: [ 41 | new TerserPlugin({ 42 | extractComments: true, 43 | parallel: true, 44 | terserOptions: { 45 | compress: { 46 | drop_console: false, 47 | }, 48 | }, 49 | }), 50 | ], 51 | }, 52 | resolve: { 53 | extensions: [".ts", ".js"], 54 | }, 55 | module: { 56 | rules: [ 57 | { test: /\.ts$/, use: ["ts-loader"], exclude: /node_modules/ }, 58 | { 59 | test: /\.css$/, 60 | use: ["style-loader", "css-loader"], 61 | }, 62 | { 63 | test: /\.js$/, 64 | exclude: /node_modules/, 65 | use: { 66 | loader: "babel-loader", 67 | }, 68 | }, 69 | { 70 | test: /\.svg$/, 71 | use: [ 72 | { 73 | loader: "html-loader", 74 | options: { 75 | minimize: true, 76 | }, 77 | }, 78 | ], 79 | }, 80 | ], 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /demos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-html-edit-button-demos", 3 | "version": "0.0.1", 4 | "private": true, 5 | "workspaces": [ 6 | "javascript", 7 | "react", 8 | "typescript", 9 | "vue" 10 | ], 11 | "scripts": { 12 | "start:javascript": "yarn workspace javascript start", 13 | "start:react": "yarn workspace react start", 14 | "start:typescript": "yarn workspace typescript start", 15 | "start:vue": "yarn workspace vue start", 16 | "build:javascript": "yarn workspace javascript build", 17 | "build:react": "yarn workspace react build", 18 | "build:typescript": "yarn workspace typescript build", 19 | "build:vue": "yarn workspace vue build", 20 | "build": "./build.sh" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.26.0", 24 | "babel-loader": "^7.1.3", 25 | "copy-webpack-plugin": "^12.0.2", 26 | "webpack": "5.89.0", 27 | "webpack-cli": "4.10.0", 28 | "webpack-dev-server": "4.15.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demos/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demos/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "webpack serve --mode development --hot", 9 | "build": "webpack --mode production" 10 | }, 11 | "devDependencies": { 12 | "react-quill": "2.0.0", 13 | "react": "18.2.0", 14 | "react-dom": "18.2.0", 15 | "babel-core": "^6.26.0", 16 | "babel-loader": "9.1.3", 17 | "@babel/preset-react": "7.23.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demos/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (react) 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demos/react/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import htmlEditButton from "../../../src/quill.htmlEditButton"; 4 | // import htmlEditButton from "quill-html-edit-button"; 5 | // ^ In production use this 6 | import ReactQuill, { Quill } from "react-quill"; 7 | import "react-quill/dist/quill.snow.css"; 8 | 9 | import Toolbar from "quill/modules/toolbar"; 10 | import Snow from "quill/themes/snow"; 11 | 12 | Quill.register({ 13 | "modules/toolbar": Toolbar, 14 | "themes/snow": Snow, 15 | "modules/htmlEditButton": htmlEditButton, 16 | }); 17 | 18 | const modules = { 19 | htmlEditButton: { 20 | debug: true, 21 | }, 22 | }; 23 | 24 | export function MyApp() { 25 | const [value, setValue] = useState("Here is some text!"); 26 | 27 | return ( 28 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /demos/react/src/index.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | /* global ReactQuill */ 3 | "use strict"; 4 | 5 | import { MyApp } from "./app"; 6 | import ReactDOM from "react-dom/client"; 7 | import React from "react"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("app")); 10 | 11 | root.render(); 12 | -------------------------------------------------------------------------------- /demos/react/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | const isProd = process.argv.includes("production"); 6 | 7 | /** 8 | * @type {import('webpack').Configuration} 9 | */ 10 | module.exports = { 11 | entry: { 12 | index: "./src/index.jsx", 13 | }, 14 | output: { 15 | filename: "[name].min.js", 16 | libraryTarget: "umd", 17 | path: path.resolve(__dirname, "dist"), 18 | }, 19 | devServer: { 20 | port: 8001, 21 | hot: true, 22 | devMiddleware: { 23 | writeToDisk: true, 24 | }, 25 | }, 26 | plugins: [ 27 | new CopyWebpackPlugin({ 28 | patterns: [ 29 | { from: "public", to: "." }, 30 | { from: "../header-text.js", to: "." }, 31 | ], 32 | }), 33 | ], 34 | devtool: isProd ? "source-map" : "inline-source-map", 35 | optimization: { 36 | minimize: true, 37 | minimizer: [ 38 | new TerserPlugin({ 39 | extractComments: true, 40 | parallel: true, 41 | }), 42 | ], 43 | }, 44 | resolve: { 45 | extensions: [".js", ".jsx", ".ts"], 46 | }, 47 | module: { 48 | rules: [ 49 | { test: /\.ts$/, use: ["ts-loader"], exclude: /node_modules/ }, 50 | { 51 | test: /\.css$/, 52 | use: ["style-loader", "css-loader"], 53 | }, 54 | { 55 | test: /\.jsx$/, 56 | exclude: /node_modules/, 57 | use: { 58 | loader: "babel-loader", 59 | }, 60 | }, 61 | { 62 | test: /\.svg$/, 63 | use: [ 64 | { 65 | loader: "html-loader", 66 | options: { 67 | minimize: true, 68 | }, 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /demos/script-tags/demo-quill-1.x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (script tag) Quill 1.x 7 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

Select the HTML button from the toolbar

35 |


36 |

Try editing this sentence!

37 |
38 | 39 | 43 | 44 | 45 | 46 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /demos/script-tags/demo-quill-2.x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (script tag) Quill 2.x 7 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

Select the HTML button from the toolbar

35 |


36 |

Try editing this sentence!

37 |

Wow a table!

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
AustraliaAmericaAmericalia
47 |
48 | 49 | 53 | 54 | 55 | 56 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /demos/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "webpack serve --mode development --hot", 9 | "build": "webpack --mode production" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demos/typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (typescript) 7 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 |
33 |

Select the HTML button from the toolbar

34 |


35 |

Try editing this sentence!

36 |
37 | 38 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demos/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Currently there's no types for quill modules, 3 | so you need to either make types, or use a js 4 | file to import them (which is what setup.js is). 5 | */ 6 | import Quill from "./setup"; 7 | 8 | const fullToolbarOptions = [ 9 | [{ header: [1, 2, 3, false] }], 10 | ["bold", "italic"], 11 | ["clean"], 12 | ["image"], 13 | [{ list: "ordered" }, { list: "bullet" }], 14 | ]; 15 | 16 | console.log("Demo loaded..."); 17 | 18 | var quill = new Quill("#editor", { 19 | theme: "snow", 20 | modules: { 21 | toolbar: { 22 | container: fullToolbarOptions, 23 | }, 24 | htmlEditButton: { debug: true, syntax: true }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /demos/typescript/src/setup.js: -------------------------------------------------------------------------------- 1 | import Quill from "quill/core"; 2 | 3 | import htmlEditButton from "../../../src/quill.htmlEditButton"; 4 | /* 5 | import htmlEditButton from "quill-html-edit-button"; 6 | ^ In production use this 7 | */ 8 | import Toolbar from "quill/modules/toolbar"; 9 | import Snow from "quill/themes/snow"; 10 | 11 | Quill.register({ 12 | "modules/toolbar": Toolbar, 13 | "themes/snow": Snow, 14 | }); 15 | Quill.register("modules/htmlEditButton", htmlEditButton); 16 | 17 | export default Quill; 18 | -------------------------------------------------------------------------------- /demos/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "strict": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "allowJs": true, 9 | "checkJs": false, 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "outDir": "dist", 15 | "lib": ["es2017", "dom"] 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /demos/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | const isProd = process.argv.includes("production"); 6 | 7 | /** 8 | * @type {import('webpack').Configuration} 9 | */ 10 | module.exports = { 11 | entry: { 12 | index: "./src/index.ts", 13 | }, 14 | output: { 15 | filename: "[name].min.js", 16 | libraryTarget: "umd", 17 | path: path.resolve(__dirname, "dist"), 18 | }, 19 | devServer: { 20 | port: 8001, 21 | hot: true, 22 | devMiddleware: { 23 | writeToDisk: true, 24 | }, 25 | }, 26 | plugins: [ 27 | new CopyWebpackPlugin({ 28 | patterns: [ 29 | { from: "public", to: "." }, 30 | { from: "../header-text.js", to: "." }, 31 | ], 32 | }), 33 | ], 34 | devtool: isProd ? "source-map" : "inline-source-map", 35 | optimization: { 36 | minimize: true, 37 | minimizer: [ 38 | new TerserPlugin({ 39 | extractComments: true, 40 | parallel: true, 41 | terserOptions: { 42 | compress: { 43 | drop_console: false, 44 | }, 45 | }, 46 | }), 47 | ], 48 | }, 49 | resolve: { 50 | extensions: [".ts", ".js"], 51 | }, 52 | module: { 53 | rules: [ 54 | { test: /\.ts$/, use: ["ts-loader"], exclude: /node_modules/ }, 55 | { 56 | test: /\.css$/, 57 | use: ["style-loader", "css-loader"], 58 | }, 59 | { 60 | test: /\.svg$/, 61 | use: [ 62 | { 63 | loader: "html-loader", 64 | options: { 65 | minimize: true, 66 | }, 67 | }, 68 | ], 69 | }, 70 | ], 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /demos/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "webpack serve --mode development --hot", 9 | "build": "webpack --mode production" 10 | }, 11 | "devDependencies": { 12 | "@vueup/vue-quill": "1.0.0-beta.8", 13 | "vue": "3.3.7", 14 | "vue-loader": "17.3.1", 15 | "vue-template-compiler": "2.6.14" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demos/vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Demo (vue) 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demos/vue/src/app.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 37 | -------------------------------------------------------------------------------- /demos/vue/src/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./app.vue"; 3 | 4 | import "@vueup/vue-quill/dist/vue-quill.bubble.css"; 5 | import "@vueup/vue-quill/dist/vue-quill.snow.css"; 6 | 7 | createApp(App).mount("#app"); 8 | -------------------------------------------------------------------------------- /demos/vue/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | const { VueLoaderPlugin } = require("vue-loader"); 5 | 6 | const isProd = process.argv.includes("production"); 7 | 8 | /** 9 | * @type {import('webpack').Configuration} 10 | */ 11 | module.exports = { 12 | entry: { 13 | index: "./src/index.js", 14 | }, 15 | output: { 16 | filename: "[name].min.js", 17 | libraryTarget: "umd", 18 | path: path.resolve(__dirname, "dist"), 19 | }, 20 | devServer: { 21 | port: 8001, 22 | hot: true, 23 | devMiddleware: { 24 | writeToDisk: true, 25 | }, 26 | }, 27 | plugins: [ 28 | new CopyWebpackPlugin({ 29 | patterns: [ 30 | { from: "public", to: "." }, 31 | { from: "../header-text.js", to: "." }, 32 | ], 33 | }), 34 | new VueLoaderPlugin(), 35 | ], 36 | devtool: isProd ? "source-map" : "inline-source-map", 37 | optimization: { 38 | minimize: true, 39 | minimizer: [ 40 | new TerserPlugin({ 41 | extractComments: true, 42 | parallel: true, 43 | }), 44 | ], 45 | }, 46 | resolve: { 47 | extensions: [".ts", ".js", ".vue"], 48 | alias: { 49 | vue: "vue/dist/vue.esm-bundler.js", 50 | }, 51 | }, 52 | module: { 53 | rules: [ 54 | { test: /\.ts$/, use: ["ts-loader"], exclude: /node_modules/ }, 55 | { 56 | test: /\.css$/, 57 | use: ["style-loader", "css-loader"], 58 | }, 59 | { 60 | test: /\.js$/, 61 | exclude: /node_modules/, 62 | use: { 63 | loader: "babel-loader", 64 | }, 65 | }, 66 | { 67 | test: /\.svg$/, 68 | use: [ 69 | { 70 | loader: "html-loader", 71 | options: { 72 | minimize: true, 73 | }, 74 | }, 75 | ], 76 | }, 77 | { 78 | test: /\.vue$/, 79 | exclude: /node_modules/, 80 | loader: "vue-loader", 81 | }, 82 | ], 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.ts$" 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-html-edit-button", 3 | "description": "A Quill rich text editor Module which adds an html edit button to the quill editor", 4 | "version": "0.0.0", 5 | "main": "dist/quill.htmlEditButton.min.js", 6 | "types": "types/index.d.ts", 7 | "repository": "https://github.com/benwinding/quill-html-edit-button", 8 | "author": "Ben Winding ", 9 | "license": "MIT", 10 | "scripts": { 11 | "watch": "webpack --watch --progress", 12 | "build": "yarn build-lib && yarn build-types", 13 | "build-lib": "webpack --mode production", 14 | "build-types": "tsc --declaration --emitDeclarationOnly", 15 | "test": "jest", 16 | "test-watch": "jest --watch-all", 17 | "lint": "prettier --write src/**/*.ts", 18 | "clean": "rm -rf dist/" 19 | }, 20 | "keywords": [ 21 | "quill", 22 | "quilljs", 23 | "html", 24 | "button", 25 | "edit" 26 | ], 27 | "peerDependencies": { 28 | "quill": "^2.x" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^26.0.15", 32 | "@types/node": "^14.14.22", 33 | "@types/quill": "^2.0.1", 34 | "css-loader": "^0.28.8", 35 | "highlight.js": "^10.1.2", 36 | "html-loader": "^1.3.2", 37 | "jest": "^26.6.3", 38 | "prettier": "^3.0.3", 39 | "quill": "^2.0.2", 40 | "style-loader": "^0.19.1", 41 | "ts-jest": "^26.4.4", 42 | "ts-loader": "^9.5.1", 43 | "typescript": "^5.5.3", 44 | "webpack": "5.93.0", 45 | "webpack-cli": "5.1.4", 46 | "webpack-dev-server": "5.0.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/html-formatter.ts: -------------------------------------------------------------------------------- 1 | import { QuillHtmlLogger } from "./logger"; 2 | 3 | // Adapted FROM jsfiddle here: https://jsfiddle.net/buksy/rxucg1gd/ 4 | export function FormatHTMLStringIndentation( 5 | code: string, 6 | logger: QuillHtmlLogger, 7 | ) { 8 | "use strict"; 9 | let stripWhiteSpaces = true; 10 | let stripEmptyLines = true; 11 | const whitespace = " ".repeat(2); // Default indenting 4 whitespaces 12 | let currentIndent = 0; 13 | const newlineChar = "\n"; 14 | let prevChar = null; 15 | let char = null; 16 | let nextChar = null; 17 | 18 | let result = ""; 19 | for (let pos = 0; pos <= code.length; pos++) { 20 | prevChar = char; 21 | char = code.substr(pos, 1); 22 | nextChar = code.substr(pos + 1, 1); 23 | 24 | const isBrTag = code.substr(pos, 4) === "
"; 25 | const isOpeningTag = char === "<" && nextChar !== "/" && !isBrTag; 26 | const isClosingTag = char === "<" && nextChar === "/" && !isBrTag; 27 | const isTagEnd = prevChar === ">" && char !== "<" && currentIndent > 0; 28 | const isTagNext = 29 | !isBrTag && 30 | !isOpeningTag && 31 | !isClosingTag && 32 | isTagEnd && 33 | code.substr(pos, code.substr(pos).indexOf("<")).trim() === ""; 34 | if (isBrTag) { 35 | // If opening tag, add newline character and indention 36 | result += newlineChar; 37 | currentIndent--; 38 | pos += 4; 39 | } 40 | if (isOpeningTag) { 41 | // If opening tag, add newline character and indention 42 | result += newlineChar + whitespace.repeat(currentIndent); 43 | currentIndent++; 44 | } 45 | // if Closing tag, add newline and indention 46 | else if (isClosingTag) { 47 | // If there're more closing tags than opening 48 | if (--currentIndent < 0) currentIndent = 0; 49 | result += newlineChar + whitespace.repeat(currentIndent); 50 | } 51 | // remove multiple whitespaces 52 | else if (stripWhiteSpaces === true && char === " " && nextChar === " ") 53 | char = ""; 54 | // remove empty lines 55 | else if (stripEmptyLines === true && char === newlineChar) { 56 | //debugger; 57 | if (code.substr(pos, code.substr(pos).indexOf("<")).trim() === "") 58 | char = ""; 59 | } 60 | if (isTagEnd && !isTagNext) { 61 | result += newlineChar + whitespace.repeat(currentIndent); 62 | } 63 | 64 | result += char; 65 | } 66 | logger.log("formatHTML", { 67 | before: code, 68 | after: result, 69 | }); 70 | return result; 71 | } 72 | -------------------------------------------------------------------------------- /src/html-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConvertMultipleSpacesToSingle, 3 | FixTagSpaceOpenTag, 4 | FixTagSpaceCloseTag, 5 | PreserveNewlinesBr, 6 | PreserveNewlinesPTags, 7 | } from "./html-parser"; 8 | 9 | describe("html parser multiple spaces converted to single", () => { 10 | test("space on right of tag", () => { 11 | expect(ConvertMultipleSpacesToSingle("OKAY ")).toBe( 12 | "OKAY", 13 | ); 14 | }); 15 | test("new lines on right", () => { 16 | expect( 17 | ConvertMultipleSpacesToSingle(`OKAY 18 | `), 19 | ).toBe("OKAY"); 20 | }); 21 | test(" spaces between", () => { 22 | expect(ConvertMultipleSpacesToSingle("11 22")).toBe( 23 | "11 22", 24 | ); 25 | }); 26 | }); 27 | 28 | describe("html parser convert br tags", () => { 29 | test("
is converted to

", () => { 30 | expect(PreserveNewlinesBr("
")).toBe("

"); 31 | }); 32 | test("
is converted to

", () => { 33 | expect(PreserveNewlinesBr("
")).toBe("

"); 34 | }); 35 | test("
is converted to

", () => { 36 | expect(PreserveNewlinesBr("
")).toBe("

"); 37 | }); 38 | }); 39 | 40 | describe("html parser convert p tags", () => { 41 | test("

is converted to

", () => { 42 | expect(PreserveNewlinesPTags("

")).toBe("

"); 43 | }); 44 | }); 45 | 46 | describe("html parser spacing around open tags", () => { 47 | test("<> space right of > stripped", () => { 48 | expect(FixTagSpaceOpenTag(" test")).toBe("test"); 49 | }); 50 | test("<> spaces between left NOT stripped", () => { 51 | expect(FixTagSpaceOpenTag(" ")).toBe(" "); 52 | }); 53 | }); 54 | 55 | describe("html parser spacing around close tags", () => { 56 | test("<> space left of < stripped", () => { 57 | expect(FixTagSpaceCloseTag("test ")).toBe("test "); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/html-parser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Formats the the output from the popup so that the quill editor can properly display it 3 | */ 4 | 5 | export function OutputHTMLParser(inputHtmlFromQuillPopup: string): string { 6 | return Compose( 7 | [ 8 | ConvertMultipleSpacesToSingle, 9 | FixTagSpaceOpenTag, 10 | FixTagSpaceCloseTag, 11 | PreserveNewlinesBr, 12 | PreserveNewlinesPTags, 13 | ], 14 | inputHtmlFromQuillPopup, 15 | ); 16 | } 17 | 18 | export function ConvertMultipleSpacesToSingle(input: string): string { 19 | return input.replace(/\s+/g, " ").trim(); 20 | } 21 | 22 | export function PreserveNewlinesBr(input: string): string { 23 | return input.replace(/)/g, "

"); 24 | } 25 | 26 | export function PreserveNewlinesPTags(input: string): string { 27 | return input.replace(/

<\/p>/g, "

"); 28 | } 29 | 30 | export function FixTagSpaceOpenTag(input: string): string { 31 | // Open tag remove space on inside 32 | return input.replace(/(<(?!\/)[\w=\."'\s]*>) /g, "$1"); 33 | } 34 | 35 | export function FixTagSpaceCloseTag(input: string): string { 36 | // Close tag remove space on inside 37 | return input.replace(/ (<\/[\w]+>)/g, "$1"); 38 | } 39 | 40 | export function Compose(functions: Array<(input: T) => T>, input: T): T { 41 | return functions.reduce((acc, cur) => cur(acc), input); 42 | } 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { QuillHtmlEditButtonOptions } from "./options"; 2 | import { htmlEditButton } from "./quill.htmlEditButton"; 3 | 4 | export { QuillHtmlEditButtonOptions }; 5 | export { htmlEditButton }; 6 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export class QuillHtmlLogger { 2 | private debug = false; 3 | 4 | setDebug(debug: boolean) { 5 | this.debug = debug; 6 | } 7 | 8 | prefixString() { 9 | return ` quill-html-edit-button: `; 10 | } 11 | get log() { 12 | if (!this.debug) { 13 | return (...args: any) => {}; 14 | } 15 | const boundLogFn = console.log.bind(console, this.prefixString()); 16 | return boundLogFn; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | export interface QuillHtmlEditButtonOptions { 3 | // Flags 4 | debug?: boolean; // default: false 5 | syntax?: boolean; // default: false 6 | // Overlay 7 | closeOnClickOverlay: boolean; // default: true 8 | prependSelector: string; // default: null 9 | // Labels 10 | buttonHTML?: string; // default: "<>" 11 | buttonTitle?: string; // default: "Show HTML source" 12 | msg: string; // default: 'Edit HTML here, when you click "OK" the quill editor\'s contents will be replaced' 13 | okText: string; // default: "Ok" 14 | cancelText: string; // default: "Cancel" 15 | // Quill Modules (for the HTML editor) 16 | editorModules?: { 17 | // Any modules here will be declared in HTML quill editor instance 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/quill.htmlEditButton.ts: -------------------------------------------------------------------------------- 1 | import { QuillHtmlLogger } from "./logger"; 2 | import "./styles.css"; 3 | import Quill from "quill"; 4 | import { QuillHtmlEditButtonOptions } from "./options"; 5 | import { OutputHTMLParser } from "./html-parser"; 6 | import { FormatHTMLStringIndentation } from "./html-formatter"; 7 | import { default as Toolbar } from "quill/modules/toolbar"; 8 | 9 | function $create(elName: string) { 10 | return document.createElement(elName); 11 | } 12 | function $setAttr(el: HTMLElement, key: string, value: string) { 13 | return el.setAttribute(key, value); 14 | } 15 | 16 | const Logger = new QuillHtmlLogger(); 17 | 18 | class htmlEditButton { 19 | constructor(quill: Quill, optionsInput: QuillHtmlEditButtonOptions) { 20 | const options = optionsInput || ({} as QuillHtmlEditButtonOptions); 21 | const debug = !!(options && options.debug); 22 | Logger.setDebug(debug); 23 | Logger.log("logging enabled"); 24 | // Add button to all quill toolbar instances 25 | const toolbarModule = quill.getModule("toolbar") as Toolbar; 26 | if (!toolbarModule) { 27 | throw new Error( 28 | 'quill.htmlEditButton requires the "toolbar" module to be included too', 29 | ); 30 | } 31 | // this.registerDivModule(); 32 | let toolbarEl = toolbarModule.container; 33 | const buttonContainer = $create("span"); 34 | $setAttr(buttonContainer, "class", "ql-formats"); 35 | const button = $create("button") as HTMLButtonElement; 36 | button.innerHTML = options.buttonHTML || "<>"; 37 | button.title = options.buttonTitle || "Show HTML source"; 38 | button.type = "button"; 39 | const onSave = (html: string) => { 40 | quill.clipboard.dangerouslyPasteHTML(html); 41 | }; 42 | button.onclick = function (e) { 43 | e.preventDefault(); 44 | launchPopupEditor(quill, options, onSave); 45 | }; 46 | buttonContainer.appendChild(button); 47 | toolbarEl?.appendChild(buttonContainer); 48 | } 49 | } 50 | 51 | function launchPopupEditor( 52 | quill: Quill & any, 53 | options: QuillHtmlEditButtonOptions, 54 | saveCallback: (html: string) => void, 55 | ) { 56 | const htmlFromEditor = quill.container.querySelector(".ql-editor").innerHTML; 57 | const popupContainer = $create("div"); 58 | const overlayContainer = $create("div"); 59 | const msg = 60 | options.msg || 61 | 'Edit HTML here, when you click "OK" the quill editor\'s contents will be replaced'; 62 | const cancelText = options.cancelText || "Cancel"; 63 | const okText = options.okText || "Ok"; 64 | const closeOnClickOverlay = options.closeOnClickOverlay !== false; 65 | 66 | $setAttr(overlayContainer, "class", "ql-html-overlayContainer"); 67 | $setAttr(popupContainer, "class", "ql-html-popupContainer"); 68 | const popupTitle = $create("span"); 69 | $setAttr(popupTitle, "class", "ql-html-popupTitle"); 70 | popupTitle.innerText = msg; 71 | const textContainer = $create("div"); 72 | textContainer.appendChild(popupTitle); 73 | $setAttr(textContainer, "class", "ql-html-textContainer"); 74 | const codeBlock = $create("pre"); 75 | $setAttr(codeBlock, "data-language", "xml"); 76 | codeBlock.innerText = FormatHTMLStringIndentation(htmlFromEditor, Logger); 77 | const htmlEditor = $create("div"); 78 | $setAttr(htmlEditor, "class", "ql-html-textArea"); 79 | const buttonCancel = $create("button"); 80 | buttonCancel.innerHTML = cancelText; 81 | $setAttr(buttonCancel, "class", "ql-html-buttonCancel"); 82 | const buttonOk = $create("button"); 83 | buttonOk.innerHTML = okText; 84 | $setAttr(buttonOk, "class", "ql-html-buttonOk"); 85 | const buttonGroup = $create("div"); 86 | $setAttr(buttonGroup, "class", "ql-html-buttonGroup"); 87 | const prependSelector = document.querySelector(options.prependSelector); 88 | 89 | buttonGroup.appendChild(buttonCancel); 90 | buttonGroup.appendChild(buttonOk); 91 | htmlEditor.appendChild(codeBlock); 92 | textContainer.appendChild(htmlEditor); 93 | textContainer.appendChild(buttonGroup); 94 | popupContainer.appendChild(textContainer); 95 | overlayContainer.appendChild(popupContainer); 96 | 97 | if (prependSelector) { 98 | prependSelector.prepend(overlayContainer); 99 | } else { 100 | document.body.appendChild(overlayContainer); 101 | } 102 | 103 | const modules = options && options.editorModules; 104 | const hasModules = !!modules && !!Object.keys(modules).length; 105 | const modulesSafe = hasModules ? modules : {}; 106 | // console.time('new Quill') 107 | const editor = new Quill(htmlEditor, { 108 | modules: { 109 | syntax: options.syntax, 110 | ...modulesSafe, 111 | }, 112 | }); 113 | // console.timeEnd('new Quill') 114 | 115 | buttonCancel.onclick = function () { 116 | if (prependSelector) { 117 | prependSelector.removeChild(overlayContainer); 118 | } else { 119 | document.body.removeChild(overlayContainer); 120 | } 121 | }; 122 | 123 | if (closeOnClickOverlay) { 124 | overlayContainer.onclick = buttonCancel.onclick; 125 | } 126 | 127 | popupContainer.onclick = function (e) { 128 | e.preventDefault(); 129 | e.stopPropagation(); 130 | }; 131 | buttonOk.onclick = function () { 132 | const container = (editor as any).container as HTMLElement; 133 | const qlElement = container.querySelector(".ql-editor") as HTMLDivElement; 134 | const htmlInputFromPopup = qlElement.innerText; 135 | const htmlOutputFormatted = OutputHTMLParser(htmlInputFromPopup); 136 | Logger.log("OutputHTMLParser", { htmlInputFromPopup, htmlOutputFormatted }); 137 | saveCallback(htmlOutputFormatted); 138 | if (prependSelector) { 139 | prependSelector.removeChild(overlayContainer); 140 | } else { 141 | document.body.removeChild(overlayContainer); 142 | } 143 | }; 144 | } 145 | 146 | (window as any)["htmlEditButton"] = htmlEditButton; 147 | export default htmlEditButton; 148 | export { htmlEditButton }; 149 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .ql-html-overlayContainer { 2 | background: #0000007d; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | z-index: 9999; 9 | } 10 | 11 | .ql-html-popupContainer { 12 | background: #ddd; 13 | position: absolute; 14 | top: 5%; 15 | left: 5%; 16 | right: 5%; 17 | bottom: 5%; 18 | border-radius: 10px; 19 | } 20 | 21 | .ql-html-textContainer { 22 | position: relative; 23 | height: calc(100% - 40px); 24 | padding: 20px; 25 | } 26 | 27 | .ql-html-textArea { 28 | background: #fff; 29 | position: absolute; 30 | left: 15px; 31 | width: calc(100% - 30px); 32 | height: calc(100% - 60px) !important; 33 | } 34 | 35 | .ql-html-textArea .ql-syntax { 36 | word-break: break-all; 37 | white-space: pre-wrap; 38 | } 39 | 40 | .ql-html-buttonCancel { 41 | margin-right: 20px; 42 | } 43 | 44 | .ql-html-popupTitle { 45 | margin: 0; 46 | display: block; 47 | font-style: italic; 48 | } 49 | 50 | .ql-html-buttonGroup { 51 | position: absolute; 52 | bottom: 20px; 53 | transform: scale(1.5); 54 | left: calc(50% - 60px); 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "strict": true, 6 | "declaration": true, 7 | "module": "commonjs", 8 | "target": "es5", 9 | "allowJs": true, 10 | "checkJs": false, 11 | "moduleResolution": "node", 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "outDir": "types", 16 | "lib": ["es2017", "dom"] 17 | }, 18 | "include": ["src/index.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | 4 | const isProd = process.argv.includes("production"); 5 | 6 | /** 7 | * @type {import('webpack').Configuration} 8 | */ 9 | module.exports = [ 10 | { 11 | entry: { 12 | "quill.htmlEditButton": "./src/quill.htmlEditButton.ts", 13 | }, 14 | output: { 15 | filename: "[name].min.js", 16 | path: path.resolve(__dirname, "dist"), 17 | libraryTarget: "umd", 18 | publicPath: "/dist/", 19 | }, 20 | devServer: { 21 | port: 8001, 22 | hot: true, 23 | static: { 24 | directory: path.join(__dirname, 'src'), 25 | serveIndex: true, 26 | }, 27 | devMiddleware: { 28 | writeToDisk: true, 29 | }, 30 | }, 31 | devtool: isProd ? "source-map" : "inline-source-map", 32 | optimization: { 33 | minimize: true, 34 | minimizer: [ 35 | new TerserPlugin({ 36 | extractComments: true, 37 | parallel: true, 38 | terserOptions: { 39 | compress: { 40 | drop_console: false, 41 | }, 42 | }, 43 | }), 44 | ], 45 | }, 46 | resolve: { 47 | extensions: [".ts", ".js"], 48 | }, 49 | module: { 50 | rules: [ 51 | { test: /\.ts$/, use: ["ts-loader"], exclude: /node_modules/ }, 52 | { 53 | test: /\.css$/, 54 | use: ["style-loader", "css-loader"], 55 | }, 56 | { 57 | test: /\.svg$/, 58 | use: [ 59 | { 60 | loader: "html-loader", 61 | options: { 62 | minimize: true, 63 | }, 64 | }, 65 | ], 66 | }, 67 | ], 68 | }, 69 | }, 70 | ]; 71 | --------------------------------------------------------------------------------