├── .npmignore ├── .gitignore ├── src ├── toolbox-icon.svg ├── index.css └── index.js ├── webpack.config.js ├── package.json ├── LICENSE ├── .github └── workflows │ └── npm-publish.yml ├── README.md ├── dist └── bundle.js └── yarn.lock /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | src/ 3 | webpack.config.js 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | .idea/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /src/toolbox-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .ce-paragraph { 2 | line-height: 1.6em; 3 | outline: none; 4 | } 5 | 6 | .ce-paragraph[data-placeholder]:empty::before{ 7 | content: attr(data-placeholder); 8 | color: #707684; 9 | font-weight: normal; 10 | opacity: 0; 11 | } 12 | 13 | /** Show placeholder at the first paragraph if Editor is empty */ 14 | .codex-editor--empty .ce-block:first-child .ce-paragraph[data-placeholder]:empty::before { 15 | opacity: 1; 16 | } 17 | 18 | .codex-editor--toolbox-opened .ce-block:first-child .ce-paragraph[data-placeholder]:empty::before, 19 | .codex-editor--empty .ce-block:first-child .ce-paragraph[data-placeholder]:empty:focus::before { 20 | opacity: 0; 21 | } 22 | 23 | .ce-paragraph p:first-of-type{ 24 | margin-top: 0; 25 | } 26 | 27 | .ce-paragraph p:last-of-type{ 28 | margin-bottom: 0; 29 | } 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.js', 3 | module: { 4 | rules: [ 5 | { 6 | test: /\.js$/, 7 | exclude: /node_modules/, 8 | use: [ 9 | { 10 | loader: 'babel-loader', 11 | query: { 12 | presets: [ '@babel/preset-env' ], 13 | }, 14 | }, 15 | ] 16 | }, 17 | { 18 | test: /\.css$/, 19 | use: [ 20 | 'style-loader', 21 | 'css-loader' 22 | ] 23 | }, 24 | { 25 | test: /\.(svg)$/, 26 | use: [ 27 | { 28 | loader: 'raw-loader', 29 | } 30 | ] 31 | } 32 | ] 33 | }, 34 | output: { 35 | path: __dirname + '/dist', 36 | publicPath: '/', 37 | filename: 'bundle.js', 38 | library: 'Paragraph', 39 | libraryTarget: 'umd' 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@editorjs/paragraph", 3 | "version": "2.8.0", 4 | "keywords": [ 5 | "codex editor", 6 | "paragraph", 7 | "tool", 8 | "editor.js", 9 | "editorjs" 10 | ], 11 | "description": "Paragraph Tool for Editor.js", 12 | "license": "MIT", 13 | "repository": "https://github.com/editor-js/paragraph", 14 | "main": "./dist/bundle.js", 15 | "scripts": { 16 | "build": "webpack --mode production", 17 | "build:dev": "webpack --mode development --watch" 18 | }, 19 | "author": { 20 | "name": "CodeX", 21 | "email": "team@codex.so" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.10.2", 25 | "@babel/preset-env": "^7.10.2", 26 | "babel-loader": "^8.1.0", 27 | "css-loader": "^3.5.3", 28 | "raw-loader": "^4.0.1", 29 | "style-loader": "^1.2.1", 30 | "webpack": "^4.43.0", 31 | "webpack-cli": "^3.3.11" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CodeX 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 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to NPM 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org/ 17 | - run: yarn 18 | - run: yarn build 19 | - run: yarn publish --access=public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | notify: 23 | needs: publish 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Get package info 28 | id: package 29 | uses: codex-team/action-nodejs-package-info@v1 30 | - name: Send a message 31 | uses: codex-team/action-codexbot-notify@v1 32 | with: 33 | webhook: ${{ secrets.CODEX_BOT_NOTIFY_EDITORJS_PUBLIC_CHAT }} 34 | message: '📦 [${{ steps.package.outputs.name }}](${{ steps.package.outputs.npmjs-link }}) ${{ steps.package.outputs.version }} was published' 35 | parse_mode: 'markdown' 36 | disable_web_page_preview: true 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://badgen.net/badge/Editor.js/v2.0/blue) 2 | 3 | # Paragraph Tool for Editor.js 4 | 5 | Basic text Tool for the [Editor.js](https://ifmo.su/editor). 6 | 7 | ## Installation 8 | 9 | ### Install via NPM 10 | 11 | Get the package 12 | 13 | ```shell 14 | npm i --save @editorjs/paragraph 15 | ``` 16 | 17 | Include module at your application 18 | 19 | ```javascript 20 | const Paragraph = require('@editorjs/paragraph'); 21 | ``` 22 | 23 | ### Download to your project's source dir 24 | 25 | 1. Upload folder `dist` from repository 26 | 2. Add `dist/bundle.js` file to your page. 27 | 28 | ### Load from CDN 29 | 30 | You can also load specific version of package from [jsDelivr CDN](https://www.jsdelivr.com/package/npm/@editorjs/paragraph). 31 | 32 | `https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.0.2` 33 | 34 | Then require this script on page with Editor.js. 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | ## Usage 41 | 42 | The Paragraph tool is included at editor.js by default. So you don't need to connect it manually. 43 | If you want to connect your customized version of this tool, do not forget to use the [`defaultBlock`](https://editorjs.io/configuration#change-the-default-block) 44 | option of the editor config. 45 | 46 | Add a new Tool to the `tools` property of the Editor.js initial config. 47 | 48 | ```javascript 49 | var editor = EditorJS({ 50 | ... 51 | 52 | tools: { 53 | ... 54 | paragraph: { 55 | class: Paragraph, 56 | inlineToolbar: true, 57 | }, 58 | } 59 | 60 | ... 61 | }); 62 | ``` 63 | 64 | ## Config Params 65 | 66 | The Paragraph Tool supports these configuration parameters: 67 | 68 | | Field | Type | Description | 69 | | ----- | -------- | ------------------ | 70 | | placeholder | `string` | The placeholder. Will be shown only in the first paragraph when the whole editor is empty. | 71 | | preserveBlank | `boolean` | (default: `false`) Whether or not to keep blank paragraphs when saving editor data | 72 | 73 | ## Output data 74 | 75 | | Field | Type | Description | 76 | | ------ | -------- | ---------------- | 77 | | text | `string` | paragraph's text | 78 | 79 | 80 | ```json 81 | { 82 | "type" : "paragraph", 83 | "data" : { 84 | "text" : "Check out our projects on a GitHub page.", 85 | } 86 | } 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build styles 3 | */ 4 | require('./index.css').toString(); 5 | 6 | /** 7 | * Base Paragraph Block for the Editor.js. 8 | * Represents simple paragraph 9 | * 10 | * @author CodeX (team@codex.so) 11 | * @copyright CodeX 2018 12 | * @license The MIT License (MIT) 13 | */ 14 | 15 | /** 16 | * @typedef {object} ParagraphConfig 17 | * @property {string} placeholder - placeholder for the empty paragraph 18 | * @property {boolean} preserveBlank - Whether or not to keep blank paragraphs when saving editor data 19 | */ 20 | 21 | /** 22 | * @typedef {Object} ParagraphData 23 | * @description Tool's input and output data format 24 | * @property {String} text — Paragraph's content. Can include HTML tags: 25 | */ 26 | class Paragraph { 27 | /** 28 | * Default placeholder for Paragraph Tool 29 | * 30 | * @return {string} 31 | * @constructor 32 | */ 33 | static get DEFAULT_PLACEHOLDER() { 34 | return ''; 35 | } 36 | 37 | /** 38 | * Render plugin`s main Element and fill it with saved data 39 | * 40 | * @param {object} params - constructor params 41 | * @param {ParagraphData} params.data - previously saved data 42 | * @param {ParagraphConfig} params.config - user config for Tool 43 | * @param {object} params.api - editor.js api 44 | * @param {boolean} readOnly - read only mode flag 45 | */ 46 | constructor({data, config, api, readOnly}) { 47 | this.api = api; 48 | this.readOnly = readOnly; 49 | 50 | this._CSS = { 51 | block: this.api.styles.block, 52 | wrapper: 'ce-paragraph' 53 | }; 54 | 55 | if (!this.readOnly) { 56 | this.onKeyUp = this.onKeyUp.bind(this); 57 | } 58 | 59 | /** 60 | * Placeholder for paragraph if it is first Block 61 | * @type {string} 62 | */ 63 | this._placeholder = config.placeholder ? config.placeholder : Paragraph.DEFAULT_PLACEHOLDER; 64 | this._data = {}; 65 | this._element = this.drawView(); 66 | this._preserveBlank = config.preserveBlank !== undefined ? config.preserveBlank : false; 67 | 68 | this.data = data; 69 | } 70 | 71 | /** 72 | * Check if text content is empty and set empty string to inner html. 73 | * We need this because some browsers (e.g. Safari) insert
into empty contenteditanle elements 74 | * 75 | * @param {KeyboardEvent} e - key up event 76 | */ 77 | onKeyUp(e) { 78 | if (e.code !== 'Backspace' && e.code !== 'Delete') { 79 | return; 80 | } 81 | 82 | const {textContent} = this._element; 83 | 84 | if (textContent === '') { 85 | this._element.innerHTML = ''; 86 | } 87 | } 88 | 89 | /** 90 | * Create Tool's view 91 | * @return {HTMLElement} 92 | * @private 93 | */ 94 | drawView() { 95 | let div = document.createElement('DIV'); 96 | 97 | div.classList.add(this._CSS.wrapper, this._CSS.block); 98 | div.contentEditable = false; 99 | div.dataset.placeholder = this.api.i18n.t(this._placeholder); 100 | 101 | if (!this.readOnly) { 102 | div.contentEditable = true; 103 | div.addEventListener('keyup', this.onKeyUp); 104 | } 105 | 106 | return div; 107 | } 108 | 109 | /** 110 | * Return Tool's view 111 | * 112 | * @returns {HTMLDivElement} 113 | */ 114 | render() { 115 | return this._element; 116 | } 117 | 118 | /** 119 | * Method that specified how to merge two Text blocks. 120 | * Called by Editor.js by backspace at the beginning of the Block 121 | * @param {ParagraphData} data 122 | * @public 123 | */ 124 | merge(data) { 125 | let newData = { 126 | text : this.data.text + data.text 127 | }; 128 | 129 | this.data = newData; 130 | } 131 | 132 | /** 133 | * Validate Paragraph block data: 134 | * - check for emptiness 135 | * 136 | * @param {ParagraphData} savedData — data received after saving 137 | * @returns {boolean} false if saved data is not correct, otherwise true 138 | * @public 139 | */ 140 | validate(savedData) { 141 | if (savedData.text.trim() === '' && !this._preserveBlank) { 142 | return false; 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Extract Tool's data from the view 150 | * @param {HTMLDivElement} toolsContent - Paragraph tools rendered view 151 | * @returns {ParagraphData} - saved data 152 | * @public 153 | */ 154 | save(toolsContent) { 155 | return { 156 | text: toolsContent.innerHTML 157 | }; 158 | } 159 | 160 | /** 161 | * On paste callback fired from Editor. 162 | * 163 | * @param {PasteEvent} event - event with pasted data 164 | */ 165 | onPaste(event) { 166 | const data = { 167 | text: event.detail.data.innerHTML 168 | }; 169 | 170 | this.data = data; 171 | } 172 | 173 | /** 174 | * Enable Conversion Toolbar. Paragraph can be converted to/from other tools 175 | */ 176 | static get conversionConfig() { 177 | return { 178 | export: 'text', // to convert Paragraph to other block, use 'text' property of saved data 179 | import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data 180 | }; 181 | } 182 | 183 | /** 184 | * Sanitizer rules 185 | */ 186 | static get sanitize() { 187 | return { 188 | text: { 189 | br: true, 190 | } 191 | }; 192 | } 193 | 194 | /** 195 | * Returns true to notify the core that read-only mode is supported 196 | * 197 | * @return {boolean} 198 | */ 199 | static get isReadOnlySupported() { 200 | return true; 201 | } 202 | 203 | /** 204 | * Get current Tools`s data 205 | * @returns {ParagraphData} Current data 206 | * @private 207 | */ 208 | get data() { 209 | let text = this._element.innerHTML; 210 | 211 | this._data.text = text; 212 | 213 | return this._data; 214 | } 215 | 216 | /** 217 | * Store data in plugin: 218 | * - at the this._data property 219 | * - at the HTML 220 | * 221 | * @param {ParagraphData} data — data to set 222 | * @private 223 | */ 224 | set data(data) { 225 | this._data = data || {}; 226 | 227 | this._element.innerHTML = this._data.text || ''; 228 | } 229 | 230 | /** 231 | * Used by Editor paste handling API. 232 | * Provides configuration to handle P tags. 233 | * 234 | * @returns {{tags: string[]}} 235 | */ 236 | static get pasteConfig() { 237 | return { 238 | tags: [ 'P' ] 239 | }; 240 | } 241 | 242 | /** 243 | * Icon and title for displaying at the Toolbox 244 | * 245 | * @return {{icon: string, title: string}} 246 | */ 247 | static get toolbox() { 248 | return { 249 | icon: require('./toolbox-icon.svg').default, 250 | title: 'Text' 251 | }; 252 | } 253 | } 254 | 255 | module.exports = Paragraph; 256 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Paragraph=t():e.Paragraph=t()}(window,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=0)}([function(e,t,n){function r(e,t){for(var n=0;n