Skip to content

Commit

Permalink
PLANET-4801 Only search DOM on frontend, useSelect in editor
Browse files Browse the repository at this point in the history
* Since we only need to watch for changes in the editor, we don't need
to make the code that queries the DOM update the block when the DOM
changes. Instead we can use `useSelect` to get the blocks and filter the
headers (and potentially other blocks that can output a heading).
* Decouple fetching the headers from creating a hierarchical
representation.
* Just fetch the headings in render of SubmenuFrontend. This component
doesn't get re-rendered anyway, and also the headings won't change
(unless other blocks still need to be rendered, we probably should check
that).
* Split functions into dedicated files.
* Make backtop into a component. This can be put anywhere on the page
since it is a fixed display element. Since there can only be one SubMenu
block, and there is no other way to add a backtop, we just add it there.
Though maybe we should decouple this and make this into a setting on the
page.
* Use plain old links instead of implementing our own behavior. Mainly
did this to remove (a lot of) complexity from the code while doing other
changes.  We should probably evaluate if we still want to do a transition,
though this can probably still make use of anchors.
  • Loading branch information
Inwerpsel committed Aug 14, 2020
1 parent b21b331 commit 446ec65
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 187 deletions.
18 changes: 18 additions & 0 deletions assets/src/blocks/Submenu/BackTop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import useScrollPosition from '@react-hook/window-scroll';

export const BackTop = ({}) => {
const cookies = document.getElementById('set-cookie');

const cookiesVisible = cookies && cookies.style.display !== 'none';

const scrollPosition = useScrollPosition(60);

return scrollPosition < 400 ? '' : <a
className={ 'back-top' }
style={ {
display: 'block',
bottom: cookiesVisible ? '120px' : '50px',
} }
onClick={ () => window.scrollTo(0, 0) }
/>;
};
69 changes: 62 additions & 7 deletions assets/src/blocks/Submenu/SubmenuEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Button, PanelBody } from '@wordpress/components';
import { SubmenuLevel } from './SubmenuLevel';
import { SubmenuItems } from './SubmenuItems';
import { InspectorControls } from '@wordpress/block-editor';
import { getSubmenuStyle } from './submenuFunctions';
import { useSubmenuItemsLoad } from './useSubmenuItemsLoad';
import { getSubmenuStyle } from './getSubmenuStyle';
import { makeHierarchical } from './getSubmenuItems';
import { generateAnchor } from './generateAnchor';
import { useSelect } from '@wordpress/data';

const { __ } = wp.i18n;
const { RichText } = wp.blockEditor;
Expand Down Expand Up @@ -69,11 +71,64 @@ const renderEdit = (attributes, setAttributes) => {
);
}

