├── dev ├── .tmp │ └── .gitignore └── server.js ├── .gitignore ├── .eslintrc ├── .postcssrc ├── .npmignore ├── assets └── 68747470733a2f2f636170656c6c612e706963732f30363461666437622d623637652d343832622d623932612d6434343562303938646566322e6a7067.jpeg ├── src ├── svg │ └── toolbox.svg ├── uploader.js ├── index.css └── index.js ├── .github └── workflows │ └── npm-publish.yml ├── webpack.config.js ├── package.json └── README.md /dev/.tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | .idea/ 4 | dist 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "codex" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-smart-import: {} 3 | postcss-cssnext: {} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | src/ 3 | .eslintrc 4 | .postcssrc 5 | webpack.config.js 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /assets/68747470733a2f2f636170656c6c612e706963732f30363461666437622d623637652d343832622d623932612d6434343562303938646566322e6a7067.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/editor-js/personality/master/assets/68747470733a2f2f636170656c6c612e706963732f30363461666437622d623637652d343832622d623932612d6434343562303938646566322e6a7067.jpeg -------------------------------------------------------------------------------- /src/svg/toolbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.js', 3 | output: { 4 | path: __dirname + '/dist', 5 | publicPath: '/', 6 | filename: 'bundle.js', 7 | library: 'Personality', 8 | libraryTarget: 'umd', 9 | libraryExport: 'default' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: [ 17 | { 18 | loader: 'babel-loader', 19 | query: { 20 | presets: [ '@babel/preset-env' ], 21 | }, 22 | }, 23 | { 24 | loader: 'eslint-loader', 25 | options: { 26 | fix: true 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | 'style-loader', 35 | 'css-loader', 36 | { 37 | loader: 'postcss-loader', 38 | options: { 39 | plugins: [ 40 | require('postcss-nested') 41 | ] 42 | } 43 | } 44 | ] 45 | }, 46 | { 47 | test: /\.svg$/, 48 | loader: 'svg-inline-loader?removeSVGTagAttrs=false' 49 | } 50 | ] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/uploader.js: -------------------------------------------------------------------------------- 1 | import ajax from '@codexteam/ajax'; 2 | 3 | /** 4 | * Module for file uploading. 5 | */ 6 | export default class Uploader { 7 | /** 8 | * @param {PersonalityConfig} config 9 | * @param {function} onUpload - one callback for all uploading (file, d-n-d, pasting) 10 | * @param {function} onError - callback for uploading errors 11 | */ 12 | constructor({ config, onUpload, onError }) { 13 | this.config = config; 14 | this.onUpload = onUpload; 15 | this.onError = onError; 16 | } 17 | 18 | /** 19 | * Handle clicks on the upload file button 20 | * @fires ajax.transport() 21 | * @param {function} onPreview - callback fired when preview is ready 22 | */ 23 | uploadSelectedFile({ onPreview }) { 24 | ajax.transport({ 25 | url: this.config.endpoint, 26 | accept: this.config.types, 27 | beforeSend: (files) => { 28 | const reader = new FileReader(); 29 | 30 | reader.readAsDataURL(files[0]); 31 | reader.onload = (e) => { 32 | onPreview(e.target.result); 33 | }; 34 | }, 35 | fieldName: this.config.field 36 | }).then((response) => { 37 | this.onUpload(response); 38 | }).catch((error) => { 39 | const message = error.body ? error.body.message : 'Uploading failed'; 40 | 41 | this.onError(message); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@editorjs/personality", 3 | "version": "2.0.2", 4 | "description": "Personality tool for Editor.js", 5 | "scripts": { 6 | "build": "webpack --mode production", 7 | "build:dev": "webpack --mode development --watch" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/editor-js/personality.git" 12 | }, 13 | "author": { 14 | "name": "CodeX", 15 | "email": "team@codex.so" 16 | }, 17 | "main": "./dist/bundle.js", 18 | "keywords": [ 19 | "redactor", 20 | "codex editor", 21 | "personality", 22 | "tool", 23 | "editor.js", 24 | "editorjs" 25 | ], 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/editor-js/personality/issues" 29 | }, 30 | "homepage": "https://github.com/editor-js/personality#readme", 31 | "devDependencies": { 32 | "@babel/core": "^7.11.6", 33 | "@babel/preset-env": "^7.11.5", 34 | "@codexteam/ajax": "^4.0.1", 35 | "babel-loader": "^8.1.0", 36 | "css-loader": "^3.1.0", 37 | "eslint": "^6.1.0", 38 | "eslint-config-codex": "github:codex-team/eslint-config", 39 | "eslint-loader": "^2.2.1", 40 | "file-loader": "^4.1.0", 41 | "formidable": "^1.2.1", 42 | "postcss-cssnext": "^3.1.0", 43 | "postcss-loader": "^3.0.0", 44 | "postcss-nested": "^4.1.2", 45 | "postcss-smart-import": "^0.7.6", 46 | "request": "^2.88.0", 47 | "style-loader": "^0.23.1", 48 | "svg-inline-loader": "^0.8.0", 49 | "webpack": "^4.29.6", 50 | "webpack-cli": "^3.3.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .cdx-personality { 2 | padding: 30px; 3 | margin: 0.7em 0; 4 | border: 1px solid #e5e6ec; 5 | border-radius: 3px; 6 | background: #fff; 7 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03); 8 | 9 | &::after { 10 | content: ''; 11 | clear: both; 12 | display: table; 13 | } 14 | 15 | [contentEditable=true][data-placeholder] { 16 | &::before { 17 | position: absolute; 18 | content: attr(data-placeholder); 19 | color: #707684; 20 | font-weight: normal; 21 | opacity: 0; 22 | } 23 | 24 | &:empty { 25 | &::before { 26 | opacity: 1; 27 | } 28 | 29 | &:focus::before { 30 | opacity: 0.3; 31 | } 32 | } 33 | } 34 | 35 | &__photo { 36 | float: right; 37 | width: 70px; 38 | height: 70px; 39 | margin-left: 30px; 40 | border-radius: 3px; 41 | background: #f6f6f9 url('data:image/svg+xml,') center center no-repeat; 42 | cursor: pointer; 43 | overflow: hidden; 44 | } 45 | 46 | &__name { 47 | font-weight: 600; 48 | outline: none; 49 | } 50 | 51 | &__description { 52 | font-size: 0.86em; 53 | margin: 10px 0; 54 | outline: none; 55 | } 56 | 57 | &__link { 58 | font-size: 0.68em; 59 | color: #6e758a; 60 | letter-spacing: 0.1px; 61 | text-overflow: ellipsis; 62 | outline: none; 63 | } 64 | } 65 | 66 | .codex-editor--narrow { 67 | .cdx-personality { 68 | padding: 15px; 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /dev/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample HTTP server for accept uploaded images 3 | * [!] Use it only for debugging purposes 4 | * 5 | * How to use [requires Node.js 10.0.0+ and npm install]: 6 | * 7 | * 1. $ node dev/server.js 8 | * 2. set 'endpoint' at the Personality Tools 'config' in example-dev.html 9 | * endpoint : 'http://localhost:8008/uploadFile' 10 | * 11 | */ 12 | const http = require('http'); 13 | const formidable = require('formidable'); 14 | const crypto = require('crypto'); 15 | 16 | class ServerExample { 17 | constructor({port, fieldName}) { 18 | this.uploadDir = __dirname + '/\.tmp'; 19 | this.fieldName = fieldName; 20 | this.server = http.createServer((req, res) => { 21 | this.onRequest(req, res); 22 | }).listen(port); 23 | 24 | this.server.on('listening', () => { 25 | console.log('Server is listening ' + port + '...'); 26 | }); 27 | 28 | this.server.on('error', (error) => { 29 | console.log('Failed to run server', error); 30 | }); 31 | } 32 | 33 | /** 34 | * Request handler 35 | * @param {http.IncomingMessage} request 36 | * @param {http.ServerResponse} response 37 | */ 38 | onRequest(request, response) { 39 | this.allowCors(response); 40 | 41 | const { method, url } = request; 42 | 43 | if (method.toLowerCase() !== 'post') { 44 | response.end(); 45 | return; 46 | } 47 | 48 | switch (url) { 49 | case '/uploadFile': 50 | this.uploadFile(request, response); 51 | break; 52 | } 53 | } 54 | 55 | /** 56 | * Allows CORS requests for debugging 57 | * @param response 58 | */ 59 | allowCors(response) { 60 | response.setHeader('Access-Control-Allow-Origin', '*'); 61 | response.setHeader('Access-Control-Allow-Credentials', 'true'); 62 | response.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT'); 63 | response.setHeader('Access-Control-Allow-Headers', 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers'); 64 | } 65 | 66 | /** 67 | * Handles uploading by file 68 | * @param request 69 | * @param response 70 | */ 71 | uploadFile(request, response) { 72 | let responseJson = { 73 | success: 0 74 | }; 75 | 76 | let responseCode = 200; 77 | 78 | this.getForm(request) 79 | .then(({ files }) => { 80 | let image = files[this.fieldName] || {}; 81 | 82 | responseJson.success = 1; 83 | responseJson.file = { 84 | url: image.path, 85 | name: image.name, 86 | size: image.size 87 | }; 88 | }) 89 | .catch((error) => { 90 | responseJson.success = 0; 91 | responseJson.message = error.message; 92 | responseCode = 500; 93 | }) 94 | .finally(() => { 95 | response.writeHead(responseCode, { 'Content-Type': 'application/json' }); 96 | response.end(JSON.stringify(responseJson)); 97 | }); 98 | } 99 | 100 | /** 101 | * Accepts post form data 102 | * @param request 103 | * @return {Promise<{files: object, fields: object}>} 104 | */ 105 | getForm(request) { 106 | return new Promise((resolve, reject) => { 107 | const form = new formidable.IncomingForm(); 108 | 109 | form.uploadDir = this.uploadDir; 110 | form.keepExtensions = true; 111 | 112 | form.parse(request, (err, fields, files) => { 113 | if (err) { 114 | reject(err); 115 | } else { 116 | console.log('fields', fields); 117 | console.log('files', files); 118 | resolve({files, fields}); 119 | } 120 | }); 121 | }); 122 | } 123 | 124 | /** 125 | * Generates md5 hash for string 126 | * @param string 127 | * @return {string} 128 | */ 129 | md5(string) { 130 | return crypto.createHash('md5').update(string).digest('hex'); 131 | } 132 | } 133 | 134 | new ServerExample({ 135 | port: 8008, 136 | fieldName: 'image' 137 | }); 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://badgen.net/badge/Editor.js/v2.0/blue) 2 | 3 | # Personality Tool 4 | 5 | Personality Tool for the [Editor.js](https://editorjs.io). 6 | 7 | ![](assets/68747470733a2f2f636170656c6c612e706963732f30363461666437622d623637652d343832622d623932612d6434343562303938646566322e6a7067.jpeg) 8 | 9 | ## Features 10 | 11 | This tool allows you to create Personality block in your articles. 12 | 13 | **Note** Tool requires server-side implementation for image uploading. See [backend response format](#server-format) for more details. 14 | 15 | ## Get the package 16 | 17 | You can get the package using any of these ways. 18 | 19 | ### Install via NPM 20 | 21 | Get the package 22 | 23 | ```shell 24 | npm i --save-dev @editorjs/personality 25 | ``` 26 | 27 | Include module at your application 28 | 29 | ```javascript 30 | const Personality = require('@editorjs/personality'); 31 | ``` 32 | 33 | ### Download to your project's source dir 34 | 35 | 1. Upload folder `dist` from repository 36 | 2. Add `dist/bundle.js` file to your page. 37 | 38 | ### Load from CDN 39 | 40 | You can load specific version of package from [jsDelivr CDN](https://cdn.jsdelivr.net/npm/@editorjs/personality@2.0.0). 41 | 42 | `https://cdn.jsdelivr.net/npm/@editorjs/personality@2.0.0` 43 | 44 | Then require this script on page with Editor.js through the `` tag. 45 | 46 | ## Usage 47 | 48 | Add a new Tool to the `tools` property of the Editor.js initial config. 49 | 50 | ```javascript 51 | var editor = EditorJS({ 52 | ... 53 | 54 | tools: { 55 | ... 56 | personality: { 57 | class: Personality, 58 | config: { 59 | endpoint: 'http://localhost:8008/uploadFile' // Your backend file uploader endpoint 60 | } 61 | } 62 | } 63 | 64 | ... 65 | }); 66 | ``` 67 | 68 | ## Config Params 69 | 70 | Personality Tool supports these configuration parameters: 71 | 72 | | Field | Type | Description | 73 | | ----- | -------- | ------------------ | 74 | | endpoint | `string` | **Required** Endpoint for photo uploading. | 75 | | field | `string` | (default: `image`) Name of uploaded image field in POST request | 76 | | types | `string` | (default: `image/*`) Mime-types of files that can be [accepted with file selection](https://github.com/codex-team/ajax#accept-string).| 77 | | namePlaceholder | `string` | (default: `Name`) Placeholder for name field | 78 | | descriptionPlaceholder | `string` | (default: `Description`) Placeholder for description field | 79 | | linkPlaceholder | `string` | (default: `Link`) Link field placeholder | 80 | 81 | ## Output data 82 | 83 | This Tool returns `data` with following format 84 | 85 | | Field | Type | Description | 86 | | -------------- | --------- | ---------------------------------| 87 | | name | `string` | Person's name | 88 | | description | `string` | Person's description | 89 | | link | `string` | Link to person's website | 90 | | photo | `string` | Uploaded image url from backend. | 91 | 92 | ```json 93 | { 94 | "type" : "personality", 95 | "data" : { 96 | "name" : "Elon Musk", 97 | "description" : "Elon Reeve Musk FRS is a technology entrepreneur, investor, and engineer. He holds South African, Canadian, and U.S. citizenship and is the founder", 98 | "link" : "https://twitter.com/elonmusk", 99 | "photo" : "https://capella.pics/3c0e1b97-bc56-4961-b54e-2a6c2c3260f2.jpg" 100 | } 101 | } 102 | ``` 103 | 104 | ## Backend response format 105 | 106 | This Tool works with uploading files from the device 107 | 108 | **Scenario:** 109 | 110 | 1. User select file from the device 111 | 2. Tool sends it to **your** backend (on `config.endpoint.byFile` route) 112 | 3. Your backend should save file and return file data with JSON at specified format. 113 | 4. Personality tool shows saved image and stores server answer 114 | 115 | So, you can implement backend for file saving by your own way. It is a specific and trivial task depending on your 116 | environment and stack. 117 | 118 | Response of your uploader **should** cover following format: 119 | 120 | ```json5 121 | { 122 | "success" : 1, 123 | "file": { 124 | "url" : "https://capella.pics/3c0e1b97-bc56-4961-b54e-2a6c2c3260f2.jpg" 125 | } 126 | } 127 | ``` 128 | 129 | **success** - uploading status. 1 for successful, 0 for failed 130 | 131 | **file** - uploaded file data. **Must** contain an `url` field with full public path to the uploaded image. 132 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ToolboxIcon from './svg/toolbox.svg'; 2 | import './index.css'; 3 | import Uploader from './uploader'; 4 | 5 | /** 6 | * Timeout when loader should be removed 7 | */ 8 | const LOADER_DELAY = 500; 9 | 10 | /** 11 | * @typedef {object} PersonalityToolData 12 | * @description Personality Tool's input and output data format 13 | * @property {string} name — person's name 14 | * @property {string} description - person's description 15 | * @property {string} link - link to person's website 16 | * @property {string} photo - person's photo url 17 | */ 18 | 19 | /** 20 | * @typedef {object} PersonalityConfig 21 | * @description Config supported by Tool 22 | * @property {string} endpoint - image file upload url 23 | * @property {string} field - field name for uploaded image 24 | * @property {string} types - available mime-types 25 | * @property {string} namePlaceholder - placeholder for name field 26 | * @property {string} descriptionPlaceholder - description placeholder 27 | * @property {string} linkPlaceholder - link placeholder 28 | */ 29 | 30 | /** 31 | * @typedef {object} UploadResponseFormat 32 | * @description This format expected from backend on file uploading 33 | * @property {number} success - 1 for successful uploading, 0 for failure 34 | * @property {object} file - Object with file data. 35 | * 'url' is required, 36 | * also can contain any additional data that will be saved and passed back 37 | * @property {string} file.url - [Required] image source URL 38 | */ 39 | 40 | /** 41 | * Personality Tool for the Editor.js 42 | */ 43 | export default class Personality { 44 | /** 45 | * @param {PersonalityToolData} data - Tool's data 46 | * @param {PersonalityConfig} config - Tool's config 47 | * @param {API} api - Editor.js API 48 | */ 49 | constructor({ data, config, api }) { 50 | this.api = api; 51 | 52 | this.nodes = { 53 | wrapper: null, 54 | name: null, 55 | description: null, 56 | link: null, 57 | photo: null 58 | }; 59 | 60 | this.config = { 61 | endpoint: config.endpoint || '', 62 | field: config.field || 'image', 63 | types: config.types || 'image/*', 64 | namePlaceholder: config.namePlaceholder || 'Name', 65 | descriptionPlaceholder: config.descriptionPlaceholder || 'Description', 66 | linkPlaceholder: config.linkPlaceholder || 'Link' 67 | }; 68 | 69 | /** 70 | * Set saved state 71 | */ 72 | this.data = data; 73 | 74 | /** 75 | * Module for image files uploading 76 | */ 77 | this.uploader = new Uploader({ 78 | config: this.config, 79 | onUpload: (response) => this.onUpload(response), 80 | onError: (error) => this.uploadingFailed(error) 81 | }); 82 | } 83 | 84 | /** 85 | * Get Tool toolbox settings 86 | * icon - Tool icon's SVG 87 | * title - title to show in toolbox 88 | */ 89 | static get toolbox() { 90 | return { 91 | icon: ToolboxIcon, 92 | title: 'Personality' 93 | }; 94 | } 95 | 96 | /** 97 | * File uploading callback 98 | * @param {UploadResponseFormat} response 99 | */ 100 | onUpload(response) { 101 | const { body: { success, file } } = response; 102 | 103 | if (success && file && file.url) { 104 | this.data.photo = file.url; 105 | 106 | this.showFullImage(); 107 | } 108 | } 109 | 110 | /** 111 | * On success: remove loader and show full image 112 | */ 113 | showFullImage() { 114 | setTimeout(() => { 115 | this.nodes.photo.classList.remove(this.CSS.loader); 116 | this.nodes.photo.style.background = `url('${this.data.photo}') center center / cover no-repeat`; 117 | }, LOADER_DELAY); 118 | } 119 | 120 | /** 121 | * On fail: remove loader and reveal default image placeholder 122 | */ 123 | stopLoading() { 124 | setTimeout(() => { 125 | this.nodes.photo.classList.remove(this.CSS.loader); 126 | this.nodes.photo.removeAttribute('style'); 127 | }, LOADER_DELAY); 128 | } 129 | 130 | /** 131 | * Show loader when file upload started 132 | */ 133 | addLoader() { 134 | this.nodes.photo.style.background = 'none'; 135 | this.nodes.photo.classList.add(this.CSS.loader); 136 | } 137 | 138 | /** 139 | * If file uploading failed, remove loader and show notification 140 | * @param {string} errorMessage - error message 141 | */ 142 | uploadingFailed(errorMessage) { 143 | this.stopLoading(); 144 | 145 | this.api.notifier.show({ 146 | message: errorMessage, 147 | style: 'error' 148 | }); 149 | } 150 | 151 | /** 152 | * Tool's CSS classes 153 | */ 154 | get CSS() { 155 | return { 156 | baseClass: this.api.styles.block, 157 | input: this.api.styles.input, 158 | loader: this.api.styles.loader, 159 | 160 | /** 161 | * Tool's classes 162 | */ 163 | wrapper: 'cdx-personality', 164 | name: 'cdx-personality__name', 165 | photo: 'cdx-personality__photo', 166 | link: 'cdx-personality__link', 167 | description: 'cdx-personality__description' 168 | }; 169 | } 170 | 171 | /** 172 | * Return Block data 173 | * @param {HTMLElement} toolsContent 174 | * @return {PersonalityToolData} 175 | */ 176 | save(toolsContent) { 177 | const name = toolsContent.querySelector(`.${this.CSS.name}`).textContent; 178 | const description = toolsContent.querySelector(`.${this.CSS.description}`).textContent; 179 | const link = toolsContent.querySelector(`.${this.CSS.link}`).textContent; 180 | const photo = this.data.photo; 181 | 182 | /** 183 | * Fill missing fields with empty strings 184 | */ 185 | Object.assign(this.data, { 186 | name: name.trim() || '', 187 | description: description.trim() || '', 188 | link: link.trim() || '', 189 | photo: photo || '' 190 | }); 191 | 192 | return this.data; 193 | } 194 | 195 | /** 196 | * Renders Block content 197 | * @return {HTMLDivElement} 198 | */ 199 | render() { 200 | const { name, description, photo, link } = this.data; 201 | 202 | this.nodes.wrapper = this.make('div', this.CSS.wrapper); 203 | 204 | this.nodes.name = this.make('div', this.CSS.name, { 205 | contentEditable: true 206 | }); 207 | 208 | this.nodes.description = this.make('div', this.CSS.description, { 209 | contentEditable: true 210 | }); 211 | 212 | this.nodes.link = this.make('div', this.CSS.link, { 213 | contentEditable: true 214 | }); 215 | 216 | this.nodes.photo = this.make('div', this.CSS.photo); 217 | 218 | if (photo) { 219 | this.nodes.photo.style.background = `url('${photo}') center center / cover no-repeat`; 220 | } 221 | 222 | if (description) { 223 | this.nodes.description.textContent = description; 224 | } else { 225 | this.nodes.description.dataset.placeholder = this.config.descriptionPlaceholder; 226 | } 227 | 228 | if (name) { 229 | this.nodes.name.textContent = name; 230 | } else { 231 | this.nodes.name.dataset.placeholder = this.config.namePlaceholder; 232 | } 233 | 234 | if (link) { 235 | this.nodes.link.textContent = link; 236 | } else { 237 | this.nodes.link.dataset.placeholder = this.config.linkPlaceholder; 238 | } 239 | 240 | this.nodes.photo.addEventListener('click', () => { 241 | this.uploader.uploadSelectedFile({ 242 | onPreview: () => { 243 | this.addLoader(); 244 | } 245 | }); 246 | }); 247 | 248 | this.nodes.wrapper.appendChild(this.nodes.photo); 249 | this.nodes.wrapper.appendChild(this.nodes.name); 250 | this.nodes.wrapper.appendChild(this.nodes.description); 251 | this.nodes.wrapper.appendChild(this.nodes.link); 252 | 253 | return this.nodes.wrapper; 254 | } 255 | 256 | /** 257 | * Validate saved data 258 | * @param {PersonalityToolData} savedData - tool's data 259 | * @returns {boolean} - validation result 260 | */ 261 | validate(savedData) { 262 | /** 263 | * Return false if fields are empty 264 | */ 265 | return savedData.name || 266 | savedData.description || 267 | savedData.link || 268 | savedData.photo; 269 | } 270 | 271 | /** 272 | * Helper method for elements creation 273 | * @param tagName 274 | * @param classNames 275 | * @param attributes 276 | * @return {HTMLElement} 277 | */ 278 | make(tagName, classNames = null, attributes = {}) { 279 | const el = document.createElement(tagName); 280 | 281 | if (Array.isArray(classNames)) { 282 | el.classList.add(...classNames); 283 | } else if (classNames) { 284 | el.classList.add(classNames); 285 | } 286 | 287 | for (const attrName in attributes) { 288 | el[attrName] = attributes[attrName]; 289 | } 290 | 291 | return el; 292 | } 293 | } 294 | --------------------------------------------------------------------------------