├── 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 |
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 | 
2 |
3 | # Personality Tool
4 |
5 | Personality Tool for the [Editor.js](https://editorjs.io).
6 |
7 | 
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 |
--------------------------------------------------------------------------------