mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-24 15:49:42 +02:00
Page creation basics (#7)
* Page cration basics * remove unused code * add client-side Header validation * remove static method * rm await duplication
This commit is contained in:
parent
5c0560a2ed
commit
073772c047
17 changed files with 476 additions and 93 deletions
|
@ -12,7 +12,7 @@ class Pages {
|
|||
* @returns {['title', 'body']}
|
||||
*/
|
||||
static get REQUIRED_FIELDS() {
|
||||
return ['title', 'body'];
|
||||
return [ 'body' ];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,23 +48,47 @@ class Pages {
|
|||
* @returns {Promise<Page>}
|
||||
*/
|
||||
static async insert(data) {
|
||||
if (!Pages.validate(data)) {
|
||||
throw new Error('Invalid request format');
|
||||
try {
|
||||
Pages.validate(data);
|
||||
|
||||
const page = new Model(data);
|
||||
|
||||
return page.save();
|
||||
} catch (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
|
||||
const page = new Model(data);
|
||||
|
||||
return page.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check PageData object for required fields
|
||||
*
|
||||
* @param {PageData} data
|
||||
* @returns {boolean}
|
||||
* @throws {Error} - validation error
|
||||
*/
|
||||
static validate(data) {
|
||||
return Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
|
||||
const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
|
||||
|
||||
if (!allRequiredFields) {
|
||||
throw new Error('Some of required fields is missed');
|
||||
}
|
||||
|
||||
const hasBlocks = data.body && data.body.blocks && Array.isArray(data.body.blocks) && data.body.blocks.length > 0;
|
||||
|
||||
if (!hasBlocks) {
|
||||
throw new Error('Page body is invalid');
|
||||
}
|
||||
|
||||
const hasHeaderAsFirstBlock = data.body.blocks[0].type === 'header';
|
||||
|
||||
if (!hasHeaderAsFirstBlock) {
|
||||
throw new Error('First page Block must be a Header');
|
||||
}
|
||||
|
||||
const headerIsNotEmpty = data.body.blocks[0].data.text.replace('<br>', '').trim() !== '';
|
||||
|
||||
if (!headerIsNotEmpty) {
|
||||
throw new Error('Please, fill page Header');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,42 @@
|
|||
import CodeXEditor from 'codex.editor';
|
||||
import Header from 'codex.editor.header';
|
||||
|
||||
/**
|
||||
* Class for working with Editor.js
|
||||
*/
|
||||
export default class Editor {
|
||||
/**
|
||||
* Creates Editor instance
|
||||
*/
|
||||
constructor() {
|
||||
this.editor = new CodeXEditor();
|
||||
this.editor = new CodeXEditor({
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
config: {
|
||||
placeholder: 'Enter a title'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
data: {
|
||||
text: '',
|
||||
level: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Editor data
|
||||
* @return {Promise.<{}>}
|
||||
*/
|
||||
save() {
|
||||
return this.editor.saver.save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,115 @@
|
|||
/**
|
||||
* Module for pages create/edit
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} editorData
|
||||
* @property {{type, data}[]} blocks - array of Blocks
|
||||
* @property {string} version - used Editor version
|
||||
* @property {number} time - saving time
|
||||
*/
|
||||
|
||||
export default class Writing {
|
||||
/**
|
||||
* Creates base properties
|
||||
*/
|
||||
constructor(){
|
||||
this.editorWrapper = null;
|
||||
this.editor = null;
|
||||
this.nodes = {
|
||||
editorWrapper: null,
|
||||
saveButton: null,
|
||||
parentIdSelector: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ModuleDispatcher to initialize module from DOM
|
||||
* @param {object} settings - module settings
|
||||
* @param {HTMLElement} moduleEl - module element
|
||||
*/
|
||||
init(config, moduleEl) {
|
||||
this.editorWrapper = document.createElement('div');
|
||||
this.editorWrapper.id = 'codex-editor';
|
||||
init(settings, moduleEl) {
|
||||
/**
|
||||
* Create Editor
|
||||
*/
|
||||
this.nodes.editorWrapper = document.createElement('div');
|
||||
this.nodes.editorWrapper.id = 'codex-editor';
|
||||
|
||||
moduleEl.appendChild(this.editorWrapper);
|
||||
moduleEl.appendChild(this.nodes.editorWrapper);
|
||||
|
||||
this.loadEditor().then((editor) => {
|
||||
this.editor = editor;
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Activate form elements
|
||||
*/
|
||||
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit"]');
|
||||
this.nodes.saveButton.addEventListener('click', () => {
|
||||
this.saveButtonClicked();
|
||||
});
|
||||
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads class for working with Editor
|
||||
* @return {Promise<Editor>}
|
||||
*/
|
||||
async loadEditor(){
|
||||
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor');
|
||||
|
||||
return new Editor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all writing form data
|
||||
* @throws {Error} - validation error
|
||||
* @return {Promise.<{parent: string, body: {editorData}}>}
|
||||
*/
|
||||
async getData(){
|
||||
const editorData = await this.editor.save();
|
||||
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
|
||||
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Entry should start with Header');
|
||||
}
|
||||
|
||||
return {
|
||||
parent: this.nodes.parentIdSelector.value,
|
||||
body: editorData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for clicks on the Save button
|
||||
*/
|
||||
async saveButtonClicked(){
|
||||
try {
|
||||
const writingData = await this.getData();
|
||||
|
||||
try {
|
||||
let response = await fetch('/page', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(writingData),
|
||||
});
|
||||
|
||||
response = await response.json();
|
||||
|
||||
if (response.success){
|
||||
document.location = '/page/' + response.result._id;
|
||||
} else {
|
||||
alert(response.error);
|
||||
console.log('Validation failed:', response.error);
|
||||
}
|
||||
|
||||
} catch (sendingError) {
|
||||
console.log('Saving request failed:', sendingError);
|
||||
}
|
||||
} catch (savingError){
|
||||
alert(savingError);
|
||||
console.log('Saving error: ', savingError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
20
src/frontend/styles/components/writing.pcss
Normal file
20
src/frontend/styles/components/writing.pcss
Normal file
|
@ -0,0 +1,20 @@
|
|||
.writing-header {
|
||||
display: flex;
|
||||
padding: 15px 0;
|
||||
margin-top: calc(-1 * var(--layout-padding-vertical));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
z-index: 2;
|
||||
box-shadow: 0 3px 10px #fff;
|
||||
|
||||
&__save {
|
||||
@apply --button;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__left {
|
||||
margin: auto 0;
|
||||
color: var(--color-text-second);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,6 @@
|
|||
|
||||
&__aside,
|
||||
&__content {
|
||||
padding: 40px 0;
|
||||
padding: var(--layout-padding-vertical) 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
@import url('layout.pcss');
|
||||
@import url('components/header.pcss');
|
||||
@import url('components/aside.pcss');
|
||||
@import url('components/writing.pcss');
|
||||
|
||||
body {
|
||||
font-family: system-ui, Helvetica, Arial, Verdana;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
* Site layout sizes
|
||||
*/
|
||||
--layout-padding-horisontal: 40px;
|
||||
--layout-padding-vertical: 40px;
|
||||
--layout-width-aside: 250px;
|
||||
--layout-width-main-col: 650px;
|
||||
|
||||
|
|
|
@ -67,10 +67,10 @@ class Page {
|
|||
* @param {PageData} pageData
|
||||
*/
|
||||
set data(pageData) {
|
||||
const {title, body, parent} = pageData;
|
||||
const {body, parent} = pageData;
|
||||
|
||||
this.title = title || this.title;
|
||||
this.body = body || this.body;
|
||||
this.title = this.extractTitleFromBody();
|
||||
this._parent = parent || this._parent;
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,16 @@ class Page {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract first header from editor data
|
||||
* @return {string}
|
||||
*/
|
||||
extractTitleFromBody() {
|
||||
const headerBlock = this.body ? this.body.blocks.find(block => block.type === 'header') : '';
|
||||
|
||||
return headerBlock ? headerBlock.data.text : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Link given page as parent
|
||||
*
|
||||
|
|
|
@ -7,7 +7,11 @@ const Pages = require('../controllers/pages');
|
|||
* Create new page form
|
||||
*/
|
||||
router.get('/page/new', async (req, res) => {
|
||||
res.render('pages/form');
|
||||
let pagesAvailable = await Pages.getAll();
|
||||
|
||||
res.render('pages/form', {
|
||||
pagesAvailable
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.docs-header__button {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
<section data-module="writing">
|
||||
<header class="writing-header">
|
||||
<span class="writing-header__left">
|
||||
New Page at the
|
||||
<select name="parent">
|
||||
<option value="0">Root</option>
|
||||
{% for page in pagesAvailable %}
|
||||
<option value="{{ page._id }}">{{ page.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
<span class="writing-header__save" name="js-submit">
|
||||
Save
|
||||
</span>
|
||||
</header>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue