Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/blocks/.stylelintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": ["stylelint-config-idiomatic-order"],
"plugins": ["stylelint-prettier"],
"overrides": [
{
"files": ["**/*.scss"],
"customSyntax": "postcss-scss"
}
],
"rules": {
"prettier/prettier": true,
"order/properties-alphabetical-order": null
}
}
10 changes: 10 additions & 0 deletions packages/blocks/Text/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import config from '@plone/registry';

const TextBlockInfo = {
id: 'slate',
Expand All @@ -10,6 +11,15 @@ const TextBlockInfo = {
() => import(/* webpackChunkName: "plone-blocks" */ './TextBlockEdit'),
),
category: 'text',
tocEntry: (block = {}) => {
const { value, override_toc, entry_text, level, plaintext } = block;
const type = value?.[0]?.type;
return override_toc && level
? [parseInt(level.slice(1)), entry_text]
: config.settings.slate.topLevelTargetElements.includes(type)
? [parseInt(type.slice(1)), plaintext]
: null;
},
};

export default TextBlockInfo;
191 changes: 191 additions & 0 deletions packages/blocks/ToC/ToCBlockView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { useMemo } from 'react';
import clsx from 'clsx';
import config from '@plone/registry';
import type { BlocksFormData, BlockViewProps, Content } from '@plone/types';
import { getBlocksFieldName } from '@plone/helpers';
import './styles/ToC.css';

export const getBlocksTocEntries = (
properties: Content,
tocData: BlocksFormData,
): ReturnedToCEntries => {
const blocksFieldName = getBlocksFieldName(properties);
const blocksLayoutFieldName = getBlocksFieldName(properties, 'blocks_layout');

const blocks = properties[blocksFieldName];
const blocks_layout = properties[blocksLayoutFieldName];

const levels =
tocData.levels?.length > 0
? tocData.levels.map((l) => parseInt(l.slice(1)))
: [1, 2, 3, 4, 5, 6];
let rootLevel = Infinity;
let blocksFormEntries = [];
const tocEntries = {};
const tocEntriesLayout = [];

blocks_layout.items.forEach((id) => {
const block = blocks[id];
const blockConfig = config.blocks.blocksConfig[block['@type']];

if (!block || !blockConfig) {
return null;
}
if (!blockConfig.tocEntries && !blockConfig.tocEntry) {
return null;
}

const blockTocEntry = blockConfig.tocEntry?.(block, tocData);

const blockTocEntries = [
...(blockConfig.tocEntries?.(block, tocData) ||
(blockTocEntry ? [blockTocEntry] : [])),
];

blocksFormEntries = [...blocksFormEntries, ...blockTocEntries];

blockTocEntries.forEach((entry, index) => {
const tocEntryId = `${id}-${index}`;
const level = entry[0];
const title = entry[1];
const items = [];
if (!level || !levels.includes(level)) return;
tocEntriesLayout.push(tocEntryId);
tocEntries[tocEntryId] = {
level,
title: title || block.plaintext,
items,
id: tocEntryId,
};
if (level < rootLevel) {
rootLevel = level;
}
});
});

return {
rootLevel,
blocksFormEntries,
tocEntries,
tocEntriesLayout,
};
};

const ToCBlockView = (props: BlockViewProps) => {
const { data, blocksConfig } = props;

const title = data.title && !data.hide_title ? data.title : '';
const metadata = props.metadata || props.properties;
const blocksFieldName = getBlocksFieldName(metadata);
const variation = (blocksConfig.toc.variations || []).find(
(v) => v.id === data.variation,
);
const Renderer = variation?.view;

const levels = useMemo(
() =>
data.levels?.length > 0
? data.levels.map((l) => parseInt(l.slice(1)))
: [1, 2, 3, 4, 5, 6],
[data],
);

const tocEntries = useMemo(() => {
const entries = [];
let prevEntry: Partial<ToCEntry> = {};
const { rootLevel, tocEntries, tocEntriesLayout } = getBlocksTocEntries(
metadata,
data,
);

tocEntriesLayout.forEach((id) => {
const block = metadata[blocksFieldName][id];
if (typeof block === 'undefined') {
return null;
}
if (!config.blocks.blocksConfig[block['@type']]?.tocEntry) return null;
const entry = config.blocks.blocksConfig[block['@type']]?.tocEntry(
block,
data,
);

if (entry) {
const level = entry[0];
const title = entry[1];
const items = [];
if (!title?.trim() && !block.plaintext?.trim()) return;
if (!level || !levels.includes(level)) return;
tocEntriesLayout.push(id);
tocEntries[id] = {
level,
title: title || block.plaintext,
items,
id,
override_toc: block.override_toc,
plaintext: block.plaintext,
};
}
});

tocEntriesLayout.forEach((id) => {
const entry = tocEntries[id];
if (entry.level === rootLevel) {
entries.push(entry);
prevEntry = entry;
return;
}
if (!prevEntry.id) return;
if (entry.level > prevEntry.level) {
entry.parentId = prevEntry.id;
(prevEntry.items || []).push(entry);
prevEntry = entry;
} else if (entry.level < prevEntry.level) {
let parent = tocEntries[prevEntry.parentId];
while (entry.level <= parent.level) {
parent = tocEntries[parent.parentId];
}
entry.parentId = parent.id;
parent.items.push(entry);
prevEntry = entry;
} else {
entry.parentId = prevEntry.parentId;
tocEntries[prevEntry.parentId].items.push(entry);
prevEntry = entry;
}
});

return entries;
}, [data, levels, metadata, blocksFieldName]);

return tocEntries.length > 0 ? (
<nav
aria-label={title as string}
className={clsx('table-of-contents', variation?.id)}
>
{Renderer ? (
<Renderer {...props} tocEntries={tocEntries} metadata={metadata} />
) : (
<div>View extension not found</div>
)}
</nav>
) : null;
};