const renderView = (attributes, setAttributes) => {
// We can put the other blocks that can have a header inside in here along with the attribute containing the heading text.
// Then we can also filter those to include them in the menu.
const blockTypesWithHeadings = [
{name: 'planet4-blocks/articles', fieldName: 'article_heading', level: 2},
];

const { menuItems } = useSubmenuItemsLoad(attributes.levels, true);

const style = getSubmenuStyle(attributes.className, attributes.submenu_style);
const renderView = (attributes, setAttributes, className) => {

const { blocks } = useSelect(select => {
return ({ blocks: select('core/editor').getBlocks() });
});

const { levels: selectedLevels } = attributes

const flatHeaders = [];
blocks.forEach(block => {
if (block.name === 'core/heading') {
const blockLevel = block.attributes.level

const levelConfig = selectedLevels.find(selected => selected.heading === blockLevel);

if (!levelConfig) {
return;
}

const anchor = block.attributes.anchor || generateAnchor(block.attributes.content);

flatHeaders.push({
level: blockLevel,
content: block.attributes.content,
anchor,
style: levelConfig.style,
shouldLink: levelConfig.link,
});

return;
}

const blockType = blockTypesWithHeadings.find(({ name }) => name === block.name);

if (blockType) {
const { fieldName, level } = blockType;
const levelConfig = selectedLevels.find(selected => selected.heading === level);

if (!levelConfig) {
return;
}
flatHeaders.push({
level,
content: block.attributes[fieldName],
});
}
});

const menuItems = makeHierarchical(flatHeaders);

const style = getSubmenuStyle(className, attributes.submenu_style);

return (
<section className={`block submenu-block submenu-${style}`}>
Expand All @@ -87,7 +142,7 @@ const renderView = (attributes, setAttributes) => {
characterLimit={60}
multiline="false"
/>
{menuItems.length > 0 ?
{flatHeaders.length > 0 ?
<SubmenuItems menuItems={menuItems} /> :
<div className='EmptyMessage'>
{__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')}
Expand All @@ -100,6 +155,6 @@ const renderView = (attributes, setAttributes) => {
export const SubmenuEditor = ({ attributes, setAttributes, isSelected, className }) => (
<Fragment>
{isSelected && renderEdit(attributes, setAttributes)}
{renderView({ className, ...attributes }, setAttributes)}
{renderView(attributes, setAttributes, className)}
</Fragment>
);
22 changes: 11 additions & 11 deletions assets/src/blocks/Submenu/SubmenuFrontend.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Fragment, useEffect } from '@wordpress/element';
import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions';
import { getSubmenuStyle } from './getSubmenuStyle';
import { SubmenuItems } from './SubmenuItems';
import { useSubmenuItemsLoad } from './useSubmenuItemsLoad';
import { makeHierarchical } from './makeHierarchical';
import { getHeadingsFromDom } from './getHeadingsFromDom';
import { BackTop } from './BackTop';

export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => {

const { menuItems } = useSubmenuItemsLoad(levels, false);

useEffect(() => addSubmenuActions(menuItems), [menuItems]);

const headings = getHeadingsFromDom(levels);
const menuItems = makeHierarchical(headings);
const style = getSubmenuStyle(className, submenu_style);

return (
<section className={`block submenu-block submenu-${style}`}>
<h2>{title}</h2>
<SubmenuItems menuItems={menuItems} />
<section className={ `block submenu-block submenu-${ style }` }>
<h2>{ title }</h2>
<SubmenuItems menuItems={ menuItems }/>
<BackTop/>
</section>
);
}
};
25 changes: 4 additions & 21 deletions assets/src/blocks/Submenu/SubmenuItems.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
export const SubmenuItems = ({ menuItems }) => {

const onSubmenuLinkClick = id => {
const target = document.getElementById(id);
if (target) {
$('html, body').animate({
scrollTop: target.offsetTop - 100
}, 2000, () => {
const position = window.pageYOffset;
window.location.hash = id;
window.scrollTo(0, position);
});
}
}

const renderMenuItems = (items) => {
return items.map(({ text, style, link, id, children }) => (
<li key={text} className={`list-style-${style || 'none'} ${link ? "list-link" : "list-heading"}`}>
{link ?
return items.map(({ anchor, text, style, shouldLink, children }) => (
<li key={anchor} className={`list-style-${style || 'none'} ${shouldLink ? "list-link" : "list-heading"}`}>
{shouldLink ?
<a
href={`#${id}`}
className="icon-link submenu-link"
onClick={event => {
event.preventDefault();
onSubmenuLinkClick(id);
}}
href={`#${anchor}`}
>
{text}
</a>
Expand Down
2 changes: 2 additions & 0 deletions assets/src/blocks/Submenu/generateAnchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const generateAnchor = text => text.toLowerCase().trim().replace(/ /g, '-')

26 changes: 26 additions & 0 deletions assets/src/blocks/Submenu/getHeadingsFromDom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { generateAnchor } from './generateAnchor';

const getHeadingLevel = heading => Number(heading.tagName.replace('H', ''));

export const getHeadingsFromDom = (selectedLevels) => {
const container = document.querySelector('.page-template');
// Get all heading tags that we need to query
const headingsSelector = selectedLevels.map(level => `:not(.submenu-block) h${level.heading}`);

return [...container.querySelectorAll(headingsSelector)].map(heading=> {
const levelConfig = selectedLevels.find((selected) => selected.heading === getHeadingLevel(heading))

if (!heading.id) {
heading.id = generateAnchor(heading.textContent);
}

return ({
content: heading.textContent,
level: levelConfig.heading,
style: levelConfig.style,
shouldLink: levelConfig.link,
anchor: heading.id,
});
});
}

13 changes: 13 additions & 0 deletions assets/src/blocks/Submenu/getSubmenuStyle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Map for old attribute 'submenu_style'
const SUBMENU_STYLES = {
1: 'long',
2: 'short',
3: 'sidebar'
};

export const getSubmenuStyle = (className, submenu_style) => {
if (className && className.includes('is-style-')) {
return className.split('is-style-')[1];
}
return submenu_style ? SUBMENU_STYLES[submenu_style] : 'long';
};
33 changes: 33 additions & 0 deletions assets/src/blocks/Submenu/makeHierarchical.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const makeHierarchical = headings => {
let previousMenuItem;

return headings.reduce((menuItems, heading) => {
const { level, shouldLink, anchor, content, style } = heading;

// const parent = deeperThanPrevious ? previousHeading.children : menuItems;
let possibleParent = previousMenuItem || menuItems;

while (possibleParent.level && possibleParent.level >= level) {
possibleParent = possibleParent.parent;
}

const parent = possibleParent;

const container = parent === menuItems ? menuItems : parent.children;

const menuItem = {
text: content,
style: style,
children: [],
parent: parent,
level,
shouldLink,
anchor,
};
container.push(menuItem);

previousMenuItem = menuItem;

return menuItems;
}, []);
};
98 changes: 0 additions & 98 deletions assets/src/blocks/Submenu/submenuFunctions.js

This file was deleted.

Loading

0 comments on commit 446ec65

Please sign in to comment.