mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-18 20:59:42 +02:00
Added page navigation (#209)
* Added navigation on page * Removed useless log, added docs to navigator component * Fixed duplicated variables, some changes in navigation functions, changed pages.twig and navigator.twig * Added flatArray model, changed navigation functions * Replaced page footer style to page.pcss * Fixed generating flat array, when pages remove * Removed useless generating * Renamed flatArray model to pagesFlatArray, updated descriptions, renamed generate to regenerate, removed hardcoded key name in cache * Changed styles naming and added margin for navigation * Added ability to change nesting in flat array, fixed BEM * Updated nesting parameter, fixed BEM * Changed navigator component by passing objects, removed navigator wrapper * Style navigator renamed to navigator__item * Update src/backend/models/pagesFlatArray.ts Co-authored-by: Peter Savchenko <specc.dev@gmail.com> * Renamed navigator__item to navigator_item * Deleted wrappers from navigator buttons, removed page__footer Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
213f9d89a3
commit
70f89f28da
15 changed files with 352 additions and 18 deletions
|
@ -2,7 +2,8 @@ import Page, { PageData } from '../models/page';
|
|||
import Alias from '../models/alias';
|
||||
import PagesOrder from './pagesOrder';
|
||||
import PageOrder from '../models/pageOrder';
|
||||
import HttpException from "../exceptions/httpException";
|
||||
import HttpException from '../exceptions/httpException';
|
||||
import PagesFlatArray from '../models/pagesFlatArray';
|
||||
|
||||
type PageDataFields = keyof PageData;
|
||||
|
||||
|
@ -195,6 +196,7 @@ class Pages {
|
|||
pagesAvailable[index] = null;
|
||||
pagesAvailable = Pages.removeChildren(pagesAvailable, item._id);
|
||||
});
|
||||
PagesFlatArray.regenerate();
|
||||
|
||||
return pagesAvailable;
|
||||
}
|
||||
|
@ -221,6 +223,7 @@ class Pages {
|
|||
|
||||
alias.save();
|
||||
}
|
||||
await PagesFlatArray.regenerate();
|
||||
|
||||
return insertedPage;
|
||||
} catch (e) {
|
||||
|
@ -264,6 +267,7 @@ class Pages {
|
|||
Alias.markAsDeprecated(previousUri);
|
||||
}
|
||||
}
|
||||
await PagesFlatArray.regenerate();
|
||||
|
||||
return updatedPage;
|
||||
}
|
||||
|
@ -286,8 +290,10 @@ class Pages {
|
|||
|
||||
await alias.destroy();
|
||||
}
|
||||
const removedPage = page.destroy();
|
||||
await PagesFlatArray.regenerate();
|
||||
|
||||
return page.destroy();
|
||||
return removedPage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PageOrder from '../models/pageOrder';
|
||||
import Page from '../models/page';
|
||||
import PagesFlatArray from '../models/pagesFlatArray';
|
||||
|
||||
/**
|
||||
* @class PagesOrder
|
||||
|
@ -62,6 +63,7 @@ class PagesOrder {
|
|||
|
||||
order.push(childId);
|
||||
await order.save();
|
||||
await PagesFlatArray.regenerate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,11 +78,13 @@ class PagesOrder {
|
|||
|
||||
oldParentOrder.remove(targetPageId);
|
||||
await oldParentOrder.save();
|
||||
await PagesFlatArray.regenerate();
|
||||
|
||||
const newParentOrder = await PageOrder.get(newParentId);
|
||||
|
||||
newParentOrder.push(targetPageId);
|
||||
await newParentOrder.save();
|
||||
await PagesFlatArray.regenerate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,6 +129,7 @@ class PagesOrder {
|
|||
pageOrder.order = Array.from(new Set([...pageOrder.order, ...unordered]));
|
||||
pageOrder.putAbove(currentPageId, putAbovePageId);
|
||||
await pageOrder.save();
|
||||
await PagesFlatArray.regenerate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,7 +143,8 @@ class PagesOrder {
|
|||
throw new Error('Page with given id does not contain order');
|
||||
}
|
||||
|
||||
return order.destroy();
|
||||
await order.destroy();
|
||||
await PagesFlatArray.regenerate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ class PageOrder {
|
|||
*
|
||||
* @param {string} pageId - identity of page
|
||||
*/
|
||||
public getPageBefore(pageId: string): string | null {
|
||||
public getSubPageBefore(pageId: string): string | null {
|
||||
if (this.order === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ class PageOrder {
|
|||
*
|
||||
* @param pageId - identity of page
|
||||
*/
|
||||
public getPageAfter(pageId: string): string | null {
|
||||
public getSubPageAfter(pageId: string): string | null {
|
||||
if (this.order === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
|
182
src/backend/models/pagesFlatArray.ts
Normal file
182
src/backend/models/pagesFlatArray.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import Page from './page';
|
||||
import PageOrder from './pageOrder';
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
// Create cache for flat array
|
||||
const cache = new NodeCache({ stdTTL: 120 });
|
||||
|
||||
const cacheKey = 'pagesFlatArray';
|
||||
|
||||
/**
|
||||
* Element for pagesFlatArray
|
||||
*/
|
||||
export interface PagesFlatArrayData {
|
||||
/**
|
||||
* Page id
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Page parent id
|
||||
*/
|
||||
parentId?: string;
|
||||
|
||||
/**
|
||||
* id of parent with parent id '0'
|
||||
*/
|
||||
rootId: string;
|
||||
|
||||
/**
|
||||
* Page level in sidebar view
|
||||
*/
|
||||
level: number;
|
||||
|
||||
/**
|
||||
* Page title
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Page uri
|
||||
*/
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PagesFlatArray model - flat array of pages, which are ordered like in sidebar
|
||||
*/
|
||||
class PagesFlatArray {
|
||||
/**
|
||||
* Returns pages flat array
|
||||
*
|
||||
* @param nestingLimit - number of flat array nesting, set null to dismiss the restriction, default nesting 2
|
||||
* @returns {Promise<Array<PagesFlatArrayData>>}
|
||||
*/
|
||||
public static async get(nestingLimit: number | null = 2): Promise<Array<PagesFlatArrayData>> {
|
||||
// Get flat array from cache
|
||||
let arr = cache.get(cacheKey) as Array<PagesFlatArrayData>;
|
||||
|
||||
// Check is flat array consists in cache
|
||||
if (!arr) {
|
||||
arr = await this.regenerate();
|
||||
}
|
||||
|
||||
if (!nestingLimit) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
return arr.filter( (item) => item.level < nestingLimit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new flat array, saves it to cache, returns it
|
||||
* Calls, when there is no pages flat array data in cache or when page or pageOrder data updates
|
||||
*
|
||||
* @returns {Promise<Array<PagesFlatArrayData>>}
|
||||
*/
|
||||
public static async regenerate(): Promise<Array<PagesFlatArrayData>> {
|
||||
const pages = await Page.getAll();
|
||||
const pagesOrders = await PageOrder.getAll();
|
||||
|
||||
let arr = new Array<PagesFlatArrayData>();
|
||||
|
||||
// Get root order
|
||||
const rootOrder = pagesOrders.find( order => order.page == '0' );
|
||||
|
||||
// Check is root order is not empty
|
||||
if (!rootOrder) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const pageId of rootOrder.order) {
|
||||
arr = arr.concat(this.getChildrenFlatArray(pageId, 0, pages,
|
||||
pagesOrders));
|
||||
}
|
||||
|
||||
// Save generated flat array to cache
|
||||
cache.set(cacheKey, arr);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns previous page
|
||||
*
|
||||
* @param pageId - page id
|
||||
* @returns {Promise<PagesFlatArrayData | undefined>}
|
||||
*/
|
||||
public static async getPageBefore(pageId: string): Promise<PagesFlatArrayData | undefined> {
|
||||
const arr = await this.get();
|
||||
|
||||
const pageIndex = arr.findIndex( (item) => item.id == pageId);
|
||||
|
||||
// Check if index is not the first
|
||||
if (pageIndex && pageIndex > 0) {
|
||||
// Return previous element from array
|
||||
return arr[pageIndex - 1];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next page
|
||||
*
|
||||
* @param pageId - page id
|
||||
* @returns {Promise<PagesFlatArrayData | undefined>}
|
||||
*/
|
||||
public static async getPageAfter(pageId: string): Promise<PagesFlatArrayData | undefined> {
|
||||
const arr = await this.get();
|
||||
|
||||
const pageIndex = arr.findIndex( (item) => item.id == pageId );
|
||||
|
||||
// Check if index is not the last
|
||||
if (pageIndex < arr.length -1) {
|
||||
// Return next element from array
|
||||
return arr[pageIndex + 1];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns child pages array
|
||||
*
|
||||
* @param pageId - parent page id
|
||||
* @param level - page level in sidebar
|
||||
* @param pages - all pages
|
||||
* @param orders - all page orders
|
||||
* @returns {Promise<Array<PagesFlatArrayData>>}
|
||||
*/
|
||||
private static getChildrenFlatArray(pageId: string, level: number,
|
||||
pages: Array<Page>, orders: Array<PageOrder>): Array<PagesFlatArrayData> {
|
||||
let arr: Array<PagesFlatArrayData> = new Array<PagesFlatArrayData>();
|
||||
|
||||
const page = pages.find( item => item._id == pageId );
|
||||
|
||||
// Add element to child array
|
||||
if (page) {
|
||||
arr.push( {
|
||||
id: page._id!,
|
||||
level: level,
|
||||
parentId: page._parent,
|
||||
rootId: '0',
|
||||
title: page.title!,
|
||||
uri: page.uri,
|
||||
} );
|
||||
}
|
||||
|
||||
const order = orders.find(item => item.page == pageId);
|
||||
|
||||
if (order) {
|
||||
for (const childPageId of order.order) {
|
||||
arr = arr.concat(this.getChildrenFlatArray(childPageId, level + 1,
|
||||
pages, orders));
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
export default PagesFlatArray;
|
|
@ -3,6 +3,7 @@ import Aliases from '../controllers/aliases';
|
|||
import Pages from '../controllers/pages';
|
||||
import Alias from '../models/alias';
|
||||
import verifyToken from './middlewares/token';
|
||||
import PagesFlatArray from '../models/pagesFlatArray';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -32,9 +33,14 @@ router.get('*', verifyToken, async (req: Request, res: Response) => {
|
|||
|
||||
const pageParent = await page.getParent();
|
||||
|
||||
const previousPage = await PagesFlatArray.getPageBefore(alias.id);
|
||||
const nextPage = await PagesFlatArray.getPageAfter(alias.id);
|
||||
|
||||
res.render('pages/page', {
|
||||
page,
|
||||
pageParent,
|
||||
previousPage,
|
||||
nextPage,
|
||||
config: req.app.locals.config,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -158,8 +158,8 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
|
|||
}
|
||||
|
||||
const parentPageOrder = await PagesOrder.get(page._parent);
|
||||
const pageBeforeId = parentPageOrder.getPageBefore(page._id);
|
||||
const pageAfterId = parentPageOrder.getPageAfter(page._id);
|
||||
const pageBeforeId = parentPageOrder.getSubPageBefore(page._id);
|
||||
const pageAfterId = parentPageOrder.getSubPageAfter(page._id);
|
||||
|
||||
let pageToRedirect;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import Pages from '../controllers/pages';
|
|||
import PagesOrder from '../controllers/pagesOrder';
|
||||
import verifyToken from './middlewares/token';
|
||||
import allowEdit from './middlewares/locals';
|
||||
import PagesFlatArray from '../models/pagesFlatArray';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -62,10 +63,15 @@ router.get('/page/:id', verifyToken, async (req: Request, res: Response, next: N
|
|||
|
||||
const pageParent = await page.parent;
|
||||
|
||||
const previousPage = await PagesFlatArray.getPageBefore(pageId);
|
||||
const nextPage = await PagesFlatArray.getPageAfter(pageId);
|
||||
|
||||
res.render('pages/page', {
|
||||
page,
|
||||
pageParent,
|
||||
config: req.app.locals.config,
|
||||
previousPage,
|
||||
nextPage,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(404);
|
||||
|
|
50
src/backend/views/components/navigator.twig
Normal file
50
src/backend/views/components/navigator.twig
Normal file
|
@ -0,0 +1,50 @@
|
|||
{#
|
||||
Reusable navigaor component.
|
||||
Available props:
|
||||
- previousPage
|
||||
- nextPage
|
||||
- class: additional class for the navigator
|
||||
|
||||
Usage example:
|
||||
{% include 'components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
|
||||
#}
|
||||
|
||||
{% set mainClass = 'navigator__item' %}
|
||||
|
||||
|
||||
{% set tag = 'div' %}
|
||||
|
||||
{% set tag = 'a' %}
|
||||
|
||||
<div class="navigator">
|
||||
{% if previousPage %}
|
||||
<{{ tag }}
|
||||
{{ name is not empty ? 'name="' ~ name ~ '"': '' }}
|
||||
class="{{ mainClass }} {{ mainClass }}--previous {{ class ?? '' }}"
|
||||
href="/{{ previousPage.uri }}"
|
||||
>
|
||||
<div class="{{ mainClass }}-direction">
|
||||
previous
|
||||
</div>
|
||||
<div class="{{ mainClass }}-label">
|
||||
{{ previousPage.title }}
|
||||
</div>
|
||||
</{{ tag }}>
|
||||
{% endif %}
|
||||
|
||||
{% if nextPage %}
|
||||
<{{ tag }}
|
||||
{{ name is not empty ? 'name="' ~ name ~ '"': '' }}
|
||||
class="{{ mainClass }} {{ mainClass }}--next {{ class ?? '' }}"
|
||||
href="/{{ nextPage.uri }}"
|
||||
>
|
||||
<div class="{{ mainClass }}-direction">
|
||||
next
|
||||
</div>
|
||||
<div class="{{ mainClass }}-label">
|
||||
{{ nextPage.title }}
|
||||
</div>
|
||||
</{{ tag }}>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -39,8 +39,7 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
<footer>
|
||||
</footer>
|
||||
{% include 'components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
||||
|
|
42
src/frontend/styles/components/navigator.pcss
Normal file
42
src/frontend/styles/components/navigator.pcss
Normal file
|
@ -0,0 +1,42 @@
|
|||
.navigator {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navigator__item {
|
||||
margin-top: 35px;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-link-hover);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
color: black;
|
||||
width: max-content;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
|
||||
&--previous {
|
||||
align-items: flex-start;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&--next {
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&-direction {
|
||||
text-transform: capitalize;
|
||||
color: var(--color-direction-navigation);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -496,3 +496,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
@import './components/auth.pcss';
|
||||
@import './components/button.pcss';
|
||||
@import './components/sidebar.pcss';
|
||||
@import './components/navigator.pcss';
|
||||
@import './components/table-of-content.pcss';
|
||||
|
||||
body {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--color-text-main: #313649;
|
||||
--color-text-second: #5d6068;
|
||||
--color-direction-navigation: #717682;
|
||||
--color-line-gray: #E8E8EB;
|
||||
--color-link-active: #2071cc;
|
||||
--color-link-hover: #F3F6F8;
|
||||
|
@ -124,7 +125,7 @@
|
|||
|
||||
--squircle {
|
||||
border-radius: 8px;
|
||||
|
||||
|
||||
@supports(-webkit-mask-box-image: url('')){
|
||||
border-radius: 0;
|
||||
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10.3872C0 1.83334 1.83334 0 10.3872 0H13.6128C22.1667 0 24 1.83334 24 10.3872V13.6128C24 22.1667 22.1667 24 13.6128 24H10.3872C1.83334 24 0 22.1667 0 13.6128V10.3872Z' fill='black'/%3E%3C/svg%3E%0A") 48% 41% 37.9% 53.3%;;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue