├── test ├── index.js ├── mocha.opts ├── specs │ └── editor.spec.js └── istanbul.reporter.js ├── .gitignore ├── assets ├── fonts │ ├── Noto-Sans-700 │ │ ├── Noto-Sans-700.eot │ │ ├── Noto-Sans-700.ttf │ │ ├── Noto-Sans-700.woff │ │ ├── Noto-Sans-700.woff2 │ │ └── Noto-Sans-700.svg │ ├── Noto-Sans-italic │ │ ├── Noto-Sans-italic.eot │ │ ├── Noto-Sans-italic.ttf │ │ ├── Noto-Sans-italic.woff │ │ └── Noto-Sans-italic.woff2 │ ├── Noto-Sans-regular │ │ ├── Noto-Sans-regular.eot │ │ ├── Noto-Sans-regular.ttf │ │ ├── Noto-Sans-regular.woff │ │ └── Noto-Sans-regular.woff2 │ └── Noto-Sans-700italic │ │ ├── Noto-Sans-700italic.eot │ │ ├── Noto-Sans-700italic.ttf │ │ ├── Noto-Sans-700italic.woff │ │ ├── Noto-Sans-700italic.woff2 │ │ └── Noto-Sans-700italic.svg ├── css │ └── style.scss └── js │ └── scale.fix.js ├── .babelrc ├── src ├── helpers │ ├── store.js │ ├── EditorColorPicker.svelte │ ├── EditorModal.svelte │ ├── util.js │ └── actions.js ├── Editor.svelte.d.ts ├── app.js └── Editor.svelte ├── tsconfig.json ├── dist ├── index.html └── index.min.js ├── .github └── workflows │ └── build-publish.yml ├── LICENSE ├── rollup.config.js ├── package.json ├── index.md ├── _sass ├── fonts.scss ├── rouge-github.scss └── jekyll-theme-minimal.scss ├── _layouts └── default.html └── README.md /test/index.js: -------------------------------------------------------------------------------- 1 | require('browser-env')(); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | coverage 5 | .history -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter test/istanbul.reporter.js --recursive --require babel-core/register -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700/Noto-Sans-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700/Noto-Sans-700.eot -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700/Noto-Sans-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700/Noto-Sans-700.ttf -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700/Noto-Sans-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700/Noto-Sans-700.woff -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700/Noto-Sans-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700/Noto-Sans-700.woff2 -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-italic/Noto-Sans-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-italic/Noto-Sans-italic.eot -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-italic/Noto-Sans-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-italic/Noto-Sans-italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-italic/Noto-Sans-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-italic/Noto-Sans-italic.woff -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-italic/Noto-Sans-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-italic/Noto-Sans-italic.woff2 -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-regular/Noto-Sans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.eot -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-regular/Noto-Sans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-regular/Noto-Sans-regular.woff2 -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nenadpnc/cl-editor/HEAD/assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": ["istanbul"] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "jekyll-theme-minimal"; 5 | 6 | a { 7 | cursor: pointer; 8 | } 9 | 10 | #inlineEdit { 11 | display: inline-block; 12 | } 13 | 14 | #inlineEdit .cl { 15 | margin-top: -46.5px; 16 | margin-left: -10px; 17 | margin-right: 10px; 18 | } -------------------------------------------------------------------------------- /src/helpers/store.js: -------------------------------------------------------------------------------- 1 | import {writable} from "svelte/store"; 2 | 3 | const state = (function(name) { 4 | let state = { 5 | actionBtns: [], 6 | actionObj: {} 7 | } 8 | 9 | const { subscribe, set, update } = writable(state); 10 | 11 | return { 12 | name, 13 | set, 14 | update, 15 | subscribe 16 | } 17 | }); 18 | 19 | export const createStateStore = state; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "sourceMap": false, 8 | "noImplicitReturns": true, 9 | "typeRoots": [ 10 | "types" 11 | ] 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist" 16 | ], 17 | "include": [ 18 | "src/**/*.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /test/specs/editor.spec.js: -------------------------------------------------------------------------------- 1 | const Editor = require('../../dist/index'); 2 | const { expect } = require('chai'); 3 | 4 | describe('Editor', () => { 5 | const div = () => document.createElement('div'); 6 | 7 | describe('output', () => { 8 | it('renders edtitor', () => { 9 | const el = div(); 10 | new Editor({ 11 | target: el 12 | }); 13 | 14 | const editorWrapper = el.querySelector('.cl'); 15 | expect(editorWrapper).to.not.be.undefined; 16 | }); 17 | }); 18 | }); -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Demo - cl-editor 5 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 | Edit this line of text 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/istanbul.reporter.js: -------------------------------------------------------------------------------- 1 | const instanbul = require('istanbul'); 2 | const MochaSpecReporter = require('mocha/lib/reporters/spec'); 3 | 4 | module.exports = function (runner) { 5 | const collector = new instanbul.Collector(); 6 | const reporter = new instanbul.Reporter(); 7 | reporter.addAll(['lcov', 'json']); 8 | new MochaSpecReporter(runner); 9 | 10 | runner.on('end', function () { 11 | collector.add(global.__coverage__); 12 | 13 | reporter.write(collector, true, function () { 14 | process.stdout.write('report generated'); 15 | }); 16 | }); 17 | }; -------------------------------------------------------------------------------- /.github/workflows/build-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build-Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | test-and-lint: 9 | runs-on: [ubuntu-latest] 10 | 11 | steps: 12 | - name: Checkout 🛎 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup node env 🏗 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '14' 19 | registry-url: https://registry.npmjs.org/ 20 | 21 | - name: Install dependencies 👨🏻‍💻 22 | uses: bahmutov/npm-install@v1 23 | 24 | - name: Build Application 🛠 25 | run: npm run prod 26 | 27 | - name: Publish package 28 | run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 31 | -------------------------------------------------------------------------------- /src/Editor.svelte.d.ts: -------------------------------------------------------------------------------- 1 | declare class Editor { 2 | constructor(options: { 3 | target: Element, 4 | props?: { 5 | actions?: ({name: string, title?: string, icon?: string, result?: Function} | string)[], 6 | height?: string, 7 | html?: string, 8 | removeFormatTags?: string[] 9 | } 10 | }); 11 | 12 | $destroy(detach?: boolean); 13 | 14 | $on(event: 'change' | 'blur', cb: (event?: any) => void); 15 | 16 | exec(cmd: string, value?: string): void 17 | 18 | getHtml(sanitize?: boolean): string 19 | 20 | getText(): string 21 | 22 | setHtml(html: string, sanitize?: boolean): void 23 | 24 | saveRange(element: Element): void 25 | 26 | restoreRange(element: Element): void 27 | 28 | refs: { 29 | colorPicker: HTMLDivElement, 30 | editor: HTMLDivElement, 31 | modal: HTMLDivElement, 32 | raw: HTMLTextAreaElement 33 | } 34 | 35 | } 36 | 37 | export default Editor; 38 | -------------------------------------------------------------------------------- /assets/js/scale.fix.js: -------------------------------------------------------------------------------- 1 | (function(document) { 2 | var metas = document.getElementsByTagName('meta'), 3 | changeViewportContent = function(content) { 4 | for (var i = 0; i < metas.length; i++) { 5 | if (metas[i].name == "viewport") { 6 | metas[i].content = content; 7 | } 8 | } 9 | }, 10 | initialize = function() { 11 | changeViewportContent("width=device-width, minimum-scale=1.0, maximum-scale=1.0"); 12 | }, 13 | gestureStart = function() { 14 | changeViewportContent("width=device-width, minimum-scale=0.25, maximum-scale=1.6"); 15 | }, 16 | gestureEnd = function() { 17 | initialize(); 18 | }; 19 | 20 | 21 | if (navigator.userAgent.match(/iPhone/i)) { 22 | initialize(); 23 | 24 | document.addEventListener("touchstart", gestureStart, false); 25 | document.addEventListener("touchend", gestureEnd, false); 26 | } 27 | })(document); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nenad Panić 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 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | import babel from 'rollup-plugin-babel'; 4 | import filesize from 'rollup-plugin-filesize'; 5 | import resolve from 'rollup-plugin-node-resolve'; 6 | import commonjs from 'rollup-plugin-commonjs'; 7 | 8 | const plugins = [ 9 | svelte({ 10 | extensions: ['.svelte'], 11 | emitCss: false, 12 | exclude:'src/**/*.ts', 13 | compilerOptions: { 14 | dev: false 15 | } 16 | }), 17 | resolve({ 18 | brower: true, 19 | dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/') 20 | }), 21 | commonjs(), 22 | babel({ include: 'node_modules/svelte/**' }) 23 | ]; 24 | 25 | export default [ 26 | { 27 | input: 'src/Editor.svelte', 28 | output: { 29 | file: 'dist/index.min.js', 30 | format: 'umd', 31 | name: 'clEditor' 32 | }, 33 | plugins: [...plugins, terser(), filesize()] 34 | }, 35 | { 36 | input: 'src/Editor.svelte', 37 | output: { 38 | file: 'dist/index.js', 39 | format: 'umd', 40 | name: 'clEditor', 41 | }, 42 | plugins 43 | }, 44 | { 45 | input: 'src/app.js', 46 | output: { 47 | file: 'dist/index.dev.js', 48 | format: 'iife', 49 | }, 50 | plugins 51 | } 52 | ]; 53 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Editor from './Editor.svelte'; 2 | 3 | let inlineEditor; 4 | const inlineEdit = document.getElementById('inlineEdit'); 5 | inlineEdit.addEventListener('click', showEditor); 6 | 7 | const editor = new Editor({ 8 | target: document.getElementById('editor1') 9 | }); 10 | 11 | const editor2 = new Editor({ 12 | target: document.getElementById('editor2'), 13 | props: { 14 | actions: [ 15 | 'b', 'i', 'u', 'strike', 'h1', 'h2', 'p', 16 | { 17 | name: 'copy', 18 | icon: '📋', 19 | title: 'Copy', 20 | result: () => { 21 | const selection = window.getSelection(); 22 | if (!selection.toString().length) { 23 | const range = document.createRange(); 24 | range.selectNodeContents(editor2.refs.editor); 25 | selection.removeAllRanges(); 26 | selection.addRange(range); 27 | } 28 | editor2.exec('copy'); 29 | } 30 | } 31 | ], 32 | html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean a odio neque. Duis ac laoreet lacus.', 33 | height: '150px' 34 | } 35 | }); 36 | 37 | editor2.$on('change', (event) => { 38 | console.log(event.detail); 39 | }) 40 | 41 | function showEditor() { 42 | let html = inlineEdit.innerHTML; 43 | inlineEdit.innerHTML = ''; 44 | inlineEditor = new Editor({ 45 | target: inlineEdit, 46 | props: { 47 | actions: ['b', 'i', 'u', 'strike', 'removeFormat'], 48 | height: 'auto', 49 | html: html 50 | } 51 | }); 52 | 53 | inlineEdit.removeEventListener('click', showEditor); 54 | 55 | inlineEditor.$on('blur', () => { 56 | html = inlineEditor.getHtml(); 57 | inlineEditor.$destroy(); 58 | inlineEdit.innerHTML = html; 59 | inlineEdit.addEventListener('click', showEditor); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cl-editor", 3 | "version": "2.3.0", 4 | "description": "Lightweight text editor built with svelte + typescript", 5 | "scripts": { 6 | "dev": "concurrently \"rollup -c rollup.config.js \" \"live-server ./dist/ --port=3000\"", 7 | "prod": "rollup -c rollup.config.js", 8 | "test": "mocha", 9 | "prepublishOnly": "npm run prod" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/nenadpnc/cl-editor" 14 | }, 15 | "keywords": [ 16 | "html text editor", 17 | "wysiwyg", 18 | "wysiwyg-html-editor", 19 | "svelte", 20 | "typescript" 21 | ], 22 | "author": "nenadpnc", 23 | "license": "MIT", 24 | "homepage": "https://nenadpnc.github.io/cl-editor/", 25 | "devDependencies": { 26 | "@babel/core": "^7.16.0", 27 | "babel-plugin-external-helpers": "^6.22.0", 28 | "babel-preset-env": "^1.7.0", 29 | "babel-runtime": "^6.26.0", 30 | "browser-env": "^3.3.0", 31 | "bundlesize": "^0.18.1", 32 | "chai": "^4.3.4", 33 | "concurrently": "^6.4.0", 34 | "copyfiles": "^2.4.1", 35 | "cross-env": "^7.0.3", 36 | "install": "^0.13.0", 37 | "live-server": "^1.2.1", 38 | "mocha": "^9.1.3", 39 | "rollup": "^2.60.2", 40 | "rollup-plugin-babel": "^4.4.0", 41 | "rollup-plugin-commonjs": "^10.1.0", 42 | "rollup-plugin-filesize": "^9.1.1", 43 | "rollup-plugin-node-resolve": "^5.2.0", 44 | "rollup-plugin-svelte": "^7.1.0", 45 | "rollup-plugin-terser": "^7.0.2", 46 | "sinon": "^12.0.1", 47 | "svelte": "^3.44.2", 48 | "typescript": "^4.5.2" 49 | }, 50 | "main": "./dist/index.js", 51 | "svelte": "./src/Editor.svelte", 52 | "types": "./src/Editor.svelte.d.ts", 53 | "files": [ 54 | "dist/*.js", 55 | "dist/*.map", 56 | "src/**/*" 57 | ], 58 | "bundlesize": [ 59 | { 60 | "path": "./dist/index.min.js", 61 | "maxSize": "10 kB" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | ## [](#header-2)Basic Example 6 | 7 | Includes all available actions and default height. 8 | 9 | ```js 10 | const editor = new Editor({ 11 | target: document.getElementById('editor1') 12 | }) 13 | ``` 14 |
15 |
16 |
17 | 18 | ## [](#header-2)Custom action example 19 | 20 | Example of custom _**copy**_ action. 21 | 22 | ```js 23 | const editor2 = new Editor({ 24 | target: document.getElementById('editor2'), 25 | data: { 26 | actions: [ 27 | 'b', 'i', 'u', 'strike', 'h1', 'h2', 'p', 28 | { 29 | name: 'copy', 30 | icon: '📋', 31 | title: 'Copy', 32 | result: () => { 33 | const selection = window.getSelection(); 34 | if (!selection.toString().length) { 35 | const range = document.createRange(); 36 | range.selectNodeContents(editor2.refs.editor); 37 | selection.removeAllRanges(); 38 | selection.addRange(range); 39 | } 40 | editor2.exec('copy'); 41 | } 42 | } 43 | ], 44 | html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean a odio neque. Duis ac laoreet lacus.', 45 | height: '150px' 46 | } 47 | }) 48 | ``` 49 | 50 |
51 |
52 |
53 | 54 | ## [](#header-2)Example using _**blur**_ event 55 | 56 | You can use editor _blur_ event to inline edit text. 57 | 58 | ```html 59 |
60 | Edit this line of text 61 |
62 | ``` 63 | 64 | ```js 65 | let inlineEditor; 66 | const inlineEdit = document.getElementById('inlineEdit'); 67 | inlineEdit.addEventListener('click', showEditor); 68 | 69 | function showEditor() { 70 | let html = inlineEdit.innerHTML; 71 | inlineEdit.innerHTML = ''; 72 | inlineEditor = new Editor({ 73 | target: inlineEdit, 74 | data: { 75 | actions: ['b', 'i', 'u', 'strike', 'removeFormat'], 76 | height: 'auto', 77 | html: html 78 | } 79 | }); 80 | 81 | inlineEdit.removeEventListener('click', showEditor); 82 | 83 | inlineEditor.on('blur', () => { 84 | html = inlineEditor.getHtml(); 85 | inlineEditor.destroy(); 86 | inlineEdit.innerHTML = html; 87 | inlineEdit.addEventListener('click', showEditor); 88 | }); 89 | } 90 | ``` 91 |
92 |
93 |
94 | Edit this line of text 95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /src/helpers/EditorColorPicker.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {#each btns as btn} 5 | 6 | {/each} 7 |
8 |
9 | 10 | 34 | 35 | 96 | -------------------------------------------------------------------------------- /_sass/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Noto Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot'); 6 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'), 7 | local('Noto Sans'), 8 | local('Noto-Sans-regular'), 9 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'), 10 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'), 11 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'), 12 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Noto Sans'; 17 | font-weight: 700; 18 | font-style: normal; 19 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot'); 20 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'), 21 | local('Noto Sans Bold'), 22 | local('Noto-Sans-700'), 23 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'), 24 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'), 25 | url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'), 26 | url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Noto Sans'; 31 | font-weight: 400; 32 | font-style: italic; 33 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot'); 34 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'), 35 | local('Noto Sans Italic'), 36 | local('Noto-Sans-italic'), 37 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'), 38 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'), 39 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'), 40 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Noto Sans'; 45 | font-weight: 700; 46 | font-style: italic; 47 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot'); 48 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'), 49 | local('Noto Sans Bold Italic'), 50 | local('Noto-Sans-700italic'), 51 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'), 52 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'), 53 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'), 54 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg'); 55 | } 56 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% seo %} 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 |
18 |

{{ site.title | default: site.github.repository_name }}

19 |

{{ site.description | default: site.github.project_tagline }}

20 | 21 | {% if site.github.is_project_page %} 22 |

View the Project on GitHub {{ github_name }}

23 | {% endif %} 24 | 25 | {% if site.github.is_user_page %} 26 |

View My GitHub Profile

27 | {% endif %} 28 | 29 | {% if site.show_downloads %} 30 | 35 | {% endif %} 36 |
37 |
38 | 39 | {{ content }} 40 | 41 |
42 | 48 |
49 | 50 | 51 | 52 | 53 | {% if site.google_analytics %} 54 | 63 | {% endif %} 64 | 65 | -------------------------------------------------------------------------------- /_sass/rouge-github.scss: -------------------------------------------------------------------------------- 1 | .highlight table td { padding: 5px; } 2 | .highlight table pre { margin: 0; } 3 | .highlight .cm { 4 | color: #999988; 5 | font-style: italic; 6 | } 7 | .highlight .cp { 8 | color: #999999; 9 | font-weight: bold; 10 | } 11 | .highlight .c1 { 12 | color: #999988; 13 | font-style: italic; 14 | } 15 | .highlight .cs { 16 | color: #999999; 17 | font-weight: bold; 18 | font-style: italic; 19 | } 20 | .highlight .c, .highlight .cd { 21 | color: #999988; 22 | font-style: italic; 23 | } 24 | .highlight .err { 25 | color: #a61717; 26 | background-color: #e3d2d2; 27 | } 28 | .highlight .gd { 29 | color: #000000; 30 | background-color: #ffdddd; 31 | } 32 | .highlight .ge { 33 | color: #000000; 34 | font-style: italic; 35 | } 36 | .highlight .gr { 37 | color: #aa0000; 38 | } 39 | .highlight .gh { 40 | color: #999999; 41 | } 42 | .highlight .gi { 43 | color: #000000; 44 | background-color: #ddffdd; 45 | } 46 | .highlight .go { 47 | color: #888888; 48 | } 49 | .highlight .gp { 50 | color: #555555; 51 | } 52 | .highlight .gs { 53 | font-weight: bold; 54 | } 55 | .highlight .gu { 56 | color: #aaaaaa; 57 | } 58 | .highlight .gt { 59 | color: #aa0000; 60 | } 61 | .highlight .kc { 62 | color: #000000; 63 | font-weight: bold; 64 | } 65 | .highlight .kd { 66 | color: #000000; 67 | font-weight: bold; 68 | } 69 | .highlight .kn { 70 | color: #000000; 71 | font-weight: bold; 72 | } 73 | .highlight .kp { 74 | color: #000000; 75 | font-weight: bold; 76 | } 77 | .highlight .kr { 78 | color: #000000; 79 | font-weight: bold; 80 | } 81 | .highlight .kt { 82 | color: #445588; 83 | font-weight: bold; 84 | } 85 | .highlight .k, .highlight .kv { 86 | color: #000000; 87 | font-weight: bold; 88 | } 89 | .highlight .mf { 90 | color: #009999; 91 | } 92 | .highlight .mh { 93 | color: #009999; 94 | } 95 | .highlight .il { 96 | color: #009999; 97 | } 98 | .highlight .mi { 99 | color: #009999; 100 | } 101 | .highlight .mo { 102 | color: #009999; 103 | } 104 | .highlight .m, .highlight .mb, .highlight .mx { 105 | color: #009999; 106 | } 107 | .highlight .sb { 108 | color: #d14; 109 | } 110 | .highlight .sc { 111 | color: #d14; 112 | } 113 | .highlight .sd { 114 | color: #d14; 115 | } 116 | .highlight .s2 { 117 | color: #d14; 118 | } 119 | .highlight .se { 120 | color: #d14; 121 | } 122 | .highlight .sh { 123 | color: #d14; 124 | } 125 | .highlight .si { 126 | color: #d14; 127 | } 128 | .highlight .sx { 129 | color: #d14; 130 | } 131 | .highlight .sr { 132 | color: #009926; 133 | } 134 | .highlight .s1 { 135 | color: #d14; 136 | } 137 | .highlight .ss { 138 | color: #990073; 139 | } 140 | .highlight .s { 141 | color: #d14; 142 | } 143 | .highlight .na { 144 | color: #008080; 145 | } 146 | .highlight .bp { 147 | color: #999999; 148 | } 149 | .highlight .nb { 150 | color: #0086B3; 151 | } 152 | .highlight .nc { 153 | color: #445588; 154 | font-weight: bold; 155 | } 156 | .highlight .no { 157 | color: #008080; 158 | } 159 | .highlight .nd { 160 | color: #3c5d5d; 161 | font-weight: bold; 162 | } 163 | .highlight .ni { 164 | color: #800080; 165 | } 166 | .highlight .ne { 167 | color: #990000; 168 | font-weight: bold; 169 | } 170 | .highlight .nf { 171 | color: #990000; 172 | font-weight: bold; 173 | } 174 | .highlight .nl { 175 | color: #990000; 176 | font-weight: bold; 177 | } 178 | .highlight .nn { 179 | color: #555555; 180 | } 181 | .highlight .nt { 182 | color: #000080; 183 | } 184 | .highlight .vc { 185 | color: #008080; 186 | } 187 | .highlight .vg { 188 | color: #008080; 189 | } 190 | .highlight .vi { 191 | color: #008080; 192 | } 193 | .highlight .nv { 194 | color: #008080; 195 | } 196 | .highlight .ow { 197 | color: #000000; 198 | font-weight: bold; 199 | } 200 | .highlight .o { 201 | color: #000000; 202 | font-weight: bold; 203 | } 204 | .highlight .w { 205 | color: #bbbbbb; 206 | } 207 | .highlight { 208 | background-color: #f8f8f8; 209 | } 210 | -------------------------------------------------------------------------------- /_sass/jekyll-theme-minimal.scss: -------------------------------------------------------------------------------- 1 | @import "fonts"; 2 | @import "rouge-github"; 3 | 4 | body { 5 | background-color: #fff; 6 | padding:50px; 7 | font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | color:#727272; 9 | font-weight:400; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | color:#222; 14 | margin:0 0 20px; 15 | } 16 | 17 | p, ul, ol, table, pre, dl { 18 | margin:0 0 20px; 19 | } 20 | 21 | h1, h2, h3 { 22 | line-height:1.1; 23 | } 24 | 25 | h1 { 26 | font-size:28px; 27 | } 28 | 29 | h2 { 30 | color:#393939; 31 | } 32 | 33 | h3, h4, h5, h6 { 34 | color:#494949; 35 | } 36 | 37 | a { 38 | color:#267CB9; 39 | text-decoration:none; 40 | } 41 | 42 | a:hover, a:focus { 43 | color:#069; 44 | font-weight: bold; 45 | } 46 | 47 | a small { 48 | font-size:11px; 49 | color:#777; 50 | margin-top:-0.3em; 51 | display:block; 52 | } 53 | 54 | a:hover small { 55 | color:#777; 56 | } 57 | 58 | .wrapper { 59 | width:860px; 60 | margin:0 auto; 61 | } 62 | 63 | blockquote { 64 | border-left:1px solid #e5e5e5; 65 | margin:0; 66 | padding:0 0 0 20px; 67 | font-style:italic; 68 | } 69 | 70 | code, pre { 71 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace; 72 | color:#333; 73 | font-size:12px; 74 | } 75 | 76 | pre { 77 | padding:8px 15px; 78 | background: #f8f8f8; 79 | border-radius:5px; 80 | border:1px solid #e5e5e5; 81 | overflow-x: auto; 82 | } 83 | 84 | table { 85 | width:100%; 86 | border-collapse:collapse; 87 | } 88 | 89 | th, td { 90 | text-align:left; 91 | padding:5px 10px; 92 | border-bottom:1px solid #e5e5e5; 93 | } 94 | 95 | dt { 96 | color:#444; 97 | font-weight:700; 98 | } 99 | 100 | th { 101 | color:#444; 102 | } 103 | 104 | img { 105 | max-width:100%; 106 | } 107 | 108 | header { 109 | width:270px; 110 | float:left; 111 | position:fixed; 112 | -webkit-font-smoothing:subpixel-antialiased; 113 | } 114 | 115 | header ul { 116 | list-style:none; 117 | height:40px; 118 | padding:0; 119 | background: #f4f4f4; 120 | border-radius:5px; 121 | border:1px solid #e0e0e0; 122 | width:270px; 123 | } 124 | 125 | header li { 126 | width:89px; 127 | float:left; 128 | border-right:1px solid #e0e0e0; 129 | height:40px; 130 | } 131 | 132 | header li:first-child a { 133 | border-radius:5px 0 0 5px; 134 | } 135 | 136 | header li:last-child a { 137 | border-radius:0 5px 5px 0; 138 | } 139 | 140 | header ul a { 141 | line-height:1; 142 | font-size:11px; 143 | color:#676767; 144 | display:block; 145 | text-align:center; 146 | padding-top:6px; 147 | height:34px; 148 | } 149 | 150 | header ul a:hover, header ul a:focus { 151 | color:#675C5C; 152 | font-weight:bold; 153 | } 154 | 155 | header ul a:active { 156 | background-color:#f0f0f0; 157 | } 158 | 159 | strong { 160 | color:#222; 161 | font-weight:700; 162 | } 163 | 164 | header ul li + li + li { 165 | border-right:none; 166 | width:89px; 167 | } 168 | 169 | header ul a strong { 170 | font-size:14px; 171 | display:block; 172 | color:#222; 173 | } 174 | 175 | section { 176 | width:500px; 177 | float:right; 178 | padding-bottom:50px; 179 | } 180 | 181 | small { 182 | font-size:11px; 183 | } 184 | 185 | hr { 186 | border:0; 187 | background:#e5e5e5; 188 | height:1px; 189 | margin:0 0 20px; 190 | } 191 | 192 | footer { 193 | width:270px; 194 | float:left; 195 | position:fixed; 196 | bottom:50px; 197 | -webkit-font-smoothing:subpixel-antialiased; 198 | } 199 | 200 | @media print, screen and (max-width: 960px) { 201 | 202 | div.wrapper { 203 | width:auto; 204 | margin:0; 205 | } 206 | 207 | header, section, footer { 208 | float:none; 209 | position:static; 210 | width:auto; 211 | } 212 | 213 | header { 214 | padding-right:320px; 215 | } 216 | 217 | section { 218 | border:1px solid #e5e5e5; 219 | border-width:1px 0; 220 | padding:20px 0; 221 | margin:0 0 20px; 222 | } 223 | 224 | header a small { 225 | display:inline; 226 | } 227 | 228 | header ul { 229 | position:absolute; 230 | right:50px; 231 | top:52px; 232 | } 233 | } 234 | 235 | @media print, screen and (max-width: 720px) { 236 | body { 237 | word-wrap:break-word; 238 | } 239 | 240 | header { 241 | padding:0; 242 | } 243 | 244 | header ul, header p.view { 245 | position:static; 246 | } 247 | 248 | pre, code { 249 | word-wrap:normal; 250 | } 251 | } 252 | 253 | @media print, screen and (max-width: 480px) { 254 | body { 255 | padding:15px; 256 | } 257 | 258 | header ul { 259 | width:99%; 260 | } 261 | 262 | header li, header ul li + li + li { 263 | width:33%; 264 | } 265 | } 266 | 267 | @media print { 268 | body { 269 | padding:0.4in; 270 | font-size:12pt; 271 | color:#444; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/helpers/EditorModal.svelte: -------------------------------------------------------------------------------- 1 | 2 | {#if show} 3 |
4 |
5 | 21 |
22 | {/if} 23 | 24 | 70 | 71 | 210 | -------------------------------------------------------------------------------- /src/helpers/util.js: -------------------------------------------------------------------------------- 1 | let t = {}; 2 | 3 | export const exec = (command, value = null) => { 4 | document.execCommand(command, false, value) 5 | } 6 | 7 | export const getTagsRecursive = (element, tags) => { 8 | tags = tags || (element && element.tagName ? [element.tagName] : []); 9 | 10 | if (element && element.parentNode) { 11 | element = element.parentNode; 12 | } else { 13 | return tags; 14 | } 15 | 16 | const tag = element.tagName; 17 | if (element.style && element.getAttribute) { 18 | [element.style.textAlign || element.getAttribute('align'), element.style.color || tag === 'FONT' && 'forecolor', element.style.backgroundColor && 'backcolor'] 19 | .filter((item) => item) 20 | .forEach((item) => tags.push(item)); 21 | } 22 | 23 | if (tag === 'DIV') { 24 | return tags; 25 | } 26 | 27 | tags.push(tag); 28 | 29 | return getTagsRecursive(element, tags).filter((_tag) => _tag != null); 30 | } 31 | 32 | export const saveRange = (editor) => { 33 | const documentSelection = document.getSelection(); 34 | 35 | t.range = null; 36 | 37 | if (documentSelection.rangeCount) { 38 | let savedRange = t.range = documentSelection.getRangeAt(0); 39 | let range = document.createRange(); 40 | let rangeStart; 41 | range.selectNodeContents(editor); 42 | range.setEnd(savedRange.startContainer, savedRange.startOffset); 43 | rangeStart = (range + '').length; 44 | t.metaRange = { 45 | start: rangeStart, 46 | end: rangeStart + (savedRange + '').length 47 | }; 48 | } 49 | } 50 | export const restoreRange = (editor) => { 51 | let metaRange = t.metaRange; 52 | let savedRange = t.range; 53 | let documentSelection = document.getSelection(); 54 | let range; 55 | 56 | if (!savedRange) { 57 | return; 58 | } 59 | 60 | if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/ 61 | let charIndex = 0, 62 | nodeStack = [editor], 63 | node, 64 | foundStart = false, 65 | stop = false; 66 | 67 | range = document.createRange(); 68 | 69 | while (!stop && (node = nodeStack.pop())) { 70 | if (node.nodeType === 3) { 71 | let nextCharIndex = charIndex + node.length; 72 | if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) { 73 | range.setStart(node, metaRange.start - charIndex); 74 | foundStart = true; 75 | } 76 | if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) { 77 | range.setEnd(node, metaRange.end - charIndex); 78 | stop = true; 79 | } 80 | charIndex = nextCharIndex; 81 | } else { 82 | let cn = node.childNodes; 83 | let i = cn.length; 84 | 85 | while (i > 0) { 86 | i -= 1; 87 | nodeStack.push(cn[i]); 88 | } 89 | } 90 | } 91 | } 92 | 93 | documentSelection.removeAllRanges(); 94 | documentSelection.addRange(range || savedRange); 95 | } 96 | 97 | export const cleanHtml = (input) => { 98 | const html = input.match(/(.*?)/); 99 | let output = html && html[1] || input; 100 | output = output 101 | .replace(/\r?\n|\r/g, ' ') 102 | .replace(//g, '') 103 | .replace(new RegExp('<(/)*(meta|link|span|\\?xml:|st1:|o:|font|w:sdt)(.*?)>', 'gi'), '') 104 | .replace(/(.*?)/gi, '') 105 | .replace(/style="[^"]*"/gi, '') 106 | .replace(/style='[^']*'/gi, '') 107 | .replace(/ /gi, ' ') 108 | .replace(/>(\s+)<') 109 | .replace(/class="[^"]*"/gi, '') 110 | .replace(/class='[^']*'/gi, '') 111 | .replace(/<[^/].*?>/g, i => i.split(/[ >]/g)[0] + '>') 112 | .trim() 113 | 114 | output = removeBadTags(output); 115 | return output; 116 | } 117 | 118 | export const unwrap = (wrapper) => { 119 | const docFrag = document.createDocumentFragment(); 120 | while (wrapper.firstChild) { 121 | const child = wrapper.removeChild(wrapper.firstChild); 122 | docFrag.appendChild(child); 123 | } 124 | 125 | // replace wrapper with document fragment 126 | wrapper.parentNode.replaceChild(docFrag, wrapper); 127 | } 128 | 129 | export const removeBlockTagsRecursive = (elements, tagsToRemove) => { 130 | Array.from(elements).forEach((item) => { 131 | if (tagsToRemove.some((tag) => tag === item.tagName.toLowerCase())) { 132 | if (item.children.length) { 133 | removeBlockTagsRecursive(item.children, tagsToRemove); 134 | } 135 | unwrap(item); 136 | } 137 | }); 138 | } 139 | 140 | export const getActionBtns = (actions) => { 141 | return Object.keys(actions).map((action) => actions[action]); 142 | } 143 | 144 | export const getNewActionObj = (actions, userActions = []) => { 145 | if (userActions && userActions.length) { 146 | const newActions = {}; 147 | userActions.forEach((action) => { 148 | if (typeof action === 'string') { 149 | newActions[action] = Object.assign({}, actions[action]); 150 | } else if (actions[action.name]) { 151 | newActions[action.name] = Object.assign(actions[action.name], action); 152 | } else { 153 | newActions[action.name] = Object.assign({}, action); 154 | } 155 | }); 156 | 157 | return newActions; 158 | } else { 159 | return actions; 160 | } 161 | } 162 | 163 | export const removeBadTags = (html) => { 164 | ['style', 'script', 'applet', 'embed', 'noframes', 'noscript'].forEach((badTag) => { 165 | html = html.replace(new RegExp(`<${badTag}.*?${badTag}(.*?)>`, 'gi'), '') 166 | }); 167 | 168 | return html; 169 | } 170 | 171 | export const isEditorClick = (target, editorWrapper) => { 172 | if (target === editorWrapper) { 173 | return true; 174 | } 175 | if (target.parentElement) { 176 | return isEditorClick(target.parentElement, editorWrapper); 177 | } 178 | return false; 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lightweight text editor 2 | 3 | Built with svelte (no external dependencies) 4 | 5 | #### File size (bundle includes css, html and js) 6 | * min: 30kb 7 | * gzip: 10kb 8 | 9 | ## Installation 10 | 11 | #### npm: 12 | 13 | ```bash 14 | npm install --save cl-editor 15 | ``` 16 | 17 | #### HTML: 18 | 19 | ```html 20 | 21 | ... 22 | 23 | 24 | ... 25 |
26 | ... 27 | 28 | ``` 29 | 30 | ## Usage 31 | ```js 32 | import Editor from 'cl-editor'; 33 | // or 34 | const Editor = require('cl-editor'); 35 | ``` 36 | ```js 37 | // Initialize editor 38 | const editor = new Editor({ 39 | // required 40 | target: document.getElementById('editor'), 41 | // optional 42 | props: { 43 | // string if overwriting, object if customizing/creating 44 | // available actions: 45 | // 'viewHtml', 'undo', 'redo', 'b', 'i', 'u', 'strike', 'sup', 'sub', 'h1', 'h2', 'p', 'blockquote', 46 | // 'ol', 'ul', 'hr', 'left', 'right', 'center', 'justify', 'a', 'image', 'forecolor', 'backcolor', 'removeFormat' 47 | actions: [ 48 | 'b', 'i', 'u', 'strike', 'ul', 'ol', 49 | { 50 | name: 'copy', // required 51 | icon: 'C', // string or html string (ex. ...) 52 | title: 'Copy', 53 | result: () => { 54 | // copy current selection or whole editor content 55 | const selection = window.getSelection(); 56 | if (!selection.toString().length) { 57 | const range = document.createRange(); 58 | range.selectNodeContents(editor.refs.editor); 59 | selection.removeAllRanges(); 60 | selection.addRange(range); 61 | } 62 | editor.exec('copy'); 63 | } 64 | }, 65 | 'h1', 'h2', 'p' 66 | ], 67 | // default 300px 68 | height: '300px', 69 | // initial html 70 | html: '', 71 | // remove format action clears formatting, but also removes some html tags. 72 | // you can specify which tags you want to be removed. 73 | removeFormatTags: ['h1', 'h2', 'blackquote'] // default 74 | } 75 | }) 76 | ``` 77 | 78 | ### API 79 | ```js 80 | // Methods 81 | editor.exec(cmd: string, value?: string) // execute document command (document.executeCommand(cmd, false, value)) 82 | editor.getHtml(sanitize?: boolean) // returns html string from editor. if passed true as argument, html will be sanitized before return 83 | editor.getText() // returns text string from editor 84 | editor.setHtml(html: string, sanitize?: boolean) // sets html for editor. if second argument is true, html will be sanitized 85 | editor.saveRange() // saves current editor cursor position or user selection 86 | editor.restoreRange() // restores cursor position or user selection 87 | // saveRange and restoreRange are useful when making custom actions 88 | // that demands that focus is shifted from editor to, for example, modal window. 89 | ``` 90 | * For list of available _**exec**_ command visit [https://codepen.io/netsi1964/pen/QbLLG](https://codepen.io/netsi1964/pen/QbLLGW) 91 | ```js 92 | // Events 93 | editor.$on('change', (event) => console.log(event)) // on every keyup event 94 | editor.$on('blur', (event) => console.log(event)) // on editor blur event 95 | ``` 96 | ```js 97 | // Props 98 | editor.refs. // references to editor, raw (textarea), modal and colorPicker HTMLElements 99 | ``` 100 | 101 | #### Actions 102 | 103 | The `actions` prop lists predefined actions (and/or adds new actions) to be shown in the toolbar. 104 | If the prop is not set, all `actions` defined and exported in [actions.js](src/helpers/actions.js) are made available, in the order in which they are defined. 105 | To limit or change the order of predefined actions shown, set it by passing an array of names of actions defined, eg.: 106 | ```js 107 | actions={["b", "i", "u", "h2", "ul", "left", "center", "justify", "forecolor"]} 108 | ``` 109 | The editor looks up to see if name is already defined, and adds it to the toolbar if it is. 110 | 111 | You can add a custom action by inserting it in the array, like how "copy" is defined in example above. Take a look at `actions.js` for more examples. 112 | 113 | 114 | ### Usage in Svelte 115 | 116 | It is easier to import and work directly from the source if you are using Svelte. You can handle `change` events via `on:change`. 117 | 118 | ```jsx 119 | 126 | 127 | {@html html} 128 | html = evt.detail}/> 129 | ``` 130 | 131 | ### Example of customising the color picker palette 132 | 133 | ```jsx 134 | 146 | 147 | {@html html} 148 | html = evt.detail}/> 149 | ``` 150 | 151 | To limit or define the tools shown in the toolbar, pass in an `actions` prop. 152 | 153 | To easily get the editor content DOM element, pass an `contentId` prop, eg. `contentId='notes-content'`. 154 | 155 | This is useful if you want to listen to resize of the editor and respond accordingly. 156 | 157 | To do so, first enable resize on the editor: 158 | 159 | ```css 160 | .cl-content { 161 | resize: both; 162 | } 163 | ``` 164 | 165 | Now observe the resize: 166 | 167 | ```jsx 168 | 176 | 177 | 178 | ``` 179 | 180 | ### Run demo 181 | ```bash 182 | git clone https://github.com/nenadpnc/cl-text-editor.git cl-editor 183 | cd cl-editor 184 | npm i 185 | npm run dev 186 | ``` 187 | 188 | ## References 189 | This library is inspired by these open source repos: 190 | - [Alex-D/Trumbowyg](https://github.com/Alex-D/Trumbowyg) 191 | - [jaredreich/pell](https://github.com/jaredreich/pell) 192 | 193 | ## Licence 194 | 195 | MIT License 196 | -------------------------------------------------------------------------------- /src/Editor.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 |
8 | {#each $state.actionBtns as action} 9 | 16 | {/each} 17 |
18 |
27 |
28 | 29 | 30 | 31 | 32 |
33 | 34 | 158 | 159 | 219 | -------------------------------------------------------------------------------- /src/helpers/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | exec, 3 | removeBlockTagsRecursive, 4 | getActionBtns, 5 | saveRange, 6 | restoreRange 7 | } from "./util"; 8 | 9 | import { get } from "svelte/store"; 10 | 11 | const linkSvg = 12 | ''; 13 | const unlinkSvg = 14 | ''; 15 | 16 | export default { 17 | viewHtml: { 18 | icon: 19 | '', 20 | title: "View HTML", 21 | result: function() { 22 | let refs = get(this.references); 23 | let actionObj = get(this.state).actionObj; 24 | let helper = get(this.helper); 25 | 26 | helper.showEditor = !helper.showEditor; 27 | refs.editor.style.display = helper.showEditor ? "block" : "none"; 28 | refs.raw.style.display = helper.showEditor ? "none" : "block"; 29 | if (helper.showEditor) { 30 | refs.editor.innerHTML = refs.raw.value; 31 | } else { 32 | refs.raw.value = refs.editor.innerHTML; 33 | } 34 | setTimeout(() => { 35 | Object.keys(actionObj).forEach( 36 | action => (actionObj[action].disabled = !helper.showEditor) 37 | ); 38 | actionObj.viewHtml.disabled = false; 39 | actionObj.viewHtml.active = !helper.showEditor; 40 | 41 | this.state.update(state => { 42 | state.actionBtns = getActionBtns(actionObj); 43 | state.actionObj = actionObj; 44 | return state; 45 | }); 46 | }); 47 | } 48 | }, 49 | undo: { 50 | icon: 51 | '', 52 | title: "Undo", 53 | result: () => exec("undo") 54 | }, 55 | redo: { 56 | icon: 57 | '', 58 | title: "Redo", 59 | result: () => exec("redo") 60 | }, 61 | b: { 62 | icon: "B", 63 | title: "Bold", 64 | result: () => exec("bold") 65 | }, 66 | i: { 67 | icon: "I", 68 | title: "Italic", 69 | result: () => exec("italic") 70 | }, 71 | u: { 72 | icon: "U", 73 | title: "Underline", 74 | result: () => exec("underline") 75 | }, 76 | strike: { 77 | icon: "S", 78 | title: "Strike-through", 79 | result: () => exec("strikeThrough") 80 | }, 81 | sup: { 82 | icon: "A2", 83 | title: "Superscript", 84 | result: () => exec("superscript") 85 | }, 86 | sub: { 87 | icon: "A2", 88 | title: "Subscript", 89 | result: () => exec("subscript") 90 | }, 91 | h1: { 92 | icon: "H1", 93 | title: "Heading 1", 94 | result: () => exec("formatBlock", "

") 95 | }, 96 | h2: { 97 | icon: "H2", 98 | title: "Heading 2", 99 | result: () => exec("formatBlock", "

") 100 | }, 101 | p: { 102 | icon: "¶", 103 | title: "Paragraph", 104 | result: () => exec("formatBlock", "

") 105 | }, 106 | blockquote: { 107 | icon: "“ ”", 108 | title: "Quote", 109 | result: () => exec("formatBlock", "

") 110 | }, 111 | ol: { 112 | icon: 113 | '', 114 | title: "Ordered List", 115 | result: () => exec("insertOrderedList") 116 | }, 117 | ul: { 118 | icon: 119 | '', 120 | title: "Unordered List", 121 | result: () => exec("insertUnorderedList") 122 | }, 123 | hr: { 124 | icon: "―", 125 | title: "Horizontal Line", 126 | result: () => exec("insertHorizontalRule") 127 | }, 128 | left: { 129 | icon: 130 | '', 131 | title: "Justify left", 132 | result: () => exec("justifyLeft") 133 | }, 134 | right: { 135 | icon: 136 | '', 137 | title: "Justify right", 138 | result: () => exec("justifyRight") 139 | }, 140 | center: { 141 | icon: 142 | '', 143 | title: "Justify center", 144 | result: () => exec("justifyCenter") 145 | }, 146 | justify: { 147 | icon: 148 | '', 149 | title: "Justify full", 150 | result: () => exec("justifyFull") 151 | }, 152 | a: { 153 | icon: linkSvg, 154 | title: "Insert link", 155 | result: function() { 156 | const actionObj = get(this.state).actionObj; 157 | const refs = get(this.references); 158 | 159 | if (actionObj.a.active) { 160 | const selection = window.getSelection(); 161 | const range = document.createRange(); 162 | range.selectNodeContents(document.getSelection().focusNode); 163 | selection.removeAllRanges(); 164 | selection.addRange(range); 165 | exec("unlink"); 166 | actionObj.a.title = "Insert link"; 167 | actionObj.a.icon = linkSvg; 168 | this.state.update(state => { 169 | state.actionBtn = getActionBtns(actionObj); 170 | state.actionObj = actionObj; 171 | return state; 172 | }); 173 | } else { 174 | saveRange(refs.editor); 175 | refs.modal.$set({ 176 | show: true, 177 | event: "linkUrl", 178 | title: "Insert link", 179 | label: "Url" 180 | }); 181 | if (!get(this.helper).link) { 182 | this.helper.update(state => { 183 | state.link = true; 184 | return state; 185 | }); 186 | refs.modal.$on("linkUrl", event => { 187 | restoreRange(refs.editor); 188 | exec("createLink", event.detail); 189 | actionObj.a.title = "Unlink"; 190 | actionObj.a.icon = unlinkSvg; 191 | 192 | this.state.update(state => { 193 | state.actionBtn = getActionBtns(actionObj); 194 | state.actionObj = actionObj; 195 | return state; 196 | }); 197 | }); 198 | } 199 | } 200 | } 201 | }, 202 | image: { 203 | icon: 204 | '', 205 | title: "Image", 206 | result: function() { 207 | const refs = get(this.references); 208 | saveRange(refs.editor); 209 | refs.modal.$set({ 210 | show: true, 211 | event: "imageUrl", 212 | title: "Insert image", 213 | label: "Url" 214 | }); 215 | if (!get(this.helper).image) { 216 | this.helper.update(state => { 217 | state.image = true; 218 | return state; 219 | }); 220 | refs.modal.$on("imageUrl", event => { 221 | restoreRange(refs.editor); 222 | exec("insertImage", event.detail); 223 | }); 224 | } 225 | } 226 | }, 227 | forecolor: { 228 | icon: 229 | '', 230 | title: "Text color", 231 | colorPicker: true, 232 | result: function() { 233 | showColorPicker.call(this, "foreColor"); 234 | } 235 | }, 236 | backcolor: { 237 | icon: 238 | '', 239 | title: "Background color", 240 | colorPicker: true, 241 | result: function() { 242 | showColorPicker.call(this, "backColor"); 243 | } 244 | }, 245 | removeFormat: { 246 | icon: 247 | '', 248 | title: "Remove format", 249 | result: function() { 250 | const refs = get(this.references); 251 | const selection = window.getSelection(); 252 | if (!selection.toString().length) { 253 | removeBlockTagsRecursive( 254 | refs.editor.children, 255 | this.removeFormatTags 256 | ); 257 | const range = document.createRange(); 258 | range.selectNodeContents(refs.editor); 259 | selection.removeAllRanges(); 260 | selection.addRange(range); 261 | } 262 | exec("removeFormat"); 263 | selection.removeAllRanges(); 264 | } 265 | } 266 | }; 267 | 268 | const showColorPicker = function(cmd) { 269 | const refs = get(this.references); 270 | saveRange(refs.editor); 271 | refs.colorPicker.$set({show: true, event: cmd}); 272 | if (!get(this.helper)[cmd]) { 273 | this.helper.update(state => { 274 | state[cmd] = true; 275 | return state; 276 | }); 277 | refs.colorPicker.$on(cmd, event => { 278 | let item = event.detail; 279 | if (item.modal) { 280 | refs.modal.$set({ 281 | show: true, 282 | event: `${cmd}Changed`, 283 | title: "Text color", 284 | label: 285 | cmd === "foreColor" ? "Text color" : "Background color" 286 | }); 287 | const command = cmd; 288 | if (!get(this.helper)[`${command}Modal`]) { 289 | get(this.helper)[`${command}Modal`] = true; 290 | refs.modal.$on(`${command}Changed`, event => { 291 | let color = event.detail; 292 | restoreRange(refs.editor); 293 | exec(command, color); 294 | }); 295 | } 296 | } else { 297 | restoreRange(refs.editor); 298 | exec(cmd, item.color); 299 | } 300 | }); 301 | } 302 | }; 303 | -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).clEditor=e()}(this,(function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function o(t){t.forEach(e)}function i(t){return"function"==typeof t}function l(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function r(e,...n){if(null==e)return t;const o=e.subscribe(...n);return o.unsubscribe?()=>o.unsubscribe():o}function s(t){let e;return r(t,(t=>e=t))(),e}function c(t,e,n){t.$$.on_destroy.push(r(e,n))}function a(t,e,n){return t.set(n),e}function d(t,e){t.appendChild(e)}function h(t,e,n){const o=function(t){if(!t)return document;const e=t.getRootNode?t.getRootNode():t.ownerDocument;if(e&&e.host)return e;return t.ownerDocument}(t);if(!o.getElementById(e)){const t=g("style");t.id=e,t.textContent=n,function(t,e){d(t.head||t,e)}(o,t)}}function u(t,e,n){t.insertBefore(e,n||null)}function f(t){t.parentNode.removeChild(t)}function p(t,e){for(let n=0;nt.removeEventListener(e,n,o)}function x(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function y(t,e){e=""+e,t.wholeText!==e&&(t.data=e)}function $(t,e){t.value=null==e?"":e}function w(t,e,n,o){t.style.setProperty(e,n,o?"important":"")}function k(t,e,n){t.classList[n?"add":"remove"](e)}class j{constructor(){this.e=this.n=null}c(t){this.h(t)}m(t,e,n=null){this.e||(this.e=g(e.nodeName),this.t=e,this.c(t)),this.i(n)}h(t){this.e.innerHTML=t,this.n=Array.from(this.e.childNodes)}i(t){for(let e=0;e{const o=t.$$.callbacks[e];if(o){const i=function(t,e,n=!1){const o=document.createEvent("CustomEvent");return o.initCustomEvent(t,n,!1,e),o}(e,n);o.slice().forEach((e=>{e.call(t,i)}))}}}const L=[],C=[],T=[],B=[],F=Promise.resolve();let R=!1;function O(t){T.push(t)}let N=!1;const _=new Set;function A(){if(!N){N=!0;do{for(let t=0;t{I.delete(t),o&&(n&&t.d(1),o())})),t.o(e)}}function q(t){t&&t.c()}function V(t,n,l,r){const{fragment:s,on_mount:c,on_destroy:a,after_update:d}=t.$$;s&&s.m(n,l),r||O((()=>{const n=c.map(e).filter(i);a?a.push(...n):o(n),t.$$.on_mount=[]})),d.forEach(O)}function P(t,e){const n=t.$$;null!==n.fragment&&(o(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function X(t,e){-1===t.$$.dirty[0]&&(L.push(t),R||(R=!0,F.then(A)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const i=o.length?o[0]:n;return u.ctx&&s(u.ctx[t],u.ctx[t]=i)&&(!u.skip_bound&&u.bound[t]&&u.bound[t](i),p&&X(e,t)),n})):[],u.update(),p=!0,o(u.before_update),u.fragment=!!r&&r(u.ctx),i.target){if(i.hydrate){const t=function(t){return Array.from(t.childNodes)}(i.target);u.fragment&&u.fragment.l(t),t.forEach(f)}else u.fragment&&u.fragment.c();i.intro&&S(e.$$.fragment),V(e,i.target,i.anchor,i.customElement),A()}H(h)}class Q{$destroy(){P(this,1),this.$destroy=t}$on(t,e){const n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}let W={};const K=(t,e=null)=>{document.execCommand(t,!1,e)},G=(t,e)=>{if(e=e||(t&&t.tagName?[t.tagName]:[]),!t||!t.parentNode)return e;const n=(t=t.parentNode).tagName;return t.style&&t.getAttribute&&[t.style.textAlign||t.getAttribute("align"),t.style.color||"FONT"===n&&"forecolor",t.style.backgroundColor&&"backcolor"].filter((t=>t)).forEach((t=>e.push(t))),"DIV"===n?e:(e.push(n),G(t,e).filter((t=>null!=t)))},Y=t=>{const e=document.getSelection();if(W.range=null,e.rangeCount){let n,o=W.range=e.getRangeAt(0),i=document.createRange();i.selectNodeContents(t),i.setEnd(o.startContainer,o.startOffset),n=(i+"").length,W.metaRange={start:n,end:n+(o+"").length}}},Z=t=>{let e,n=W.metaRange,o=W.range,i=document.getSelection();if(o){if(n&&n.start!==n.end){let o,i=0,l=[t],r=!1,s=!1;for(e=document.createRange();!s&&(o=l.pop());)if(3===o.nodeType){let t=i+o.length;!r&&n.start>=i&&n.start<=t&&(e.setStart(o,n.start-i),r=!0),r&&n.end>=i&&n.end<=t&&(e.setEnd(o,n.end-i),s=!0),i=t}else{let t=o.childNodes,e=t.length;for(;e>0;)e-=1,l.push(t[e])}}i.removeAllRanges(),i.addRange(e||o)}},tt=(t,e)=>{Array.from(t).forEach((t=>{e.some((e=>e===t.tagName.toLowerCase()))&&(t.children.length&&tt(t.children,e),(t=>{const e=document.createDocumentFragment();for(;t.firstChild;){const n=t.removeChild(t.firstChild);e.appendChild(n)}t.parentNode.replaceChild(e,t)})(t))}))},et=t=>Object.keys(t).map((e=>t[e])),nt=t=>(["style","script","applet","embed","noframes","noscript"].forEach((e=>{t=t.replace(new RegExp(`<${e}.*?${e}(.*?)>`,"gi"),"")})),t),ot=(t,e)=>t===e||!!t.parentElement&&ot(t.parentElement,e),it=[];function lt(e,n=t){let o;const i=new Set;function r(t){if(l(e,t)&&(e=t,o)){const t=!it.length;for(const t of i)t[1](),it.push(t,e);if(t){for(let t=0;t{i.delete(c),0===i.size&&(o(),o=null)}}}}const rt='';var st={viewHtml:{icon:'',title:"View HTML",result:function(){let t=s(this.references),e=s(this.state).actionObj,n=s(this.helper);n.showEditor=!n.showEditor,t.editor.style.display=n.showEditor?"block":"none",t.raw.style.display=n.showEditor?"none":"block",n.showEditor?t.editor.innerHTML=t.raw.value:t.raw.value=t.editor.innerHTML,setTimeout((()=>{Object.keys(e).forEach((t=>e[t].disabled=!n.showEditor)),e.viewHtml.disabled=!1,e.viewHtml.active=!n.showEditor,this.state.update((t=>(t.actionBtns=et(e),t.actionObj=e,t)))}))}},undo:{icon:'',title:"Undo",result:()=>K("undo")},redo:{icon:'',title:"Redo",result:()=>K("redo")},b:{icon:"B",title:"Bold",result:()=>K("bold")},i:{icon:"I",title:"Italic",result:()=>K("italic")},u:{icon:"U",title:"Underline",result:()=>K("underline")},strike:{icon:"S",title:"Strike-through",result:()=>K("strikeThrough")},sup:{icon:"A2",title:"Superscript",result:()=>K("superscript")},sub:{icon:"A2",title:"Subscript",result:()=>K("subscript")},h1:{icon:"H1",title:"Heading 1",result:()=>K("formatBlock","

")},h2:{icon:"H2",title:"Heading 2",result:()=>K("formatBlock","

")},p:{icon:"¶",title:"Paragraph",result:()=>K("formatBlock","

")},blockquote:{icon:"“ ”",title:"Quote",result:()=>K("formatBlock","

")},ol:{icon:'',title:"Ordered List",result:()=>K("insertOrderedList")},ul:{icon:'',title:"Unordered List",result:()=>K("insertUnorderedList")},hr:{icon:"―",title:"Horizontal Line",result:()=>K("insertHorizontalRule")},left:{icon:'',title:"Justify left",result:()=>K("justifyLeft")},right:{icon:'',title:"Justify right",result:()=>K("justifyRight")},center:{icon:'',title:"Justify center",result:()=>K("justifyCenter")},justify:{icon:'',title:"Justify full",result:()=>K("justifyFull")},a:{icon:rt,title:"Insert link",result:function(){const t=s(this.state).actionObj,e=s(this.references);if(t.a.active){const e=window.getSelection(),n=document.createRange();n.selectNodeContents(document.getSelection().focusNode),e.removeAllRanges(),e.addRange(n),K("unlink"),t.a.title="Insert link",t.a.icon=rt,this.state.update((e=>(e.actionBtn=et(t),e.actionObj=t,e)))}else Y(e.editor),e.modal.$set({show:!0,event:"linkUrl",title:"Insert link",label:"Url"}),s(this.helper).link||(this.helper.update((t=>(t.link=!0,t))),e.modal.$on("linkUrl",(n=>{Z(e.editor),K("createLink",n.detail),t.a.title="Unlink",t.a.icon='',this.state.update((e=>(e.actionBtn=et(t),e.actionObj=t,e)))})))}},image:{icon:'',title:"Image",result:function(){const t=s(this.references);Y(t.editor),t.modal.$set({show:!0,event:"imageUrl",title:"Insert image",label:"Url"}),s(this.helper).image||(this.helper.update((t=>(t.image=!0,t))),t.modal.$on("imageUrl",(e=>{Z(t.editor),K("insertImage",e.detail)})))}},forecolor:{icon:'',title:"Text color",colorPicker:!0,result:function(){ct.call(this,"foreColor")}},backcolor:{icon:'',title:"Background color",colorPicker:!0,result:function(){ct.call(this,"backColor")}},removeFormat:{icon:'',title:"Remove format",result:function(){const t=s(this.references),e=window.getSelection();if(!e.toString().length){tt(t.editor.children,this.removeFormatTags);const n=document.createRange();n.selectNodeContents(t.editor),e.removeAllRanges(),e.addRange(n)}K("removeFormat"),e.removeAllRanges()}}};const ct=function(t){const e=s(this.references);Y(e.editor),e.colorPicker.$set({show:!0,event:t}),s(this.helper)[t]||(this.helper.update((e=>(e[t]=!0,e))),e.colorPicker.$on(t,(n=>{let o=n.detail;if(o.modal){e.modal.$set({show:!0,event:`${t}Changed`,title:"Text color",label:"foreColor"===t?"Text color":"Background color"});const n=t;s(this.helper)[`${n}Modal`]||(s(this.helper)[`${n}Modal`]=!0,e.modal.$on(`${n}Changed`,(t=>{let o=t.detail;Z(e.editor),K(n,o)})))}else Z(e.editor),K(t,o.color)})))};function at(t){h(t,"svelte-42yfje",'.cl-editor-modal.svelte-42yfje.svelte-42yfje{position:absolute;top:37px;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);max-width:520px;width:100%;height:140px;backface-visibility:hidden;z-index:11}.cl-editor-overlay.svelte-42yfje.svelte-42yfje{position:absolute;background-color:rgba(255,255,255,.5);height:100%;width:100%;left:0;top:0;z-index:10}.modal-box.svelte-42yfje.svelte-42yfje{position:absolute;top:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);max-width:500px;width:calc(100% - 20px);padding-bottom:36px;z-index:1;background-color:#FFF;text-align:center;font-size:14px;box-shadow:rgba(0,0,0,.2) 0 2px 3px;-webkit-backface-visibility:hidden;backface-visibility:hidden}.modal-title.svelte-42yfje.svelte-42yfje{font-size:24px;font-weight:700;margin:0 0 20px;padding:2px 0 4px;display:block;border-bottom:1px solid #EEE;color:#333;background:#fbfcfc}.modal-label.svelte-42yfje.svelte-42yfje{display:block;position:relative;margin:15px 12px;height:29px;line-height:29px;overflow:hidden}.modal-label.svelte-42yfje input.svelte-42yfje{position:absolute;top:0;right:0;height:27px;line-height:25px;border:1px solid #DEDEDE;background:#fff;font-size:14px;max-width:330px;width:70%;padding:0 7px;transition:all 150ms}.modal-label.svelte-42yfje input.svelte-42yfje:focus{outline:none}.input-error.svelte-42yfje input.svelte-42yfje{border:1px solid #e74c3c}.input-info.svelte-42yfje.svelte-42yfje{display:block;text-align:left;height:25px;line-height:25px;transition:all 150ms}.input-info.svelte-42yfje span.svelte-42yfje{display:block;color:#69878f;background-color:#fbfcfc;border:1px solid #DEDEDE;padding:1px 7px;width:150px}.input-error.svelte-42yfje .input-info.svelte-42yfje{margin-top:-29px}.input-error.svelte-42yfje .msg-error.svelte-42yfje{color:#e74c3c}.modal-button.svelte-42yfje.svelte-42yfje{position:absolute;bottom:10px;right:0;text-decoration:none;color:#FFF;display:block;width:100px;height:35px;line-height:33px;margin:0 10px;background-color:#333;border:none;cursor:pointer;font-family:"Lato",Helvetica,Verdana,sans-serif;font-size:16px;transition:all 150ms}.modal-submit.svelte-42yfje.svelte-42yfje{right:110px;background:#2bc06a}.modal-reset.svelte-42yfje.svelte-42yfje{color:#555;background:#e6e6e6}')}function dt(e){let n,l,r,s,c,a,h,p,w,j,z,H,M,E,L,C,T,B,F,R,O,N,_=e[2]&&ht();return{c(){n=g("div"),l=b(),r=g("div"),s=g("div"),c=g("span"),a=v(e[3]),h=b(),p=g("form"),w=g("label"),j=g("input"),H=b(),M=g("span"),E=g("span"),L=v(e[4]),C=b(),_&&_.c(),T=b(),B=g("button"),B.textContent="Confirm",F=b(),R=g("button"),R.textContent="Cancel",x(n,"class","cl-editor-overlay svelte-42yfje"),x(c,"class","modal-title svelte-42yfje"),x(j,"name","text"),x(j,"class","svelte-42yfje"),x(E,"class","svelte-42yfje"),x(M,"class","input-info svelte-42yfje"),x(w,"class","modal-label svelte-42yfje"),k(w,"input-error",e[2]),x(B,"class","modal-button modal-submit svelte-42yfje"),x(B,"type","submit"),x(R,"class","modal-button modal-reset svelte-42yfje"),x(R,"type","reset"),x(s,"class","modal-box svelte-42yfje"),x(r,"class","cl-editor-modal svelte-42yfje")},m(o,f){var g,v;u(o,n,f),u(o,l,f),u(o,r,f),d(r,s),d(s,c),d(c,a),d(s,h),d(s,p),d(p,w),d(w,j),e[11](j),$(j,e[1]),d(w,H),d(w,M),d(M,E),d(E,L),d(M,C),_&&_.m(M,null),d(p,T),d(p,B),d(p,F),d(p,R),O||(N=[m(n,"click",e[8]),m(j,"keyup",e[9]),(v=z=e[6].call(null,j),v&&i(v.destroy)?v.destroy:t),m(j,"input",e[12]),m(R,"click",e[8]),m(p,"submit",(g=e[13],function(t){return t.preventDefault(),g.call(this,t)}))],O=!0)},p(t,e){8&e&&y(a,t[3]),2&e&&j.value!==t[1]&&$(j,t[1]),16&e&&y(L,t[4]),t[2]?_||(_=ht(),_.c(),_.m(M,null)):_&&(_.d(1),_=null),4&e&&k(w,"input-error",t[2])},d(t){t&&f(n),t&&f(l),t&&f(r),e[11](null),_&&_.d(),O=!1,o(N)}}}function ht(t){let e;return{c(){e=g("span"),e.textContent="Required",x(e,"class","msg-error svelte-42yfje")},m(t,n){u(t,e,n)},d(t){t&&f(e)}}}function ut(e){let n,o=e[0]&&dt(e);return{c(){o&&o.c(),n=v("")},m(t,e){o&&o.m(t,e),u(t,n,e)},p(t,[e]){t[0]?o?o.p(t,e):(o=dt(t),o.c(),o.m(n.parentNode,n)):o&&(o.d(1),o=null)},i:t,o:t,d(t){o&&o.d(t),t&&f(n)}}}function ft(t,e,n){let o=new E,{show:i=!1}=e,{text:l=""}=e,{event:r=""}=e,{title:s=""}=e,{label:c=""}=e,{error:a=!1}=e,d={};function h(){l?(o(r,l),u()):(n(2,a=!0),d.text.focus())}function u(){n(0,i=!1),n(1,l=""),n(2,a=!1)}return t.$$set=t=>{"show"in t&&n(0,i=t.show),"text"in t&&n(1,l=t.text),"event"in t&&n(10,r=t.event),"title"in t&&n(3,s=t.title),"label"in t&&n(4,c=t.label),"error"in t&&n(2,a=t.error)},t.$$.update=()=>{33&t.$$.dirty&&i&&setTimeout((()=>{d.text.focus()}))},[i,l,a,s,c,d,t=>{t.type=r.includes("Color")?"color":"text"},h,u,function(){n(2,a=!1)},r,function(t){C[t?"unshift":"push"]((()=>{d.text=t,n(5,d)}))},function(){l=this.value,n(1,l)},t=>h()]}class pt extends Q{constructor(t){super(),J(this,t,ft,ut,l,{show:0,text:1,event:10,title:3,label:4,error:2},at)}get show(){return this.$$.ctx[0]}set show(t){this.$$set({show:t}),A()}get text(){return this.$$.ctx[1]}set text(t){this.$$set({text:t}),A()}get event(){return this.$$.ctx[10]}set event(t){this.$$set({event:t}),A()}get title(){return this.$$.ctx[3]}set title(t){this.$$set({title:t}),A()}get label(){return this.$$.ctx[4]}set label(t){this.$$set({label:t}),A()}get error(){return this.$$.ctx[2]}set error(t){this.$$set({error:t}),A()}}function gt(t){h(t,"svelte-njq4pk",'.color-picker-wrapper.svelte-njq4pk{border:1px solid #ecf0f1;border-top:none;background:#FFF;box-shadow:rgba(0,0,0,.1) 0 2px 3px;width:290px;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);padding:0;position:absolute;top:37px;z-index:11}.color-picker-overlay.svelte-njq4pk{position:absolute;background-color:rgba(255,255,255,.5);height:100%;width:100%;left:0;top:0;z-index:10}.color-picker-btn.svelte-njq4pk{display:block;position:relative;float:left;height:20px;width:20px;border:1px solid #333;padding:0;margin:2px;line-height:35px;text-decoration:none;background:#FFF;color:#333!important;cursor:pointer;text-align:left;font-size:15px;transition:all 150ms;line-height:20px;padding:0px 5px}.color-picker-btn.svelte-njq4pk:hover::after{content:" ";display:block;position:absolute;top:-5px;left:-5px;height:27px;width:27px;background:inherit;border:1px solid #FFF;box-shadow:#000 0 0 2px;z-index:10}')}function vt(t,e,n){const o=t.slice();return o[8]=e[n],o}function bt(t){let e,n,o,i,l=(t[8].text||"")+"";function r(...e){return t[6](t[8],...e)}return{c(){e=g("button"),n=v(l),x(e,"type","button"),x(e,"class","color-picker-btn svelte-njq4pk"),w(e,"background-color",t[8].color)},m(t,l){u(t,e,l),d(e,n),o||(i=m(e,"click",r),o=!0)},p(o,i){t=o,2&i&&l!==(l=(t[8].text||"")+"")&&y(n,l),2&i&&w(e,"background-color",t[8].color)},d(t){t&&f(e),o=!1,i()}}}function mt(e){let n,o,i,l,r,s,c=e[1],a=[];for(let t=0;t{"show"in t&&n(0,i=t.show),"btns"in t&&n(1,l=t.btns),"event"in t&&n(4,r=t.event),"colors"in t&&n(5,s=t.colors)},t.$$.update=()=>{32&t.$$.dirty&&n(1,l=s.map((t=>({color:t}))).concat([{text:"#",modal:!0}]))},[i,l,c,a,r,s,(t,e)=>a(t)]}class yt extends Q{constructor(t){super(),J(this,t,xt,mt,l,{show:0,btns:1,event:4,colors:5},gt)}}const $t=function(t){const{subscribe:e,set:n,update:o}=lt({actionBtns:[],actionObj:{}});return{name:t,set:n,update:o,subscribe:e}};function wt(t){h(t,"svelte-1a534py",".cl.svelte-1a534py .svelte-1a534py{box-sizing:border-box}.cl.svelte-1a534py.svelte-1a534py{box-shadow:0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);box-sizing:border-box;width:100%;position:relative}.cl-content.svelte-1a534py.svelte-1a534py{height:300px;outline:0;overflow-y:auto;padding:10px;width:100%;background-color:white}.cl-actionbar.svelte-1a534py.svelte-1a534py{background-color:#ecf0f1;border-bottom:1px solid rgba(10, 10, 10, 0.1);width:100%}.cl-button.svelte-1a534py.svelte-1a534py{background-color:transparent;border:none;cursor:pointer;height:35px;outline:0;width:35px;vertical-align:top;position:relative}.cl-button.svelte-1a534py.svelte-1a534py:hover,.cl-button.active.svelte-1a534py.svelte-1a534py{background-color:#fff}.cl-button.svelte-1a534py.svelte-1a534py:disabled{opacity:.5;pointer-events:none}.cl-textarea.svelte-1a534py.svelte-1a534py{display:none;max-width:100%;min-width:100%;border:none;padding:10px}.cl-textarea.svelte-1a534py.svelte-1a534py:focus{outline:none}")}function kt(t,e,n){const o=t.slice();return o[38]=e[n],o}function jt(t){let e,n,o,i,l,r,s,c,a=t[38].icon+"";function h(...e){return t[24](t[38],...e)}return{c(){e=g("button"),n=new j,o=b(),n.a=o,x(e,"type","button"),x(e,"class",i="cl-button "+(t[38].active?"active":"")+" svelte-1a534py"),x(e,"title",l=t[38].title),e.disabled=r=t[38].disabled},m(t,i){u(t,e,i),n.m(a,e),d(e,o),s||(c=m(e,"click",h),s=!0)},p(o,s){t=o,16&s[0]&&a!==(a=t[38].icon+"")&&n.p(a),16&s[0]&&i!==(i="cl-button "+(t[38].active?"active":"")+" svelte-1a534py")&&x(e,"class",i),16&s[0]&&l!==(l=t[38].title)&&x(e,"title",l),16&s[0]&&r!==(r=t[38].disabled)&&(e.disabled=r)},d(t){t&&f(e),s=!1,c()}}}function zt(t){let e,n,i,l,r,s,c,a,h,v,y,$,k,j=t[4].actionBtns,z=[];for(let e=0;en(34,i=t))),Ht.push({});let v="editor_"+Ht.length,b=$t(v);c(t,b,(t=>n(4,l=t)));let m=lt({});c(t,m,(t=>n(3,o=t))),a(b,l.actionObj=((t,e=[])=>{if(e&&e.length){const n={};return e.forEach((e=>{"string"==typeof e?n[e]=Object.assign({},t[e]):t[e.name]?n[e.name]=Object.assign(t[e.name],e):n[e.name]=Object.assign({},e)})),n}return t})(st,s),l);let x={exec:H,getHtml:L,getText:T,setHtml:B,saveRange:F,restoreRange:R,helper:g,references:m,state:b,removeFormatTags:p};var y;function $(t){o.editor.focus(),F(o.editor),R(o.editor),t.result.call(x),w()}function w(t){const e=t?[]:G(document.getSelection().focusNode);Object.keys(l.actionObj).forEach((t=>a(b,l.actionObj[t].active=!1,l))),e.forEach((t=>(l.actionObj[t.toLowerCase()]||{}).active=!0)),a(b,l.actionBtns=et(l.actionObj),l),b.set(l)}function k(t){t.preventDefault(),H("insertHTML",t.clipboardData.getData("text/html")?(t=>{const e=t.match(/(.*?)/);let n=e&&e[1]||t;return n=n.replace(/\r?\n|\r/g," ").replace(//g,"").replace(new RegExp("<(/)*(meta|link|span|\\?xml:|st1:|o:|font|w:sdt)(.*?)>","gi"),"").replace(/(.*?)/gi,"").replace(/style="[^"]*"/gi,"").replace(/style='[^']*'/gi,"").replace(/ /gi," ").replace(/>(\s+)<").replace(/class="[^"]*"/gi,"").replace(/class='[^']*'/gi,"").replace(/<[^/].*?>/g,(t=>t.split(/[ >]/g)[0]+">")).trim(),n=nt(n),n})(t.clipboardData.getData("text/html")):t.clipboardData.getData("text"))}function j(t){r("change",t)}function z(t){!ot(t.target,o.editorWrapper)&&i.blurActive&&r("blur",t),a(g,i.blurActive=!0,i)}function H(t,e){K(t,e)}function L(t){return t?nt(o.editor.innerHTML):o.editor.innerHTML}function T(){return o.editor.innerText}function B(t,e){const n=e?nt(t):t||"";a(m,o.editor.innerHTML=n,o),a(m,o.raw.value=n,o)}function F(){Y(o.editor)}function R(){Z(o.editor)}!function(t,e){M().$$.context.set(t,e)}(v,x),y=()=>{a(b,l.actionBtns=et(l.actionObj),l),B(h)},M().$$.on_mount.push(y);const O=o;return t.$$set=t=>{"actions"in t&&n(13,s=t.actions),"height"in t&&n(0,d=t.height),"html"in t&&n(14,h=t.html),"contentId"in t&&n(1,u=t.contentId),"colors"in t&&n(2,f=t.colors),"removeFormatTags"in t&&n(15,p=t.removeFormatTags)},[d,u,f,o,l,g,b,m,$,w,k,j,z,s,h,p,H,L,T,B,F,R,O,t=>z(t),(t,e)=>$(t),function(t){C[t?"unshift":"push"]((()=>{o.editor=t,m.set(o)}))},t=>j(t.target.innerHTML),()=>w(),()=>w(),t=>k(t),function(t){C[t?"unshift":"push"]((()=>{o.raw=t,m.set(o)}))},function(t){C[t?"unshift":"push"]((()=>{o.modal=t,m.set(o)}))},function(t){C[t?"unshift":"push"]((()=>{o.colorPicker=t,m.set(o)}))},function(t){C[t?"unshift":"push"]((()=>{o.editorWrapper=t,m.set(o)}))}]}return class extends Q{constructor(t){super(),J(this,t,Mt,zt,l,{actions:13,height:0,html:14,contentId:1,colors:2,removeFormatTags:15,exec:16,getHtml:17,getText:18,setHtml:19,saveRange:20,restoreRange:21,refs:22},wt,[-1,-1])}get actions(){return this.$$.ctx[13]}set actions(t){this.$$set({actions:t}),A()}get height(){return this.$$.ctx[0]}set height(t){this.$$set({height:t}),A()}get html(){return this.$$.ctx[14]}set html(t){this.$$set({html:t}),A()}get contentId(){return this.$$.ctx[1]}set contentId(t){this.$$set({contentId:t}),A()}get colors(){return this.$$.ctx[2]}set colors(t){this.$$set({colors:t}),A()}get removeFormatTags(){return this.$$.ctx[15]}set removeFormatTags(t){this.$$set({removeFormatTags:t}),A()}get exec(){return this.$$.ctx[16]}get getHtml(){return this.$$.ctx[17]}get getText(){return this.$$.ctx[18]}get setHtml(){return this.$$.ctx[19]}get saveRange(){return this.$$.ctx[20]}get restoreRange(){return this.$$.ctx[21]}get refs(){return this.$$.ctx[22]}}})); 2 | -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700/Noto-Sans-700.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 38 | 41 | 42 | 44 | 47 | 48 | 51 | 54 | 56 | 58 | 59 | 60 | 61 | 64 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 107 | 109 | 110 | 112 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 124 | 125 | 127 | 129 | 131 | 132 | 134 | 135 | 136 | 137 | 139 | 140 | 141 | 142 | 144 | 145 | 147 | 149 | 150 | 151 | 153 | 154 | 156 | 157 | 158 | 161 | 163 | 166 | 168 | 169 | 170 | 171 | 174 | 175 | 177 | 178 | 179 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 189 | 190 | 192 | 194 | 197 | 199 | 200 | 201 | 203 | 205 | 207 | 209 | 210 | 212 | 213 | 214 | 215 | 217 | 218 | 219 | 220 | 222 | 223 | 225 | 227 | 229 | 231 | 234 | 237 | 238 | 240 | 242 | 244 | 246 | 248 | 249 | 250 | 253 | 255 | 257 | 259 | 262 | 265 | 268 | 271 | 273 | 275 | 277 | 279 | 282 | 283 | 284 | 285 | 287 | 289 | 291 | 293 | 295 | 297 | 300 | 303 | 305 | 307 | 309 | 311 | 313 | 315 | 317 | 319 | 321 | 322 | 323 | 324 | 325 | 326 | 328 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /assets/fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 17 | 19 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 38 | 40 | 41 | 43 | 45 | 46 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 60 | 61 | 63 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 82 | 83 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 103 | 105 | 107 | 109 | 111 | 113 | 114 | 115 | 117 | 118 | 119 | 121 | 122 | 124 | 126 | 128 | 129 | 131 | 132 | 133 | 134 | 136 | 137 | 138 | 139 | 141 | 142 | 144 | 145 | 146 | 147 | 149 | 151 | 153 | 154 | 155 | 158 | 159 | 162 | 164 | 165 | 166 | 167 | 170 | 171 | 173 | 174 | 176 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 186 | 187 | 189 | 191 | 194 | 196 | 197 | 198 | 200 | 202 | 204 | 206 | 207 | 209 | 210 | 211 | 213 | 215 | 216 | 217 | 219 | 221 | 223 | 225 | 227 | 229 | 231 | 234 | 237 | 238 | 240 | 242 | 244 | 246 | 248 | 249 | 250 | 253 | 255 | 257 | 259 | 262 | 265 | 268 | 271 | 273 | 275 | 277 | 279 | 282 | 283 | 284 | 285 | 287 | 289 | 291 | 293 | 295 | 297 | 300 | 303 | 305 | 307 | 309 | 311 | 313 | 315 | 317 | 319 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | --------------------------------------------------------------------------------