export interface ToCEntry {
id: string;
level: number;
title: string;
items: ToCEntry[];
override_toc?: boolean;
plaintext?: string;
parentId?: string;
}

interface ReturnedToCEntries {
tocEntries: Record<string, ToCEntry>;
rootLevel: number;
blocksFormEntries: Array<[number, string]>;
tocEntriesLayout: string[];
}

export default ToCBlockView;
17 changes: 17 additions & 0 deletions packages/blocks/ToC/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import ToCVariations from './variations';

const ToCBlockInfo = {
id: 'toc',
title: 'Table of Contents',
view: React.lazy(
() => import(/* webpackChunkName: "plone-blocks" */ './ToCBlockView'),
),
// edit: React.lazy(
// () => import(/* webpackChunkName: "plone-blocks" */ './ToCBlockEdit'),
// ),
variations: ToCVariations,
category: 'common',
};

export default ToCBlockInfo;
32 changes: 32 additions & 0 deletions packages/blocks/ToC/styles/ToC.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
a {
color: #007eb1;
}

a:hover {
color: #006b96;
}

ol.ui.list,
.ui.ordered.list,
.ui.ordered.list .list,
ol.ui.list ol {
margin-left: 1.25rem;
counter-reset: ordered;
list-style-type: decimal;
}

ul.ui.list,
.ui.bulleted.list,
.ui.bulleted.list .list,
ul.ui.list ul {
list-style-type: disc;
}

ul.ui.list,
.ui.bulleted.list,
ol.ui.list,
.ui.ordered.list,
.ui.ordered.list .list,
ol.ui.list ol {
margin-left: 1.25rem;
}
67 changes: 67 additions & 0 deletions packages/blocks/ToC/variations/DefaultToCRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { clsx } from 'clsx';
import Slugger from 'github-slugger';
import { Link } from '@plone/components';
import { normalizeString } from '@plone/helpers';
import type { BlocksFormData } from '@plone/types';
import type { ToCEntry } from '../ToCBlockView';

const slugger = new Slugger();

const RenderListItems = ({ items, data }) => {
return items.map((item) => {
const { id, level, title, override_toc, plaintext } = item;
const slug = override_toc
? slugger.slug(normalizeString(plaintext))
: slugger.slug(normalizeString(title)) || id;
const List = data.ordered ? 'ol' : 'ul';

return (
item && (
<li
key={id}
className={`
item
headline-${level}
`}
>
<Link href={`#${slug}`}>{title}</Link>
{item.items?.length > 0 && (
<List
className={clsx('ui list', {
ordered: data.ordered,
bulleted: !data.ordered,
})}
>
<RenderListItems items={item.items} data={data} />
</List>
)}
</li>
)
);
});
};

const View = ({ data, tocEntries }: DefaultToCProps) => {
const List = data.ordered ? 'ol' : 'ul';

return (
<>
{data.title && !data.hide_title ? <h2>{data.title}</h2> : ''}
<List
className={clsx('ui list', {
ordered: data.ordered,
bulleted: !data.ordered,
})}
>
<RenderListItems items={tocEntries} data={data} />
</List>
</>
);
};

interface DefaultToCProps {
data: BlocksFormData;
tocEntries: ToCEntry[];
}

export default View;
12 changes: 12 additions & 0 deletions packages/blocks/ToC/variations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import DefaultToCRenderer from './DefaultToCRenderer';

const ToCVariations = [
{
id: 'default',
title: 'Listing (default)',
view: DefaultToCRenderer,
isDefault: true,
},
];

export default ToCVariations;
2 changes: 2 additions & 0 deletions packages/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TitleBlockInfo from './Title';
import TextBlockInfo from './Text';
import ImageBlockInfo from './Image';
import TeaserBlockInfo from './Teaser';
import ToCBlockInfo from './ToC';

export default function install(config: ConfigType) {
config.settings.slate = slate;
Expand All @@ -17,6 +18,7 @@ export default function install(config: ConfigType) {
config.blocks.blocksConfig.slate = TextBlockInfo;
config.blocks.blocksConfig.image = ImageBlockInfo;
config.blocks.blocksConfig.teaser = TeaserBlockInfo;
config.blocks.blocksConfig.toc = ToCBlockInfo;

return config;
}
1 change: 1 addition & 0 deletions packages/blocks/news/7534.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added TOC Block with Default Renderer @Catherine358
3 changes: 2 additions & 1 deletion packages/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"@plone/components": "workspace:*",
"@plone/plate": "workspace:*",
"@plone/registry": "workspace:*",
"clsx": "^2.1.1"
"clsx": "^2.1.1",
"github-slugger": "^2.0.0"
},
"devDependencies": {
"@plone/helpers": "workspace:*",
Expand Down
Loading