1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-25 08:09:41 +02:00

Editorjs checklist tool (#98)

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
Alexander Menshikov 2020-05-09 05:38:25 +03:00 committed by GitHub
parent c0a4f6f3fd
commit b744ed592a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2628 additions and 2152 deletions

View file

@ -6,7 +6,6 @@ const Alias = require('../models/alias');
*/
class Aliases {
/**
* @static
* Find and return entity with given alias
*
* @param {string} aliasName - alias name of entity

View file

@ -7,7 +7,6 @@ const Alias = require('../models/alias');
*/
class Pages {
/**
* @static
* Fields required for page model creation
*
* @returns {['title', 'body']}
@ -17,7 +16,6 @@ class Pages {
}
/**
* @static
* Find and return page model with passed id
*
* @param {string} id - page id
@ -43,20 +41,18 @@ class Pages {
}
/**
* @static
* Return all pages without children of passed page
*
* @param {string} parent - id of current page
* @returns {Promise<Page[]>}
*/
static async getAllExceptChildren(parent) {
let pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
const pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
return pagesAvailable.filter((item) => item !== null);
}
/**
* @static
* Set all children elements to null
*
* @param {Page[]} [pagesAvailable] - Array of all pages
@ -71,6 +67,7 @@ class Pages {
pagesAvailable[index] = null;
pagesAvailable = Pages.removeChildren(pagesAvailable, item._id);
});
return pagesAvailable;
}
@ -91,7 +88,7 @@ class Pages {
if (insertedPage.uri) {
const alias = new Alias({
id: insertedPage._id,
type: Alias.types.PAGE
type: Alias.types.PAGE,
}, insertedPage.uri);
alias.save();
@ -161,7 +158,7 @@ class Pages {
if (updatedPage.uri) {
const alias = new Alias({
id: updatedPage._id,
type: Alias.types.PAGE
type: Alias.types.PAGE,
}, updatedPage.uri);
alias.save();

View file

@ -70,8 +70,8 @@ class PagesOrder {
* @param {Page[]} pages - list of all available pages
* @param {string} currentPageId - page's id around which we are ordering
* @param {string} parentPageId - parent page's id that contains page above
* @param {Boolean} ignoreSelf - should we ignore current page in list or not
* @return {Page[]}
* @param {boolean} ignoreSelf - should we ignore current page in list or not
* @returns {Page[]}
*/
static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) {
const children = await Model.get(parentPageId);

View file

@ -17,6 +17,7 @@ const config = require('../../config');
class Transport {
/**
* Saves file passed from client
*
* @param {object} multerData - file data from multer
* @param {string} multerData.originalname - original name of the file
* @param {string} multerData.filename - name of the uploaded file
@ -25,12 +26,18 @@ class Transport {
* @param {string} multerData.mimetype - MIME type of the uploaded file
*
* @param {object} map - object that represents how should fields of File object should be mapped to response
* @return {Promise<FileData>}
* @returns {Promise<FileData>}
*/
static async save(multerData, map) {
const { originalname: name, path, filename, size, mimetype } = multerData;
const file = new Model({ name, filename, path, size, mimetype });
const file = new Model({
name,
filename,
path,
size,
mimetype,
});
await file.save();
@ -45,9 +52,10 @@ class Transport {
/**
* Fetches file by passed URL
*
* @param {string} url - URL of the file
* @param {object} map - object that represents how should fields of File object should be mapped to response
* @return {Promise<FileData>}
* @returns {Promise<FileData>}
*/
static async fetch(url, map) {
const fetchedFile = await fetch(url);
@ -64,7 +72,7 @@ class Transport {
filename: `${filename}.${ext}`,
path: `${config.uploads}/${filename}.${ext}`,
size: buffer.length,
mimetype: type ? type.mime : fetchedFile.headers.get('content-type')
mimetype: type ? type.mime : fetchedFile.headers.get('content-type'),
});
await file.save();
@ -94,11 +102,12 @@ class Transport {
if (fields.length > 1) {
let object = {};
let result = object;
const result = object;
fields.forEach((field, i) => {
if (i === fields.length - 1) {
object[field] = data[name];
return;
}

View file

@ -6,7 +6,6 @@ const Model = require('../models/user');
*/
class Users {
/**
* @static
* Find and return user model.
*
* @returns {Promise<User>}

View file

@ -10,6 +10,7 @@ import List from '@editorjs/list';
import Delimiter from '@editorjs/delimiter';
import Table from '@editorjs/table';
import Warning from '@editorjs/warning';
import Checklist from '@editorjs/checklist';
/**
* Inline Tools for the Editor
@ -80,6 +81,11 @@ export default class Editor {
inlineToolbar: true
},
checklist: {
class: Checklist,
inlineToolbar: true,
},
/**
* Inline Tools
*/

View file

@ -362,3 +362,59 @@
padding-left: 15px;
}
}
/**
* Checklist
* ==================
*/
.block-checklist {
margin: 20px 0;
&__item {
display: flex;
box-sizing: content-box;
align-items: center;
&-checkbox {
display: inline-block;
flex-shrink: 0;
position: relative;
width: 20px;
height: 20px;
margin: 0 10px 0 0;
border-radius: 50%;
border: 1px solid #d0d0d0;
background: #fff;
user-select: none;
&::after {
position: absolute;
top: 5px;
left: 5px;
width: 8px;
height: 5px;
border: 2px solid #fcfff4;
border-top: none;
border-right: none;
background: transparent;
content: '';
opacity: 0;
transform: rotate(-45deg);
}
&--checked {
background: #388ae5;
border-color: #388ae5;
}
}
&-text {
outline: none;
flex-grow: 1;
padding: 5px 0;
}
}
}
.block-checklist__item-checkbox--checked, .block-checklist__item-checkbox::after {
opacity: 1;
}

View file

@ -2,7 +2,7 @@ const { aliases: aliasesDb } = require('../utils/database/index');
const { binaryMD5 } = require('../utils/crypto');
/**
* @typedef {Object} AliasData
* @typedef {object} AliasData
* @property {string} _id - alias id
* @property {string} hash - alias binary hash
* @property {string} type - entity type
@ -25,22 +25,26 @@ class Alias {
/**
* Return Alias types
*
* @returns {Object}
* @returns {object}
*/
static get types() {
return {
PAGE: 'page'
PAGE: 'page',
};
};
/**
* Find and return alias with given alias
*
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
const hash = binaryMD5(aliasName);
let data = await aliasesDb.findOne({ hash: hash, deprecated: false });
let data = await aliasesDb.findOne({
hash: hash,
deprecated: false,
});
if (!data) {
data = await aliasesDb.findOne({ hash: hash });
@ -50,7 +54,7 @@ class Alias {
}
/**
* @constructor
* @class
*
* @param {AliasData} data
* @param {string} aliasName - alias of entity
@ -110,12 +114,13 @@ class Alias {
id: this.id,
type: this.type,
hash: this.hash,
deprecated: this.deprecated
deprecated: this.deprecated,
};
}
/**
* Mark alias as deprecated
*
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/

View file

@ -1,7 +1,7 @@
const { files: filesDb } = require('../utils/database/index');
/**
* @typedef {Object} FileData
* @typedef {object} FileData
*
* @property {string} _id - file id
* @property {string} name - original file name
@ -25,6 +25,7 @@ const { files: filesDb } = require('../utils/database/index');
class File {
/**
* Find and return model of file with given id
*
* @param {string} _id - file id
* @returns {Promise<File>}
*/
@ -36,6 +37,7 @@ class File {
/**
* Find and return model of file with given id
*
* @param {string} filename - uploaded filename
* @returns {Promise<File>}
*/
@ -48,7 +50,7 @@ class File {
/**
* Find all files which match passed query object
*
* @param {Object} query
* @param {object} query
* @returns {Promise<File[]>}
*/
static async getAll(query = {}) {
@ -58,7 +60,7 @@ class File {
}
/**
* @constructor
* @class
*
* @param {FileData} data
*/
@ -101,7 +103,7 @@ class File {
filename: this.filename,
path: this.path,
mimetype: this.mimetype,
size: this.size
size: this.size,
};
}
@ -137,8 +139,9 @@ class File {
/**
* Removes unnecessary public folder prefix
*
* @param {string} path
* @return {string}
* @returns {string}
*/
processPath(path) {
return path.replace(/^public/, '');

View file

@ -2,7 +2,7 @@ const urlify = require('../utils/urlify');
const { pages: pagesDb } = require('../utils/database/index');
/**
* @typedef {Object} PageData
* @typedef {object} PageData
* @property {string} _id - page id
* @property {string} title - page title
* @property {string} uri - page uri
@ -23,6 +23,7 @@ const { pages: pagesDb } = require('../utils/database/index');
class Page {
/**
* Find and return model of page with given id
*
* @param {string} _id - page id
* @returns {Promise<Page>}
*/
@ -34,6 +35,7 @@ class Page {
/**
* Find and return model of page with given uri
*
* @param {string} uri - page uri
* @returns {Promise<Page>}
*/
@ -46,7 +48,7 @@ class Page {
/**
* Find all pages which match passed query object
*
* @param {Object} query
* @param {object} query
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
@ -56,7 +58,7 @@ class Page {
}
/**
* @constructor
* @class
*
* @param {PageData} data
*/
@ -89,7 +91,7 @@ class Page {
/**
* Return PageData object
*
* @return {PageData}
* @returns {PageData}
*/
get data() {
return {
@ -97,13 +99,14 @@ class Page {
title: this.title,
uri: this.uri,
body: this.body,
parent: this._parent
parent: this._parent,
};
}
/**
* Extract first header from editor data
* @return {string}
*
* @returns {string}
*/
extractTitleFromBody() {
const headerBlock = this.body ? this.body.blocks.find(block => block.type === 'header') : '';
@ -113,7 +116,8 @@ class Page {
/**
* Transform title for uri
* @return {string}
*
* @returns {string}
*/
transformTitleToUri() {
return urlify(this.title);
@ -184,6 +188,7 @@ class Page {
* Find and return available uri
*
* @returns {Promise<string>}
* @param uri
*/
async composeUri(uri) {
let pageWithSameUriCount = 0;

View file

@ -1,7 +1,7 @@
const { pagesOrder: db } = require('../utils/database/index');
/**
* @typedef {Object} PageOrderData
* @typedef {object} PageOrderData
* @property {string} _id - row unique id
* @property {string} page - page id
* @property {Array<string>} order - list of ordered pages
@ -37,7 +37,7 @@ class PageOrder {
/**
* Find all pages which match passed query object
*
* @param {Object} query
* @param {object} query
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
@ -47,7 +47,7 @@ class PageOrder {
}
/**
* @constructor
* @class
*
* @param {PageOrderData} data
*/
@ -65,6 +65,7 @@ class PageOrder {
/**
* constructor data setter
*
* @param {PageOrderData} pageOrderData
*/
set data(pageOrderData) {
@ -74,13 +75,14 @@ class PageOrder {
/**
* Return Page Children order
*
* @returns {PageOrderData}
*/
get data() {
return {
_id: this._id,
page: '' + this._page,
order: this._order
order: this._order,
};
}
@ -176,7 +178,7 @@ class PageOrder {
/**
* Returns ordered list
*
* @return {string[]}
* @returns {string[]}
*/
get order() {
return this._order;

View file

@ -24,9 +24,9 @@ class User {
}
/**
* @constructor
* @class
*
* @param {Object} userData
* @param {object} userData
*/
constructor(userData) {
this.passHash = userData.passHash;

View file

@ -23,19 +23,20 @@ router.get('*', verifyToken, async (req, res) => {
switch (alias.type) {
case Alias.types.PAGE: {
let page = await Pages.get(alias.id);
const page = await Pages.get(alias.id);
let pageParent = await page.parent;
const pageParent = await page.parent;
res.render('pages/page', {
page, pageParent
page,
pageParent,
});
}
}
} catch (err) {
res.status(400).json({
success: false,
error: err.message
error: err.message,
});
}
});

View file

@ -16,7 +16,7 @@ const parseForm = express.urlencoded({ extended: false });
router.get('/auth', csrfProtection, function (req, res) {
res.render('auth', {
title: 'Login page',
csrfToken: req.csrfToken()
csrfToken: req.csrfToken(),
});
});
@ -24,13 +24,13 @@ router.get('/auth', csrfProtection, function (req, res) {
* Process given password
*/
router.post('/auth', parseForm, csrfProtection, async (req, res) => {
let userDoc = await Users.get();
const userDoc = await Users.get();
if (!userDoc) {
res.render('auth', {
title: 'Login page',
header: 'Password not set',
csrfToken: req.csrfToken()
csrfToken: req.csrfToken(),
});
}
@ -41,14 +41,14 @@ router.post('/auth', parseForm, csrfProtection, async (req, res) => {
res.render('auth', {
title: 'Login page',
header: 'Wrong password',
csrfToken: req.csrfToken()
csrfToken: req.csrfToken(),
});
}
const token = jwt.sign({
'iss': 'Codex Team',
'sub': 'auth',
'iat': Date.now()
iss: 'Codex Team',
sub: 'auth',
iat: Date.now(),
}, passHash + config.secret);
res.cookie('authToken', token, { httpOnly: true });

View file

@ -10,11 +10,11 @@ const allowEdit = require('./middlewares/locals');
* Create new page form
*/
router.get('/page/new', verifyToken, allowEdit, async (req, res, next) => {
let pagesAvailable = await Pages.getAll();
const pagesAvailable = await Pages.getAll();
res.render('pages/form', {
pagesAvailable,
page: null
page: null,
});
});
@ -32,7 +32,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req, res, next) => {
res.render('pages/form', {
page,
parentsChildrenOrdered,
pagesAvailable
pagesAvailable,
});
} catch (error) {
res.status(404);
@ -47,12 +47,13 @@ router.get('/page/:id', verifyToken, async (req, res, next) => {
const pageId = req.params.id;
try {
let page = await Pages.get(pageId);
const page = await Pages.get(pageId);
let pageParent = await page.parent;
const pageParent = await page.parent;
res.render('pages/page', {
page, pageParent
page,
pageParent,
});
} catch (error) {
res.status(404);

View file

@ -1,7 +1,8 @@
/**
* Helper for making async middlewares for express router
*
* @param fn
* @return {function(*=, *=, *=)}
* @returns {function(*=, *=, *=)}
*/
module.exports = function asyncMiddleware(fn) {
return (req, res, next) => {

View file

@ -2,6 +2,7 @@ const crypto = require('crypto');
/**
* Create binary md5
*
* @param stringToHash - string to hash
* @returns {string} - binary hash of argument
*/
@ -13,7 +14,8 @@ function binaryMD5(stringToHash) {
/**
* Returns 16 random bytes in hex format
* @return {Promise<string>}
*
* @returns {Promise<string>}
*/
function random16() {
return new Promise((resolve, reject) => {
@ -29,5 +31,5 @@ function random16() {
module.exports = {
binaryMD5,
random16
random16,
};

View file

@ -1,13 +1,16 @@
/**
* Merge to objects recursively
*
* @param {object} target
* @param {object[]} sources
* @return {object}
* @returns {object}
*/
function deepMerge(target, ...sources) {
const isObject = item => item && typeof item === 'object' && !Array.isArray(item);
if (!sources.length) return target;
if (!sources.length) {
return target;
}
const source = sources.shift();
if (isObject(target) && isObject(source)) {
@ -28,5 +31,5 @@ function deepMerge(target, ...sources) {
}
module.exports = {
deepMerge
deepMerge,
};

View file

@ -4,7 +4,7 @@ const config = require('../../config');
const rcPath = path.resolve(__dirname, '../../', config.rcFile || './.codexdocsrc');
/**
* @typedef {Object} RCData
* @typedef {object} RCData
* @property {string} title - website title
* @property {object[]} menu - options for website menu
* @property {string} menu[].title - menu option title
@ -20,12 +20,12 @@ module.exports = class RCParser {
* Default CodeX Docs configuration
*
* @static
* @return {{title: string, menu: Array}}
* @returns {{title: string, menu: Array}}
*/
static get DEFAULTS() {
return {
title: 'CodeX Docs',
menu: []
menu: [],
};
}
@ -33,7 +33,7 @@ module.exports = class RCParser {
* Find and parse runtime configuration file
*
* @static
* @return {{title: string, menu: []}}
* @returns {{title: string, menu: []}}
*/
static getConfiguration() {
if (!fs.existsSync(rcPath)) {
@ -48,11 +48,12 @@ module.exports = class RCParser {
userConfig = JSON.parse(file);
} catch (e) {
console.log('CodeX Docs rc file should be in JSON format.');
return RCParser.DEFAULTS;
}
for (let option in userConfig) {
if (userConfig.hasOwnProperty(option)) {
for (const option in userConfig) {
if (Object.prototype.hasOwnProperty.call(userConfig, option)) {
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
}
}
@ -70,6 +71,7 @@ module.exports = class RCParser {
if (!option || option instanceof Array || typeof option !== 'object') {
console.log(`Menu option #${i} in rc file must be a string or an object`);
return false;
}
@ -77,11 +79,13 @@ module.exports = class RCParser {
if (!title || typeof title !== 'string') {
console.log(`Menu option #${i} title must be a string.`);
return false;
}
if (!uri || typeof uri !== 'string') {
console.log(`Menu option #${i} uri must be a string.`);
return false;
}
@ -93,7 +97,7 @@ module.exports = class RCParser {
return {
title: option,
/* Replace all non alpha- and numeric-symbols with '-' */
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-')
uri: '/' + option.toLowerCase().replace(/[ -/:-@[-`{-~]+/, '-'),
};
}

View file

@ -1,70 +1,70 @@
const translationTable = {
'а': 'a',
'б': 'b',
'в': 'v',
'г': 'g',
'д': 'd',
'е': 'e',
'ж': 'g',
'з': 'z',
'и': 'i',
'й': 'y',
'к': 'k',
'л': 'l',
'м': 'm',
'н': 'n',
'о': 'o',
'п': 'p',
'р': 'r',
'с': 's',
'т': 't',
'у': 'u',
'ф': 'f',
'ы': 'i',
'э': 'e',
'А': 'A',
'Б': 'B',
'В': 'V',
'Г': 'G',
'Д': 'D',
'Е': 'E',
'Ж': 'G',
'З': 'Z',
'И': 'I',
'Й': 'Y',
'К': 'K',
'Л': 'L',
'М': 'M',
'Н': 'N',
'О': 'O',
'П': 'P',
'Р': 'R',
'С': 'S',
'Т': 'T',
'У': 'U',
'Ф': 'F',
'Ы': 'I',
'Э': 'E',
'ё': 'yo',
'х': 'h',
'ц': 'ts',
'ч': 'ch',
'ш': 'sh',
'щ': 'shch',
'ъ': "''",
'ь': "'",
'ю': 'yu',
'я': 'ya',
'Ё': 'YO',
'Х': 'H',
'Ц': 'TS',
'Ч': 'CH',
'Ш': 'SH',
'Щ': 'SHCH',
'Ъ': "''",
'Ь': "'",
'Ю': 'YU',
'Я': 'YA'
а: 'a',
б: 'b',
в: 'v',
г: 'g',
д: 'd',
е: 'e',
ж: 'g',
з: 'z',
и: 'i',
й: 'y',
к: 'k',
л: 'l',
м: 'm',
н: 'n',
о: 'o',
п: 'p',
р: 'r',
с: 's',
т: 't',
у: 'u',
ф: 'f',
ы: 'i',
э: 'e',
А: 'A',
Б: 'B',
В: 'V',
Г: 'G',
Д: 'D',
Е: 'E',
Ж: 'G',
З: 'Z',
И: 'I',
Й: 'Y',
К: 'K',
Л: 'L',
М: 'M',
Н: 'N',
О: 'O',
П: 'P',
Р: 'R',
С: 'S',
Т: 'T',
У: 'U',
Ф: 'F',
Ы: 'I',
Э: 'E',
ё: 'yo',
х: 'h',
ц: 'ts',
ч: 'ch',
ш: 'sh',
щ: 'shch',
ъ: "''",
ь: "'",
ю: 'yu',
я: 'ya',
Ё: 'YO',
Х: 'H',
Ц: 'TS',
Ч: 'CH',
Ш: 'SH',
Щ: 'SHCH',
Ъ: "''",
Ь: "'",
Ю: 'YU',
Я: 'YA',
};
/**
* Function to translate string

View file

@ -13,7 +13,7 @@ module.exports = (function () {
*
* @example svg('path/from/root/dir')
* @param filename - name of icon
* @returns {String} - svg code
* @returns {string} - svg code
*/
twig.extendFunction('svg', function (filename) {
return fs.readFileSync(`${__dirname}/../frontend/svg/${filename}.svg`, 'utf-8');

View file

@ -0,0 +1,12 @@
<div class="block-checklist">
{% for item in items %}
<div class="block-checklist__item">
{% if item.checked %}
<span class="block-checklist__item-checkbox block-checklist__item-checkbox--checked"></span>
{% else %}
<span class="block-checklist__item-checkbox"></span>
{% endif %}
<div class="block-checklist__item-text">{{ item.text }}</div>
</div>
{% endfor %}
</div>

View file

@ -32,7 +32,7 @@
{% for block in page.body.blocks %}
{# Skip first header, because it is already showed as a Title #}
{% if not (loop.first and block.type == 'header') %}
{% if block.type in ['paragraph', 'header', 'image', 'code', 'list', 'delimiter', 'table', 'warning'] %}
{% if block.type in ['paragraph', 'header', 'image', 'code', 'list', 'delimiter', 'table', 'warning', 'checklist'] %}
{% include './blocks/' ~ block.type ~ '.twig' with block.data %}
{% endif %}
{% endif %}