From af628e1f9f5d665bf30573b1d80ac0aef698f708 Mon Sep 17 00:00:00 2001 From: Maud Leray Date: Mon, 13 Jul 2020 10:35:38 +0200 Subject: [PATCH 01/20] PLANET-4811 Start WYSIWYG implementation for Submenu block PLANET-4811 Remove shortcode code for Submenu block PLANET-4811 Turn layout options into styles in the sidebar PLANET-4811 Add more destructuring to SubmenuEditor PLANET-4811 Remove more unnecessary Submenu code PLANET-4811 Add frontend implementation for Submenu WYSIWYG PLANET-4811 Rename and improve SubmenuLevel PLANET-4811 Add tooltips to Submenu style labels PLANET-4811 Retrieve submenu items PLANET-4811 Fix className for SubmenuEditor PLANET-4811 Add empty message PLANET-4811 Hide Submenu block in the frontend if no menu items PLANET-4811 Add back submenu_style attribute needed for existing blocks PLANET-4811 Get postId in SubmenuFrontend and use hooks in SubmenuEditor PLANET-4811 Add missing translations PLANET-4811 Add recursive function to get menu items PLANET-4811 Remove old conversion code PLANET-4811 Add proper links and back-to-top behavior PLANET-4811 Fix Submenu title color in the editor PLANET-4811 Fix deprecated attributes PLANET-4811 Add target links only when needed PLANET-4811 Fix Submenu rendering PLANET-4811 Simplify style registration PLANET-4811 Remove jQuery code PLANET-4811 Use wp.blockEditor instead of wp.editor to fix console warning PLANET-4811 Remove css float in the editor PLANET-4811 Create single file for all submenu functions PLANET-4811 Remove unused SubmenuIcon --- assets/src/blocks/Submenu/MenuLevel.js | 57 ------ assets/src/blocks/Submenu/Submenu.js | 118 ----------- assets/src/blocks/Submenu/SubmenuBlock.js | 191 +++++------------- assets/src/blocks/Submenu/SubmenuEditor.js | 106 ++++++++++ assets/src/blocks/Submenu/SubmenuFrontend.js | 132 ++++++++++++ assets/src/blocks/Submenu/SubmenuIcon.js | 7 - assets/src/blocks/Submenu/SubmenuLevel.js | 61 ++++++ assets/src/blocks/Submenu/submenuFunctions.js | 79 ++++++++ assets/src/frontendIndex.js | 2 + assets/src/styles/blocks/SubmenuEditor.scss | 27 ++- assets/src/styles/components/Preview.scss | 5 - classes/blocks/class-submenu.php | 116 +++-------- .../command/class-shortcode-to-gutenberg.php | 2 - .../class-shortcode-converter-factory.php | 2 - .../converters/class-submenu-converter.php | 63 ------ classes/rest/class-rest-api.php | 18 ++ public/js/submenu.js | 107 ---------- templates/blocks/submenu.twig | 37 ---- tests/unit/test-shortcode-converter.php | 61 ------ 19 files changed, 497 insertions(+), 694 deletions(-) delete mode 100644 assets/src/blocks/Submenu/MenuLevel.js delete mode 100644 assets/src/blocks/Submenu/Submenu.js create mode 100644 assets/src/blocks/Submenu/SubmenuEditor.js create mode 100644 assets/src/blocks/Submenu/SubmenuFrontend.js delete mode 100644 assets/src/blocks/Submenu/SubmenuIcon.js create mode 100644 assets/src/blocks/Submenu/SubmenuLevel.js create mode 100644 assets/src/blocks/Submenu/submenuFunctions.js delete mode 100644 classes/command/converters/class-submenu-converter.php delete mode 100644 public/js/submenu.js delete mode 100644 templates/blocks/submenu.twig diff --git a/assets/src/blocks/Submenu/MenuLevel.js b/assets/src/blocks/Submenu/MenuLevel.js deleted file mode 100644 index 9d5121d48..000000000 --- a/assets/src/blocks/Submenu/MenuLevel.js +++ /dev/null @@ -1,57 +0,0 @@ -import {Component} from '@wordpress/element'; -import { - CheckboxControl, - SelectControl, -} from '@wordpress/components'; - -export class MenuLevel extends Component { - constructor(props) { - super(props); - } - - render() { - return ( -
-
Level {this.props.index + 1}
- this.props.onHeadingChange(this.props.index, e)} - className='submenu-block-attribute-wrapper' - /> - - this.props.onLinkChange(this.props.index, e)} - className="submenu-level-link" - /> - - this.props.onStyleChange(this.props.index, e)} - className='submenu-block-attribute-wrapper' - /> -
- ); - }; -} diff --git a/assets/src/blocks/Submenu/Submenu.js b/assets/src/blocks/Submenu/Submenu.js deleted file mode 100644 index 3d53e4b1b..000000000 --- a/assets/src/blocks/Submenu/Submenu.js +++ /dev/null @@ -1,118 +0,0 @@ -import {Component, Fragment} from '@wordpress/element'; -import { - Button, - TextControl, - ServerSideRender -} from '@wordpress/components'; -import {LayoutSelector} from '../../components/LayoutSelector/LayoutSelector'; -import {Preview} from '../../components/Preview'; -import {MenuLevel} from "./MenuLevel"; - -export class Submenu extends Component { - constructor(props) { - super(props); - } - - renderEdit() { - const {__} = wp.i18n; - - return ( - -
-

{__('Anchor Link Submenu', 'p4ge')}

-

{__( - 'An in-page table of contents to help users have a sense of what\'s on the page and let them jump to a topic they are interested in.', - 'p4ge' - )}

-

{__('What style of menu do you need?', 'p4ge')}

- -
- No max items
recommended.') - }, { - label: __('Short full-width', 'p4ge'), - image: window.p4ge_vars.home + 'images/submenu-short.jpg', - value: 2, - help: __('Use: on long pages (more than 5 screens) when list items are short (up to 5 words)
No max items
recommended.'), - }, - { - label: __('Short sidebar', 'p4ge'), - image: window.p4ge_vars.home + 'images/submenu-sidebar.jpg', - value: 3, - help: __('Use: on long pages (more than 5 screens) when list items are short (up to 10 words)
Max items
recommended: 9') - }, - ]} - /> -
- -
- -
- -
- {this.props.levels.map((heading, i) => { - return ( - - ); - })} - -
- - -
-
- ); - } - - render() { - return ( - - { - this.props.isSelected - ? this.renderEdit() - : null - } - - - - - - ); - }; -} diff --git a/assets/src/blocks/Submenu/SubmenuBlock.js b/assets/src/blocks/Submenu/SubmenuBlock.js index 5e007e1e3..011790ebe 100644 --- a/assets/src/blocks/Submenu/SubmenuBlock.js +++ b/assets/src/blocks/Submenu/SubmenuBlock.js @@ -1,158 +1,71 @@ -import {Submenu} from './Submenu.js'; +import { SubmenuEditor } from './SubmenuEditor.js'; +import { Tooltip } from '@wordpress/components'; + +const { __ } = wp.i18n; + +const BLOCK_NAME = 'planet4-blocks/submenu'; + +const getStyleLabel = (label, help) => { + if (help) { + return ( + + {__(label, 'planet4-blocks-backend')} + + ); + } + return label; +} export class SubmenuBlock { constructor() { - const {registerBlockType} = wp.blocks; - const {withSelect} = wp.data; + const { registerBlockType } = wp.blocks; - registerBlockType('planet4-blocks/submenu', { + registerBlockType(BLOCK_NAME, { title: 'Submenu', icon: 'welcome-widgets-menus', category: 'planet4-blocks', - supports: { - multiple: false, // Use the block just once per post. - }, - /** - * Transforms old 'shortcake' shortcode to new gutenberg block. - * - * old block-shortcode: - * [shortcake_submenu submenu_style="3" title="title22" heading1="2" - * link1="true" style1="bullet" heading2="3" link2="true" style2="number" - * heading3="4" link3="false" - * /] - * - * new block-gutenberg: - * - * - */ - transforms: { - from: [ - { - type: 'shortcode', - // Shortcode tag can also be an array of shortcode aliases - // This `shortcode` definition will be used as a callback, - // it is a function which expects an object with at least - // a `named` key with `cover_type` property whose default value is 1. - // See: https://simonsmith.io/destructuring-objects-as-function-parameters-in-es6 - tag: 'shortcake_submenu', - attributes: { - submenu_style: { - type: 'integer', - shortcode: function (attributes) { - return Number(attributes.named.submenu_style); - } - }, - title: { - type: 'string', - shortcode: function (attributes) { - return attributes.named.title; - } - }, - levels: { - type: 'array', - shortcode: function (attributes) { - let levels = []; - if (attributes.named.heading1 > 0) { - let level = { - heading: Number(attributes.named.heading1), - link: Boolean(attributes.named.link1) || false, - style: attributes.named.style1 || 'none' - }; - levels.push(Object.assign({}, level)); - - if (attributes.named.heading2 > 0) { - let level = { - heading: Number(attributes.named.heading2), - link: Boolean(attributes.named.link2) || false, - style: attributes.named.style2 || 'none' - }; - levels.push(Object.assign({}, level)); - - if (attributes.named.heading3 > 0) { - let level = { - heading: Number(attributes.named.heading3), - link: Boolean(attributes.named.link3) || false, - style: attributes.named.style3 || 'none' - }; - levels.push(Object.assign({}, level)); - } - } - } - return levels; - }, - } - }, - }, - ] - }, attributes: { - submenu_style: { - type: 'integer', - default: 1 - }, title: { type: 'string', + default: '' + }, + submenu_style: { // Needed for old blocks conversion + type: 'integer', + default: 0 }, levels: { type: 'array', - default: [ {heading: 0, link: false, style: 'none'}] + default: [{ heading: 0, link: false, style: 'none' }] }, }, - edit: withSelect((select) => { - - })(({ - isSelected, - attributes, - setAttributes - }) => { - - function addLevel() { - setAttributes({levels: attributes.levels.concat({heading: 0, link: false, style: 'none'})}); - } - - function onTitleChange(value) { - setAttributes({title: value}); - } - - function onHeadingChange(index, value) { - let levels = JSON.parse(JSON.stringify(attributes.levels)); - levels[index].heading = Number(value); - setAttributes({levels: levels}); - } - - function onLayoutChange(value) { - setAttributes({submenu_style: Number(value)}); - } - - function onLinkChange(index, value) { - let levels = JSON.parse(JSON.stringify(attributes.levels)); - levels[index].link = value; - setAttributes({levels: levels}); - } - - function onStyleChange(index, value) { - let levels = JSON.parse(JSON.stringify(attributes.levels)); - levels[index].style = value; - setAttributes({levels: levels}); - } - - function removeLevel() { - setAttributes({levels: attributes.levels.slice(0, -1)}); + supports: { + multiple: false, // Use the block just once per post. + }, + styles: [ + { + name: 'long', + label: getStyleLabel( + 'Long full-width', + 'Use: on long pages (more than 5 screens) when list items are long (+ 10 words). No max items recommended.' + ), + isDefault: true + }, + { + name: 'short', + label: getStyleLabel( + 'Short full-width', + 'Use: on long pages (more than 5 screens) when list items are short (up to 5 words). No max items recommended.' + ) + }, + { + name: 'sidebar', + label: getStyleLabel( + 'Short sidebar', + 'Use: on long pages (more than 5 screens) when list items are short (up to 10 words). Max items recommended: 9' + ) } - - return - }), + ], + edit: SubmenuEditor, save() { return null; } diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js new file mode 100644 index 000000000..eca8f4314 --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -0,0 +1,106 @@ +import { Fragment } from '@wordpress/element'; +import { Button, PanelBody } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { SubmenuLevel } from './SubmenuLevel'; +import { SubmenuFrontend } from './SubmenuFrontend'; +import { InspectorControls } from '@wordpress/block-editor'; +import { getSubmenuStyle } from './submenuFunctions'; + +const { __ } = wp.i18n; +const { RichText } = wp.blockEditor; + +const renderEdit = (attributes, setAttributes) => { + function addLevel() { + setAttributes({ levels: attributes.levels.concat({ heading: 0, link: false, style: 'none' }) }); + } + + function onHeadingChange(index, value) { + let levels = JSON.parse(JSON.stringify(attributes.levels)); + levels[index].heading = Number(value); + setAttributes({ levels }); + } + + function onLinkChange(index, value) { + let levels = JSON.parse(JSON.stringify(attributes.levels)); + levels[index].link = value; + setAttributes({ levels }); + } + + function onStyleChange(index, value) { + let levels = JSON.parse(JSON.stringify(attributes.levels)); + levels[index].style = value; // Possible values: "none", "bullet", "number" + setAttributes({ levels }); + } + + function removeLevel() { + setAttributes({ levels: attributes.levels.slice(0, -1) }); + } + + return ( + + + + {attributes.levels.map((level, i) => ( + + ))} + + + + + + ); +} + +const renderView = (attributes, setAttributes) => { + + const { postId } = useSelect(select => ({ + postId: select('core/editor').getCurrentPostId() + }), []); + + const style = getSubmenuStyle(attributes.className, attributes.submenu_style); + + return ( + +
+ setAttributes({ title })} + keepPlaceholderOnFocus={true} + withoutInteractiveFormatting + characterLimit={60} + multiline="false" + /> + +
+
+ ); +} + +export const SubmenuEditor = ({ attributes, setAttributes, isSelected, className }) => ( + + {isSelected && renderEdit(attributes, setAttributes)} + {renderView({ className, ...attributes }, setAttributes)} + +); diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js new file mode 100644 index 000000000..eb1c6fb44 --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -0,0 +1,132 @@ +import { Component, Fragment } from '@wordpress/element'; +import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions'; +const { __ } = wp.i18n; +const { apiFetch } = wp; +const { addQueryArgs } = wp.url; + +export class SubmenuFrontend extends Component { + constructor(props) { + super(props); + this.state = { + menuItems: [] + } + + this.postId = null; + this.loadMenuItems = this.loadMenuItems.bind(this); + }; + + componentDidMount() { + // Set the post id and load the menu items + // If in the editor, the post id will be in the props + // Otherwise, we need to retrieve it from the body classnames + let postId = this.props.postId || null; + if (!postId) { + const bodyClassNames = [...document.body.classList]; + const idClassName = bodyClassNames.find(c => c.match(/page-id-/)); + postId = Number(idClassName.replace('page-id-', '')); + } + this.postId = postId; + this.loadMenuItems(postId); + } + + componentDidUpdate({ levels: prevLevels }) { + const { levels } = this.props; + if (JSON.stringify(levels) !== JSON.stringify(prevLevels)) { + this.loadMenuItems(this.postId); + } + } + + async loadMenuItems(postId) { + const { levels, isEditing } = this.props; + const queryArgs = { + path: addQueryArgs('/planet4/v1/get-submenu-items', { + levels, + post_id: postId + }) + }; + + try { + const menuItems = await apiFetch(queryArgs); + if (menuItems && menuItems.length > 0) { + this.setState({ menuItems }); + // This takes care of adding the "back to top" button, + // and also the submenu links behavior if needed + if (!isEditing) { + addSubmenuActions(menuItems); + } + } else { + this.setState({ menuItems: [] }); + } + } catch (e) { + console.log(e); + this.setState({ menuItems: [] }); + } + } + + onSubmenuLinkClick(hash, link) { + const target = document.querySelectorAll(`[data-hash-target='${hash}']`)[0]; + if (target) { + document.body.animate({ + scrollTop: target.offsetTop - 100 + }, 2000, () => { + const position = window.pageYOffset; + window.location.hash = link; + window.scrollTop(position); + }); + } + } + + getMenuItems(items) { + return items.map(({ text, style, link, id, hash, children }) => ( +
  • + {link ? + this.onSubmenuLinkClick(hash, `#${id}`)} + > + {text} + + : + {text} + } + {children && children.length > 0 && +
      + {this.getMenuItems(children)} +
    + } +
  • + )); + } + + render() { + const { title, className, isEditing, submenu_style } = this.props; + const { menuItems } = this.state; + + const style = getSubmenuStyle(className, submenu_style); + + return ( + + {(isEditing || menuItems.length > 0) && ( +
    + {title && !isEditing && +

    {title}

    + } + {menuItems.length > 0 && +
    +
      + {this.getMenuItems(menuItems)} +
    +
    + } + {isEditing && menuItems.length === 0 && +
    {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')}
    + } + {!isEditing &&  } +
    + )} +
    + ); + } +} diff --git a/assets/src/blocks/Submenu/SubmenuIcon.js b/assets/src/blocks/Submenu/SubmenuIcon.js deleted file mode 100644 index 76379341e..000000000 --- a/assets/src/blocks/Submenu/SubmenuIcon.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@wordpress/element'; - -export class SubmenuIcon extends Component { - render() { - return - } -} diff --git a/assets/src/blocks/Submenu/SubmenuLevel.js b/assets/src/blocks/Submenu/SubmenuLevel.js new file mode 100644 index 000000000..2ef37a49b --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuLevel.js @@ -0,0 +1,61 @@ +import { Component } from '@wordpress/element'; +import { + CheckboxControl, + SelectControl, +} from '@wordpress/components'; + +const { __ } = wp.i18n; + +export class SubmenuLevel extends Component { + render() { + const { + index, + heading, + onLinkChange, + link, + onHeadingChange, + style, + onStyleChange + } = this.props; + + return ( +
    +

    {`${__('Level', 'planet4-blocks')} ${Number(index + 1)}`}

    + onHeadingChange(index, e)} + /> + + onLinkChange(index, e)} + className="submenu-level-link" + /> + + onStyleChange(index, e)} + /> +
    +
    + ); + }; +} diff --git a/assets/src/blocks/Submenu/submenuFunctions.js b/assets/src/blocks/Submenu/submenuFunctions.js new file mode 100644 index 000000000..b2851f5cc --- /dev/null +++ b/assets/src/blocks/Submenu/submenuFunctions.js @@ -0,0 +1,79 @@ +// Map for old attribute 'submenu_style' +const SUBMENU_STYLES = { + 1: 'long', + 2: 'short', + 3: 'sidebar' +}; + +export const getSubmenuStyle = (className, submenu_style) => { + let style = submenu_style ? SUBMENU_STYLES[submenu_style] : 'long'; + if (className && className.includes('is-style-')) { + style = className.split('is-style-')[1]; + } + return style; +}; + +export const addSubmenuActions = submenu => { + if (submenu && Array.isArray(submenu)) { + for (let i = 0; i < submenu.length; i++) { + const menu = submenu[i]; + addTargetLinks(menu); + addChildrenLinks(menu); + } + + // Add "back to top" button behavior + const backtop = document.getElementsByClassName('back-top')[0]; + const submenuBlock = document.getElementsByClassName('submenu-block')[0]; + const cookiesBlock = document.getElementById('set-cookie'); + + if (submenuBlock) { + window.onscroll = () => { + if (window.pageYOffset > 400) { + backtop.style.display = 'block'; + if (cookiesBlock && cookiesBlock.style.display !== 'none') { + backtop.style.bottom = '120px'; + } else { + backtop.style.bottom = '50px'; + } + } else { + backtop.style.display = 'none'; + } + }; + } + } +}; + +/** + * Append html links for a submenu entry children. + * + * @param menu Submenu entry + */ +function addChildrenLinks(menu) { + if (menu.children && Array.isArray(menu.children)) { + for (let k = 0; k < menu.children.length; k++) { + const child = menu.children[k]; + addTargetLinks(child); + addChildrenLinks(child); + } + } +} + +/** + * Append html links the given item. + * + * @param item Submenu menu item + */ +function addTargetLinks(item) { + if (item.link) { + const headings = [...document.getElementsByTagName(item.type)]; + for (let l = 0; l < headings.length; l++) { + const heading = headings[l]; + if (heading.innerText.replace(/\u2010|\u2011|\u2013/, '') === item.text.replace('-', '')) { + let targetLink = document.createElement('a'); + targetLink.id = item.id; + targetLink.setAttribute('data-hash-target', item.hash); + heading.appendChild(targetLink); + } + } + } +} diff --git a/assets/src/frontendIndex.js b/assets/src/frontendIndex.js index e2f0a3ab6..d7797c549 100644 --- a/assets/src/frontendIndex.js +++ b/assets/src/frontendIndex.js @@ -6,6 +6,7 @@ import { SplittwocolumnsFrontend } from "./blocks/Splittwocolumns/Splittwocolumn import { HappypointFrontend } from './blocks/Happypoint/HappypointFrontend'; import { GalleryFrontend } from './blocks/Gallery/GalleryFrontend'; import { TimelineFrontend } from './blocks/Timeline/TimelineFrontend'; +import { SubmenuFrontend } from './blocks/Submenu/SubmenuFrontend'; const COMPONENTS = { 'planet4-blocks/spreadsheet': SpreadsheetFrontend, @@ -16,6 +17,7 @@ const COMPONENTS = { 'planet4-blocks/happypoint': HappypointFrontend, 'planet4-blocks/gallery': GalleryFrontend, 'planet4-blocks/timeline': TimelineFrontend, + 'planet4-blocks/submenu': SubmenuFrontend }; document.querySelectorAll( `[data-render]` ).forEach( diff --git a/assets/src/styles/blocks/SubmenuEditor.scss b/assets/src/styles/blocks/SubmenuEditor.scss index 2acf5a27f..8097ef0b8 100644 --- a/assets/src/styles/blocks/SubmenuEditor.scss +++ b/assets/src/styles/blocks/SubmenuEditor.scss @@ -1,17 +1,22 @@ -.submenu-block-attribute-wrapper { - display: inline-block; - margin-right: 20px; +.submenu-level-link label { + margin-bottom: 0; } -.submenu-level-link { - display: inline-block; - margin-right: 20px; +[data-type="planet4-blocks/submenu"] { + clear: both; +} - .components-base-control__field { - margin-bottom: 13px; +.editor-styles-wrapper { + .submenu-block h2 { + padding-left: 16px; + color: $grey-60; + font-family: $roboto; } -} -[data-type="planet4-blocks/submenu"] { - clear: both; + // Needed to make it easily selectable in the editor + .submenu-sidebar { + float: none; + margin-right: 0; + margin-left: auto; + } } diff --git a/assets/src/styles/components/Preview.scss b/assets/src/styles/components/Preview.scss index e3b90fa81..1be562624 100644 --- a/assets/src/styles/components/Preview.scss +++ b/assets/src/styles/components/Preview.scss @@ -5,11 +5,6 @@ padding: $space-sm; } - // Unset submenu sidebar float to make block editable in preview/edit. - .submenu-sidebar { - float: unset !important; - } - // Unset social media embed float to make block render within the preview component. .social-media-embed { float: unset !important; diff --git a/classes/blocks/class-submenu.php b/classes/blocks/class-submenu.php index 8d98fc44e..2cb9c7e6d 100644 --- a/classes/blocks/class-submenu.php +++ b/classes/blocks/class-submenu.php @@ -22,73 +22,28 @@ class Submenu extends Base_Block { /** @const string BLOCK_NAME */ const BLOCK_NAME = 'submenu'; - /** @const string EMPTY_MESSAGE */ - const EMPTY_MESSAGE = 'The submenu block produces no output on the editor.'; - - /** - * Register shortcake shortcode. - * - * @param array $attributes Shortcode attributes. - * @param string $content Content. - * - * @return mixed - */ - public function add_block_shortcode( $attributes, $content ) { - - $levels = []; - for ( $i = 1; $i <= 3; $i++ ) { - if ( ! empty( $attributes[ 'heading' . $i ] ) ) { - $level = [ - 'heading' => $attributes[ 'heading' . $i ] ?? '', - 'link' => $attributes[ 'link' . $i ] ?? '', - 'style' => $attributes[ 'style' . $i ] ?? '', - ]; - - array_push( $levels, $level ); - } - } - - $attributes['levels'] = $levels; - - $attributes = shortcode_atts( - [ - 'submenu_style' => '', - 'title' => '', - 'levels' => [ - [ - 'heading' => '', - 'link' => '', - 'style' => '', - ], - ], - ], - $attributes, - 'shortcake_submenu' - ); - - return $this->render( $attributes ); - } - /** * Submenu constructor. */ public function __construct() { - add_shortcode( 'shortcake_submenu', [ $this, 'add_block_shortcode' ] ); - register_block_type( 'planet4-blocks/submenu', [ 'editor_script' => 'planet4-blocks', - 'render_callback' => [ $this, 'render' ], + // todo: Remove when all content is migrated. + 'render_callback' => static function ( $attributes ) { + $json = wp_json_encode( [ 'attributes' => $attributes ] ); + return '
    '; + }, 'attributes' => [ - 'submenu_style' => [ - 'type' => 'integer', - 'default' => 1, - ], 'title' => [ 'type' => 'string', 'default' => '', ], + 'submenu_style' => [ // Needed for old blocks conversion. + 'type' => 'integer', + 'default' => 0, + ], /** * Levels is an array of objects. * Object structure: @@ -122,43 +77,34 @@ public function __construct() { } /** - * Get all the data that will be needed to render the block correctly. + * Required by the `Base_Block` class. + * + * @param array $fields Unused, required by the abstract function. + */ + public function prepare_data( $fields ): array { + return []; + } + + /** + * Get the menu items from the post data. * - * @param array $attributes This is the array of fields of this block. + * @param array $fields The fields entered in the editor. * - * @return array The data to be passed in the View. + * @return array The menu items to be passed in the View. */ - public function prepare_data( $attributes ): array { + public static function get_menu_items( $fields ): array { - // If request is coming from backend rendering. - if ( $this->is_rest_request() ) { - $post_id = filter_input( INPUT_GET, 'post_id', FILTER_VALIDATE_INT ); - if ( $post_id > 0 ) { - $post = get_post( $post_id ); - } - } else { - $post = get_queried_object(); + if ( isset( $fields['post_id'] ) ) { + $post = get_post( $fields['post_id'] ); } $menu = []; - if ( ! is_null( $post ) && isset( $attributes['levels'] ) ) { + if ( ! is_null( $post ) && isset( $fields['levels'] ) ) { $content = $post->post_content; - $menu = $this->parse_post_content( $content, $attributes['levels'] ); + $menu = self::parse_post_content( $content, $fields['levels'] ); } - // Enqueue js for the frontend. - if ( ! $this->is_rest_request() ) { - \P4GBKS\Loader::enqueue_local_script( 'submenu', 'public/js/submenu.js', [ 'jquery' ] ); - wp_localize_script( 'submenu', 'submenu', $menu ); - } - - $block_data = [ - 'title' => $attributes['title'] ?? '', - 'menu' => $menu, - 'style' => $attributes['submenu_style'] ?? '1', - ]; - - return $block_data; + return $menu; } /** @@ -180,7 +126,7 @@ private function parse_post_content( $content, $levels ) { $heading_meta = []; $index = 1; foreach ( $levels as $level ) { - $heading = $this->heading_attributes( $level ); + $heading = self::heading_attributes( $level ); if ( ! $heading ) { break; } @@ -200,7 +146,7 @@ private function parse_post_content( $content, $levels ) { $nodes = iterator_to_array( $node_list ); // process nodes array recursively to build menu. - return $this->build_menu( 1, $nodes, $heading_meta ); + return self::build_menu( 1, $nodes, $heading_meta ); } /** @@ -244,11 +190,11 @@ private function build_menu( $current_level, &$nodes, $heading_meta ) { // we're skipping over a heading level so create an empty node. $menu[] = new \stdClass(); } - $menu[ count( $menu ) - 1 ]->children = $this->build_menu( $current_level + 1, $nodes, $heading_meta ); + $menu[ count( $menu ) - 1 ]->children = self::build_menu( $current_level + 1, $nodes, $heading_meta ); } elseif ( $heading['level'] < $current_level ) { return $menu; } else { - $menu[] = $this->create_menu_item( $node->nodeValue, $heading['tag'], $heading['link'], $heading['style'] ); + $menu[] = self::create_menu_item( $node->nodeValue, $heading['tag'], $heading['link'], $heading['style'] ); // remove node from list only once it has been added to the menu. array_shift( $nodes ); diff --git a/classes/command/class-shortcode-to-gutenberg.php b/classes/command/class-shortcode-to-gutenberg.php index 804c3a1e2..a4c99fc7e 100644 --- a/classes/command/class-shortcode-to-gutenberg.php +++ b/classes/command/class-shortcode-to-gutenberg.php @@ -31,13 +31,11 @@ public function init() { $blocks = [ 'shortcake_columns', 'shortcake_carousel_header', - 'shortcake_counter', 'shortcake_enblock', 'shortcake_happy_point', 'shortcake_media_video', 'shortcake_newcovers', 'shortcake_social_media', - 'shortcake_submenu', 'shortcake_timeline', 'shortcake_take_action_boxout', ]; diff --git a/classes/command/converters/class-shortcode-converter-factory.php b/classes/command/converters/class-shortcode-converter-factory.php index b98c2d20f..874ca0afc 100644 --- a/classes/command/converters/class-shortcode-converter-factory.php +++ b/classes/command/converters/class-shortcode-converter-factory.php @@ -32,8 +32,6 @@ public static function get_converter( $shortcode_name, $attributes ) { return new Covers_Converter( $shortcode_name, $attributes ); case 'shortcake_social_media': return new SocialMedia_Converter( $shortcode_name, $attributes ); - case 'shortcake_submenu': - return new Submenu_Converter( $shortcode_name, $attributes ); case 'shortcake_take_action_boxout': return new TakeActionBoxout_Converter( $shortcode_name, $attributes ); } diff --git a/classes/command/converters/class-submenu-converter.php b/classes/command/converters/class-submenu-converter.php deleted file mode 100644 index 832c605f9..000000000 --- a/classes/command/converters/class-submenu-converter.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * @package P4GBKS - */ - -namespace P4GBKS\Command\Converters; - -/** - * Class for updating old shortcodes to Gutenberg blocks - */ -class Submenu_Converter extends Shortcode_Converter { - - /** - * @var null - */ - protected $aggregated_array = 'levels'; - - /** - * @var string - */ - protected $multiple_attrs_regex = '[1-4]'; - - /** - * @param string $old_name Shortcode's attribute key. - * - * @return mixed|string|string[]|null - */ - public function convert_attributes( $old_name ) { - $normalized_attribute_key = preg_replace( '/' . $this->multiple_attrs_regex . '/', '', $old_name ); - if ( array_key_exists( $normalized_attribute_key, $this->mapped_attributes ) ) { - $normalized_attribute_key = $this->mapped_attributes[ $normalized_attribute_key ]; - } - - return $normalized_attribute_key; - } - - /** - * Clears some obsolete attributes from shortcake block. - * - * If heading is not defined for one of the levels, remove the level. - * - * @param array $columns Gutenberg block's aggregated array. - * - * @return mixed - */ - protected function clear_aggregated_array( $columns ) { - return array_filter( - $columns, - function ( $column ) { - return property_exists( $column, 'heading' ) && $column->heading > 0; - } - ); - } -} diff --git a/classes/rest/class-rest-api.php b/classes/rest/class-rest-api.php index b3dbdd596..6a054ebe6 100644 --- a/classes/rest/class-rest-api.php +++ b/classes/rest/class-rest-api.php @@ -12,6 +12,7 @@ use P4GBKS\Blocks\SplitTwoColumns; use P4GBKS\Blocks\Happypoint; use P4GBKS\Blocks\Gallery; +use P4GBKS\Blocks\Submenu; /** * This class is just a place for add_endpoints to live. @@ -222,6 +223,23 @@ public static function endpoints(): void { ], ] ); + + /** + * Endpoint to get Submenu block items from a post data + */ + register_rest_route( + self::REST_NAMESPACE, + '/get-submenu-items', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => static function ( $fields ) { + $menu_items = Submenu::get_menu_items( $fields ); + return rest_ensure_response( $menu_items ); + }, + ], + ] + ); } /** diff --git a/public/js/submenu.js b/public/js/submenu.js deleted file mode 100644 index bd72ebfe0..000000000 --- a/public/js/submenu.js +++ /dev/null @@ -1,107 +0,0 @@ -/* global submenu */ - -$(document).ready(function () { - 'use strict'; - - // Parse submenu object passed to a variable from server-side. - if ('undefined' === submenu || ! Array.isArray(submenu)) { - submenu = []; // eslint-disable-line no-global-assign - } - - for (let i = 0; i < submenu.length; i++) { - let menu = submenu[i]; - - if ('undefined' === menu.id || 'undefined' === menu.type || 'undefined' === menu.link) { - continue; - } - let type = menu.type; - - // Iterate over headings and create an anchor tag for this heading. - if (menu.link) { - - let $headings = $('body ' + type); - - for (let j = 0; j < $headings.length; j++) { - let $heading = $($headings[j]); - const headingText = $heading.text().replace(/\u2010|\u2011|\u2013|-/g, '').trim(); - if (headingText === menu.text.replace(/-/g, '')) { - $heading.prepend(''); - } - } - } - - addChildrenLinks(menu); - } - - // Add click event for submenu links. - $('.submenu-link').click(function (event) { - event.preventDefault(); - const link = $.attr(this, 'href'); - let h = $(this).data('hash'); - let $target = $('*[data-hash-target="'+h+'"]'); - if ($target) { - $('html, body').animate({ - scrollTop: $target.offset().top - 100 - }, 2000, function () { - const position = $(window).scrollTop(); - window.location.hash = link; - $(window).scrollTop(position); - }); - } - - return false; - }); - - const $backtop = $( '.back-top' ); - const $submenu = $( '.submenu-block' ); - - if ( $submenu.length > 0 ) { - $( window ).scroll( function () { - if ( $( this ).scrollTop() > 400 ) { - $backtop.fadeIn(); - if ( $( '.cookie-block:visible' ).length > 0 ) { - $backtop.css( 'bottom', '120px' ); - } else { - $backtop.css( 'bottom', '50px' ); - } - } else { - $backtop.fadeOut(); - } - } ); - - $backtop.click( function () { - $( 'body, html' ).animate( { - scrollTop: 0 - }, 800 ); - return false; - } ); - } - - /** - * Append html links for a submenu entry children. - * - * @param menu Submenu entry - */ - function addChildrenLinks(menu) { - if ('undefined' === menu.children || !Array.isArray(menu.children)) { - return; - } - - for (let k = 0; k < menu.children.length; k++) { - let child = menu.children[k]; - let child_type = child.type; - let $headings = $('body ' + child_type); - - addChildrenLinks(child); - - for (let l = 0; l < $headings.length; l++) { - - let $heading = $($headings[l]); - if ($heading.text().replace(/\u2010|\u2011|\u2013/, '') === child.text.replace('-', '')) { - $heading.prepend(''); - break; - } - } - } - } -}); diff --git a/templates/blocks/submenu.twig b/templates/blocks/submenu.twig deleted file mode 100644 index b7149f3e6..000000000 --- a/templates/blocks/submenu.twig +++ /dev/null @@ -1,37 +0,0 @@ -{% block submenu %} - - {% if ( menu ) %} - - -   - {% endif %} - -{% endblock %} - -{% macro show_menu(items, class) %} - -{% endmacro %} diff --git a/tests/unit/test-shortcode-converter.php b/tests/unit/test-shortcode-converter.php index 72df31e19..a37e08577 100644 --- a/tests/unit/test-shortcode-converter.php +++ b/tests/unit/test-shortcode-converter.php @@ -36,7 +36,6 @@ public function setUp() { * @dataProvider covers_shortcodes_provider * @dataProvider media_shortcodes_provider * @dataProvider social_media_shortcodes_provider - * @dataProvider submenu_shortcodes_provider * @dataProvider timeline_shortcodes_provider * @dataProvider take_action_boxout_shortcodes_provider */ @@ -422,66 +421,6 @@ public function social_media_shortcodes_provider(): array { ]; } - /** - * Planet4 blocks shortocodes provider. - * - * @return array - */ - public function submenu_shortcodes_provider(): array { - return [ - // 1-5 - - 'submenu with title and language' => - [ - '[shortcake_submenu submenu_style="1" title="On this page" heading1="2" link1="true" heading2="3" link2="true" /]', - - '', - ], - - 'submenu 1 level with invalid 2nd and 3rd link attribute' => - [ - '[shortcake_submenu submenu_style="1" title="Submenu - Full-width style" heading1="2" link1="true" link2="false" link3="false" /]', - - '', - ], - - 'submenu 3 levels' => - [ - '[shortcake_submenu submenu_style="1" title="This is the Submenu\'s title" heading1="1" link1="true" heading2="2" link2="true" heading3="3" link3="true" /]', - - '', - ], - - 'submenu 2 levels' => - [ - '[shortcake_submenu submenu_style="3" heading1="3" link1="true" heading2="4" link2="true" /]', - - '', - ], - - 'submenu 1 level with style' => - [ - '[shortcake_submenu submenu_style="2" heading1="4" link1="true" style1="bullet" link2="false" link3="false" /]', - - '', - ], - - 'submenu invalid 2nd level' => - [ - '[shortcake_submenu submenu_style="3" title="Submenu - sidebar style" heading1="2" link1="true" heading2="0" link2="false" link3="false" /]', - - '', - ], - - 'submenu invalid 2nd level invalid heading' => - [ - '[shortcake_submenu submenu_style="3" heading1="2" link1="true" heading2="0" /]', - - '', - ], - ]; - } - /** * Planet4 blocks shortocodes provider. * From ce880194bf35959e97239fce6592d0978eff3d0a Mon Sep 17 00:00:00 2001 From: Maud Leray Date: Thu, 6 Aug 2020 20:18:07 +0200 Subject: [PATCH 02/20] PLANET-4811 Improve code after review PLANET-4811 Remove endpoint and retrieve content in the frontend instead PLANET-4811 Remove md5 PLANET-4811 Remove now unused postId prop PLANET-4811 Add children behaviour to loadMenuItems function --- assets/src/blocks/Submenu/SubmenuEditor.js | 7 +- assets/src/blocks/Submenu/SubmenuFrontend.js | 54 ++----- assets/src/blocks/Submenu/submenuFunctions.js | 81 +++++++--- classes/blocks/class-submenu.php | 142 ------------------ classes/rest/class-rest-api.php | 18 --- 5 files changed, 81 insertions(+), 221 deletions(-) diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index eca8f4314..c5f5d2487 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -1,6 +1,5 @@ import { Fragment } from '@wordpress/element'; import { Button, PanelBody } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; import { SubmenuLevel } from './SubmenuLevel'; import { SubmenuFrontend } from './SubmenuFrontend'; import { InspectorControls } from '@wordpress/block-editor'; @@ -73,10 +72,6 @@ const renderEdit = (attributes, setAttributes) => { const renderView = (attributes, setAttributes) => { - const { postId } = useSelect(select => ({ - postId: select('core/editor').getCurrentPostId() - }), []); - const style = getSubmenuStyle(attributes.className, attributes.submenu_style); return ( @@ -92,7 +87,7 @@ const renderView = (attributes, setAttributes) => { characterLimit={60} multiline="false" /> - + ); diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index eb1c6fb44..a0e476153 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -1,8 +1,6 @@ import { Component, Fragment } from '@wordpress/element'; -import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions'; +import { getSubmenuStyle, addSubmenuActions, loadMenuItems } from './submenuFunctions'; const { __ } = wp.i18n; -const { apiFetch } = wp; -const { addQueryArgs } = wp.url; export class SubmenuFrontend extends Component { constructor(props) { @@ -11,80 +9,60 @@ export class SubmenuFrontend extends Component { menuItems: [] } - this.postId = null; this.loadMenuItems = this.loadMenuItems.bind(this); }; componentDidMount() { - // Set the post id and load the menu items - // If in the editor, the post id will be in the props - // Otherwise, we need to retrieve it from the body classnames - let postId = this.props.postId || null; - if (!postId) { - const bodyClassNames = [...document.body.classList]; - const idClassName = bodyClassNames.find(c => c.match(/page-id-/)); - postId = Number(idClassName.replace('page-id-', '')); - } - this.postId = postId; - this.loadMenuItems(postId); + // We need to add a small timeout for the editor, + // otherwise the RichText elements appear empty + setTimeout(this.loadMenuItems, 100); } componentDidUpdate({ levels: prevLevels }) { const { levels } = this.props; if (JSON.stringify(levels) !== JSON.stringify(prevLevels)) { - this.loadMenuItems(this.postId); + this.loadMenuItems(); } } - async loadMenuItems(postId) { + loadMenuItems() { const { levels, isEditing } = this.props; - const queryArgs = { - path: addQueryArgs('/planet4/v1/get-submenu-items', { - levels, - post_id: postId - }) - }; - - try { - const menuItems = await apiFetch(queryArgs); - if (menuItems && menuItems.length > 0) { - this.setState({ menuItems }); + const menuItems = loadMenuItems(levels, isEditing); + if (menuItems && menuItems.length > 0) { + this.setState({ menuItems }, () => { // This takes care of adding the "back to top" button, - // and also the submenu links behavior if needed + // and also the submenu links behaviour if needed if (!isEditing) { addSubmenuActions(menuItems); } - } else { - this.setState({ menuItems: [] }); - } - } catch (e) { - console.log(e); + }); + } else { this.setState({ menuItems: [] }); } } - onSubmenuLinkClick(hash, link) { + onSubmenuLinkClick(id, hash) { const target = document.querySelectorAll(`[data-hash-target='${hash}']`)[0]; if (target) { document.body.animate({ scrollTop: target.offsetTop - 100 }, 2000, () => { const position = window.pageYOffset; - window.location.hash = link; + window.location.hash = `#${id}`; window.scrollTop(position); }); } } getMenuItems(items) { - return items.map(({ text, style, link, id, hash, children }) => ( + return items.map(({ text, style, link, id, children, hash }) => (
  • {link ? this.onSubmenuLinkClick(hash, `#${id}`)} + onClick={() => this.onSubmenuLinkClick(id, hash)} > {text} diff --git a/assets/src/blocks/Submenu/submenuFunctions.js b/assets/src/blocks/Submenu/submenuFunctions.js index b2851f5cc..0e838239e 100644 --- a/assets/src/blocks/Submenu/submenuFunctions.js +++ b/assets/src/blocks/Submenu/submenuFunctions.js @@ -6,11 +6,10 @@ const SUBMENU_STYLES = { }; export const getSubmenuStyle = (className, submenu_style) => { - let style = submenu_style ? SUBMENU_STYLES[submenu_style] : 'long'; if (className && className.includes('is-style-')) { - style = className.split('is-style-')[1]; + return className.split('is-style-')[1]; } - return style; + return submenu_style ? SUBMENU_STYLES[submenu_style] : 'long'; }; export const addSubmenuActions = submenu => { @@ -21,7 +20,7 @@ export const addSubmenuActions = submenu => { addChildrenLinks(menu); } - // Add "back to top" button behavior + // Add "back to top" button behaviour const backtop = document.getElementsByClassName('back-top')[0]; const submenuBlock = document.getElementsByClassName('submenu-block')[0]; const cookiesBlock = document.getElementById('set-cookie'); @@ -48,7 +47,7 @@ export const addSubmenuActions = submenu => { * * @param menu Submenu entry */ -function addChildrenLinks(menu) { +const addChildrenLinks = menu => { if (menu.children && Array.isArray(menu.children)) { for (let k = 0; k < menu.children.length; k++) { const child = menu.children[k]; @@ -56,24 +55,72 @@ function addChildrenLinks(menu) { addChildrenLinks(child); } } -} +}; /** * Append html links the given item. * * @param item Submenu menu item */ -function addTargetLinks(item) { +const addTargetLinks = item => { if (item.link) { - const headings = [...document.getElementsByTagName(item.type)]; - for (let l = 0; l < headings.length; l++) { - const heading = headings[l]; - if (heading.innerText.replace(/\u2010|\u2011|\u2013/, '') === item.text.replace('-', '')) { - let targetLink = document.createElement('a'); - targetLink.id = item.id; - targetLink.setAttribute('data-hash-target', item.hash); - heading.appendChild(targetLink); - } + const headings = getTags(item.type); + if (headings) { + headings.forEach(heading => { + if (heading.textContent === item.text) { + let targetLink = document.createElement('a'); + targetLink.id = item.id; + targetLink.setAttribute('data-hash-target', item.hash); + heading.appendChild(targetLink); + } + }); } } -} +}; + +export const loadMenuItems = (levels, isEditing) => { + const menuItems = []; + // Get all heading tags that we need to query + const headings = levels.map(level => `h${level.heading}`); + const tagElements = getTags(headings, isEditing ? 'editor-styles-wrapper' : 'page-template'); + if (tagElements) { + tagElements.forEach((tagElement, tagIndex) => { + const headingNumber = getTagElementHeadingNumber(tagElement); + let previousHeadingNumber = 0; + if (tagIndex > 0) { + previousHeadingNumber = getTagElementHeadingNumber(tagElements[tagIndex - 1]); + } + // Get the properties that we need to create the new menu item + const correspondingLevel = levels.find(level => level.heading === headingNumber); + const id = tagElement.textContent.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); // equivalent of WP sanitize_title function + const menuItem = { + text: tagElement.textContent, + id, + style: correspondingLevel.style, + link: correspondingLevel.link, + type: `h${correspondingLevel.heading}`, + hash: `${id}-h${correspondingLevel.heading}-${tagIndex}`, + children: [] + }; + if (previousHeadingNumber && headingNumber > previousHeadingNumber) { + // In this case we need to add this menuItem to the children of the previous one + menuItems[menuItems.length - 1].children.push(menuItem); + } else { + menuItems.push(menuItem); + } + }); + } + return menuItems; +}; + +const getTags = (headings, className = 'page-template') => { + // We need to make sure it's a div element, + // since for 'page-template' className we have it on the body too + const page = [...document.getElementsByClassName(className)].find(element => element.tagName === 'DIV'); + if (page) { + return [...page.querySelectorAll(headings)]; + } + return null; +}; + +const getTagElementHeadingNumber = tagElement => Number(tagElement.tagName.replace('H', '')); diff --git a/classes/blocks/class-submenu.php b/classes/blocks/class-submenu.php index 2cb9c7e6d..e7fa1cccf 100644 --- a/classes/blocks/class-submenu.php +++ b/classes/blocks/class-submenu.php @@ -84,147 +84,5 @@ public function __construct() { public function prepare_data( $fields ): array { return []; } - - /** - * Get the menu items from the post data. - * - * @param array $fields The fields entered in the editor. - * - * @return array The menu items to be passed in the View. - */ - public static function get_menu_items( $fields ): array { - - if ( isset( $fields['post_id'] ) ) { - $post = get_post( $fields['post_id'] ); - } - - $menu = []; - if ( ! is_null( $post ) && isset( $fields['levels'] ) ) { - $content = $post->post_content; - $menu = self::parse_post_content( $content, $fields['levels'] ); - } - - return $menu; - } - - /** - * Parse post's content to extract headings and build menu - * - * @param string $content Post content. - * @param array $levels Submenu block attributes. - * - * @return array - */ - private function parse_post_content( $content, $levels ) { - - // Validate, if $content is empty. - if ( ! $content || is_null( $levels ) ) { - return []; - } - - // make array of heading level metadata keyed by tag name. - $heading_meta = []; - $index = 1; - foreach ( $levels as $level ) { - $heading = self::heading_attributes( $level ); - if ( ! $heading ) { - break; - } - $heading['level'] = $index++; - $heading_meta[ $heading['tag'] ] = $heading; - } - - $dom = new DOMDocument(); - libxml_use_internal_errors( true ); - - $dom->loadHtml( $content ); - $xpath = new DOMXPath( $dom ); - - // get all the headings as an array of nodes. - $xpath_expression = '//' . join( ' | //', array_keys( $heading_meta ) ); - $node_list = $xpath->query( $xpath_expression ); - $nodes = iterator_to_array( $node_list ); - - // process nodes array recursively to build menu. - return self::build_menu( 1, $nodes, $heading_meta ); - } - - /** - * Extract shortcode attributes for given heading level. - * - * @param array $level Block level attributes. - * - * @return array|null associative array or null if menu level is not configured - */ - private function heading_attributes( $level ) { - return empty( $level ) - ? null - : [ - 'heading' => $level['heading'], - 'tag' => 'h' . $level['heading'], - 'link' => $level['link'] ?? false, - 'style' => $level['style'] ?? 'none', - ]; - } - - /** - * Process flat array of DOM nodes to build up menu tree structure. - * - * @param int $current_level Current menu nesting level. - * @param \DOMNode[] $nodes Array of heading DOM nodes, passed by reference. - * @param array $heading_meta Metadata about each heading tag. - * - * @return array menu tree structure - */ - private function build_menu( $current_level, &$nodes, $heading_meta ) { - $menu = []; - - // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found - while ( count( $nodes ) ) { - // consider first node in the list but don't remove it yet. - $node = $nodes[0]; - - $heading = $heading_meta[ $node->nodeName ]; - if ( $heading['level'] > $current_level ) { - if ( count( $menu ) === 0 ) { - // we're skipping over a heading level so create an empty node. - $menu[] = new \stdClass(); - } - $menu[ count( $menu ) - 1 ]->children = self::build_menu( $current_level + 1, $nodes, $heading_meta ); - } elseif ( $heading['level'] < $current_level ) { - return $menu; - } else { - $menu[] = self::create_menu_item( $node->nodeValue, $heading['tag'], $heading['link'], $heading['style'] ); - - // remove node from list only once it has been added to the menu. - array_shift( $nodes ); - } - } - - return $menu; - } - - /** - * Create a std object representing a node/heading. - * - * @param string $text Heading/menu item text. - * @param string $type Type/name of the tag. - * @param bool|string $link True if this menu item should link to the heading. - * @param string $style List style for menu item. - * - * @return \stdClass - */ - private function create_menu_item( $text, $type, $link, $style ) { - $menu_obj = new \stdClass(); - $menu_obj->text = utf8_decode( $text ); - $menu_obj->hash = md5( $text ); - $menu_obj->type = $type; - $menu_obj->style = $style; - $menu_obj->link = filter_var( $link, FILTER_VALIDATE_BOOLEAN ); - $menu_obj->id = sanitize_title( utf8_decode( $text ) ); - $menu_obj->children = []; - - return $menu_obj; - } } diff --git a/classes/rest/class-rest-api.php b/classes/rest/class-rest-api.php index 6a054ebe6..b3dbdd596 100644 --- a/classes/rest/class-rest-api.php +++ b/classes/rest/class-rest-api.php @@ -12,7 +12,6 @@ use P4GBKS\Blocks\SplitTwoColumns; use P4GBKS\Blocks\Happypoint; use P4GBKS\Blocks\Gallery; -use P4GBKS\Blocks\Submenu; /** * This class is just a place for add_endpoints to live. @@ -223,23 +222,6 @@ public static function endpoints(): void { ], ] ); - - /** - * Endpoint to get Submenu block items from a post data - */ - register_rest_route( - self::REST_NAMESPACE, - '/get-submenu-items', - [ - [ - 'methods' => WP_REST_Server::READABLE, - 'callback' => static function ( $fields ) { - $menu_items = Submenu::get_menu_items( $fields ); - return rest_ensure_response( $menu_items ); - }, - ], - ] - ); } /** From 5b1bec882d4f5615bd45e12c31b7acbc9158b8c3 Mon Sep 17 00:00:00 2001 From: Maud Leray Date: Fri, 7 Aug 2020 10:50:21 +0200 Subject: [PATCH 03/20] PLANET-4811 Improve code after review PLANET-4811 Create SubmenuItems component and useSubmenuItemsLoad hook PLANET-4811 Fix back to top button positioning PLANET-4811 Make sure ids are unique PLANET-4811 Fix scrolling animation --- assets/src/blocks/Submenu/SubmenuEditor.js | 94 +++++++------- assets/src/blocks/Submenu/SubmenuFrontend.js | 118 +++--------------- assets/src/blocks/Submenu/SubmenuItems.js | 49 ++++++++ assets/src/blocks/Submenu/submenuFunctions.js | 114 +++++++---------- .../src/blocks/Submenu/useSubmenuItemsLoad.js | 50 ++++++++ 5 files changed, 205 insertions(+), 220 deletions(-) create mode 100644 assets/src/blocks/Submenu/SubmenuItems.js create mode 100644 assets/src/blocks/Submenu/useSubmenuItemsLoad.js diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index c5f5d2487..6a204802c 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -1,9 +1,10 @@ import { Fragment } from '@wordpress/element'; import { Button, PanelBody } from '@wordpress/components'; import { SubmenuLevel } from './SubmenuLevel'; -import { SubmenuFrontend } from './SubmenuFrontend'; +import { SubmenuItems } from './SubmenuItems'; import { InspectorControls } from '@wordpress/block-editor'; import { getSubmenuStyle } from './submenuFunctions'; +import { useSubmenuItemsLoad } from './useSubmenuItemsLoad'; const { __ } = wp.i18n; const { RichText } = wp.blockEditor; @@ -36,60 +37,63 @@ const renderEdit = (attributes, setAttributes) => { } return ( - - - - {attributes.levels.map((level, i) => ( - - ))} - - - - - + + ); } const renderView = (attributes, setAttributes) => { + const { menuItems } = useSubmenuItemsLoad(attributes.levels, true); + const style = getSubmenuStyle(attributes.className, attributes.submenu_style); return ( - -
    - setAttributes({ title })} - keepPlaceholderOnFocus={true} - withoutInteractiveFormatting - characterLimit={60} - multiline="false" - /> - -
    -
    +
    + setAttributes({ title })} + keepPlaceholderOnFocus={true} + withoutInteractiveFormatting + characterLimit={60} + multiline="false" + /> + {menuItems.length > 0 ? + : +
    + {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')} +
    + } +
    ); } diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index a0e476153..8b56e6b92 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -1,110 +1,20 @@ -import { Component, Fragment } from '@wordpress/element'; -import { getSubmenuStyle, addSubmenuActions, loadMenuItems } from './submenuFunctions'; -const { __ } = wp.i18n; +import { Fragment, useEffect } from '@wordpress/element'; +import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions'; +import { SubmenuItems } from './SubmenuItems'; +import { useSubmenuItemsLoad } from './useSubmenuItemsLoad'; -export class SubmenuFrontend extends Component { - constructor(props) { - super(props); - this.state = { - menuItems: [] - } +export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => { - this.loadMenuItems = this.loadMenuItems.bind(this); - }; + const { menuItems } = useSubmenuItemsLoad(levels, false); - componentDidMount() { - // We need to add a small timeout for the editor, - // otherwise the RichText elements appear empty - setTimeout(this.loadMenuItems, 100); - } + useEffect(() => addSubmenuActions(menuItems), [menuItems]); - componentDidUpdate({ levels: prevLevels }) { - const { levels } = this.props; - if (JSON.stringify(levels) !== JSON.stringify(prevLevels)) { - this.loadMenuItems(); - } - } + const style = getSubmenuStyle(className, submenu_style); - loadMenuItems() { - const { levels, isEditing } = this.props; - const menuItems = loadMenuItems(levels, isEditing); - if (menuItems && menuItems.length > 0) { - this.setState({ menuItems }, () => { - // This takes care of adding the "back to top" button, - // and also the submenu links behaviour if needed - if (!isEditing) { - addSubmenuActions(menuItems); - } - }); - } else { - this.setState({ menuItems: [] }); - } - } - - onSubmenuLinkClick(id, hash) { - const target = document.querySelectorAll(`[data-hash-target='${hash}']`)[0]; - if (target) { - document.body.animate({ - scrollTop: target.offsetTop - 100 - }, 2000, () => { - const position = window.pageYOffset; - window.location.hash = `#${id}`; - window.scrollTop(position); - }); - } - } - - getMenuItems(items) { - return items.map(({ text, style, link, id, children, hash }) => ( -
  • - {link ? - this.onSubmenuLinkClick(id, hash)} - > - {text} - - : - {text} - } - {children && children.length > 0 && -
      - {this.getMenuItems(children)} -
    - } -
  • - )); - } - - render() { - const { title, className, isEditing, submenu_style } = this.props; - const { menuItems } = this.state; - - const style = getSubmenuStyle(className, submenu_style); - - return ( - - {(isEditing || menuItems.length > 0) && ( -
    - {title && !isEditing && -

    {title}

    - } - {menuItems.length > 0 && -
    -
      - {this.getMenuItems(menuItems)} -
    -
    - } - {isEditing && menuItems.length === 0 && -
    {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')}
    - } - {!isEditing &&  } -
    - )} -
    - ); - } + return ( +
    +

    {title}

    + +
    + ); } diff --git a/assets/src/blocks/Submenu/SubmenuItems.js b/assets/src/blocks/Submenu/SubmenuItems.js new file mode 100644 index 000000000..e375ba08a --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuItems.js @@ -0,0 +1,49 @@ +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 }) => ( +
  • + {link ? + { + event.preventDefault(); + onSubmenuLinkClick(id); + }} + > + {text} + + : + {text} + } + {children && children.length > 0 && +
      + {renderMenuItems(children)} +
    + } +
  • + )); + } + + return menuItems.length > 0 && ( +
    +
      + {renderMenuItems(menuItems)} +
    +
    + ); +} diff --git a/assets/src/blocks/Submenu/submenuFunctions.js b/assets/src/blocks/Submenu/submenuFunctions.js index 0e838239e..51a3a9dc5 100644 --- a/assets/src/blocks/Submenu/submenuFunctions.js +++ b/assets/src/blocks/Submenu/submenuFunctions.js @@ -16,111 +16,83 @@ export const addSubmenuActions = submenu => { if (submenu && Array.isArray(submenu)) { for (let i = 0; i < submenu.length; i++) { const menu = submenu[i]; - addTargetLinks(menu); - addChildrenLinks(menu); + addTargetIds(menu); + formatChildren(menu); } // Add "back to top" button behaviour - const backtop = document.getElementsByClassName('back-top')[0]; - const submenuBlock = document.getElementsByClassName('submenu-block')[0]; - const cookiesBlock = document.getElementById('set-cookie'); + let backtop = document.querySelector('a.back-top'); + const submenuBlock = document.querySelector('section.submenu-block'); if (submenuBlock) { - window.onscroll = () => { - if (window.pageYOffset > 400) { - backtop.style.display = 'block'; - if (cookiesBlock && cookiesBlock.style.display !== 'none') { - backtop.style.bottom = '120px'; - } else { - backtop.style.bottom = '50px'; - } + // If back to top button doesn't exist yet, we need to create it + if (!backtop) { + backtop = document.createElement('a'); + backtop.href = '#'; + backtop.className = 'back-top'; + document.body.appendChild(backtop); + } + addBackToTopBehaviour(backtop); + } + } +}; + +// Add onscroll function and proper positioning for back to top behaviour +const addBackToTopBehaviour = backtop => { + const cookies = document.getElementById('set-cookie'); + window.onscroll = () => { + if (window.pageYOffset > 400 && backtop.style.display !== 'block') { + backtop.style.display = 'block'; + if (cookies) { + const cookiesStyles = window.getComputedStyle(cookies); + if (cookiesStyles && cookiesStyles.display !== 'none') { + backtop.style.bottom = '120px'; } else { - backtop.style.display = 'none'; + backtop.style.bottom = '50px'; } - }; + } + } else if (window.pageYOffset <= 400 && backtop.style.display !== 'none') { + backtop.style.display = 'none'; } - } + }; }; /** - * Append html links for a submenu entry children. + * Format submenu entry children. * * @param menu Submenu entry */ -const addChildrenLinks = menu => { +const formatChildren = menu => { if (menu.children && Array.isArray(menu.children)) { for (let k = 0; k < menu.children.length; k++) { const child = menu.children[k]; - addTargetLinks(child); - addChildrenLinks(child); + addTargetIds(child); + formatChildren(child); } } }; /** - * Append html links the given item. + * Add ids to the items, to be able to scroll to them. * * @param item Submenu menu item */ -const addTargetLinks = item => { +const addTargetIds = item => { if (item.link) { - const headings = getTags(item.type); + const headings = getHeadings(item.type); if (headings) { headings.forEach(heading => { - if (heading.textContent === item.text) { - let targetLink = document.createElement('a'); - targetLink.id = item.id; - targetLink.setAttribute('data-hash-target', item.hash); - heading.appendChild(targetLink); + if (heading.textContent === item.text && !heading.id) { + heading.id = item.id; } }); } } }; -export const loadMenuItems = (levels, isEditing) => { - const menuItems = []; - // Get all heading tags that we need to query - const headings = levels.map(level => `h${level.heading}`); - const tagElements = getTags(headings, isEditing ? 'editor-styles-wrapper' : 'page-template'); - if (tagElements) { - tagElements.forEach((tagElement, tagIndex) => { - const headingNumber = getTagElementHeadingNumber(tagElement); - let previousHeadingNumber = 0; - if (tagIndex > 0) { - previousHeadingNumber = getTagElementHeadingNumber(tagElements[tagIndex - 1]); - } - // Get the properties that we need to create the new menu item - const correspondingLevel = levels.find(level => level.heading === headingNumber); - const id = tagElement.textContent.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); // equivalent of WP sanitize_title function - const menuItem = { - text: tagElement.textContent, - id, - style: correspondingLevel.style, - link: correspondingLevel.link, - type: `h${correspondingLevel.heading}`, - hash: `${id}-h${correspondingLevel.heading}-${tagIndex}`, - children: [] - }; - if (previousHeadingNumber && headingNumber > previousHeadingNumber) { - // In this case we need to add this menuItem to the children of the previous one - menuItems[menuItems.length - 1].children.push(menuItem); - } else { - menuItems.push(menuItem); - } - }); - } - return menuItems; -}; - -const getTags = (headings, className = 'page-template') => { +export const getHeadings = (headings, className = 'page-template') => { // We need to make sure it's a div element, // since for 'page-template' className we have it on the body too - const page = [...document.getElementsByClassName(className)].find(element => element.tagName === 'DIV'); - if (page) { - return [...page.querySelectorAll(headings)]; - } - return null; + const page = document.querySelector(`div.${className}`); + return page ? [...page.querySelectorAll(headings)] : null; }; - -const getTagElementHeadingNumber = tagElement => Number(tagElement.tagName.replace('H', '')); diff --git a/assets/src/blocks/Submenu/useSubmenuItemsLoad.js b/assets/src/blocks/Submenu/useSubmenuItemsLoad.js new file mode 100644 index 000000000..31b68fe13 --- /dev/null +++ b/assets/src/blocks/Submenu/useSubmenuItemsLoad.js @@ -0,0 +1,50 @@ +import { useState, useEffect } from '@wordpress/element'; +import { getHeadings } from './submenuFunctions'; + +export const useSubmenuItemsLoad = (levels, isEditing) => { + + const [menuItems, setMenuItems] = useState([]); + + const getHeadingNumber = tag => Number(tag.tagName.replace('H', '')); + + const loadMenuItems = () => { + // Get all heading tags that we need to query + const headings = levels.map(level => `h${level.heading}`); + const tags = getHeadings(headings, isEditing ? 'editor-styles-wrapper' : 'page-template'); + if (!tags) { + return []; + } + return tags.reduce((menuItems, tag, index) => { + const headingNumber = getHeadingNumber(tag); + let previousHeadingNumber = 0; + if (index > 0) { + previousHeadingNumber = getHeadingNumber(tags[index - 1]); + } + // Get the properties that we need to create the new menu item + const correspondingLevel = levels.find(level => level.heading === headingNumber); + const id = tag.id || tag.textContent.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); // equivalent of WP sanitize_title function + const menuItem = { + text: tag.textContent, + id: `${id}-h${headingNumber}-${index}`, + style: correspondingLevel.style, + link: correspondingLevel.link, + type: `h${headingNumber}`, + children: [] + }; + if (previousHeadingNumber && headingNumber > previousHeadingNumber) { + // In this case we need to add this menuItem to the children of the previous one + menuItems[menuItems.length - 1].children.push(menuItem); + } else { + menuItems.push(menuItem); + } + return menuItems; + }, []); + }; + + useEffect(() => { + const items = loadMenuItems(); + setMenuItems(items); + }, [levels]); + + return { menuItems }; +}; From 81ee9c40c8eae425174da333588fe8bf785dddac Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Fri, 14 Aug 2020 13:21:23 +0200 Subject: [PATCH 04/20] PLANET-4811 Only search DOM on frontend, useSelect in editor * 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. * Add backtop as an effect, which simply toggles the display of it. The backtop markup and style will be added to the base template in https://github.com/greenpeace/planet4-master-theme/pull/1166 * 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. PLANET-4811 Move complex logic to separate function --- assets/src/blocks/Submenu/SubmenuEditor.js | 72 ++++++++++++-- assets/src/blocks/Submenu/SubmenuFrontend.js | 28 ++++-- assets/src/blocks/Submenu/SubmenuItems.js | 25 +---- assets/src/blocks/Submenu/generateAnchor.js | 2 + .../src/blocks/Submenu/getHeadingsFromDom.js | 26 +++++ assets/src/blocks/Submenu/getSubmenuStyle.js | 13 +++ assets/src/blocks/Submenu/makeHierarchical.js | 33 +++++++ assets/src/blocks/Submenu/submenuFunctions.js | 98 ------------------- .../src/blocks/Submenu/useSubmenuItemsLoad.js | 50 ---------- 9 files changed, 162 insertions(+), 185 deletions(-) create mode 100644 assets/src/blocks/Submenu/generateAnchor.js create mode 100644 assets/src/blocks/Submenu/getHeadingsFromDom.js create mode 100644 assets/src/blocks/Submenu/getSubmenuStyle.js create mode 100644 assets/src/blocks/Submenu/makeHierarchical.js delete mode 100644 assets/src/blocks/Submenu/submenuFunctions.js delete mode 100644 assets/src/blocks/Submenu/useSubmenuItemsLoad.js diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index 6a204802c..2d48eecaf 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -3,12 +3,64 @@ 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 './makeHierarchical'; +import { generateAnchor } from './generateAnchor'; +import { useSelect } from '@wordpress/data'; const { __ } = wp.i18n; const { RichText } = wp.blockEditor; +// 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 extractHeaders = (blocks, selectedLevels) => { + const headers = []; + 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); + + headers.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; + } + headers.push({ + level, + content: block.attributes[fieldName], + }); + } + }); + + return headers; +} + const renderEdit = (attributes, setAttributes) => { function addLevel() { setAttributes({ levels: attributes.levels.concat({ heading: 0, link: false, style: 'none' }) }); @@ -69,11 +121,17 @@ const renderEdit = (attributes, setAttributes) => { ); } -const renderView = (attributes, setAttributes) => { +const renderView = (attributes, setAttributes, className) => { + + const { blocks } = useSelect(select => { + return ({ blocks: select('core/editor').getBlocks() }); + }); + + const flatHeaders = extractHeaders(blocks, attributes.levels); - const { menuItems } = useSubmenuItemsLoad(attributes.levels, true); + const menuItems = makeHierarchical(flatHeaders); - const style = getSubmenuStyle(attributes.className, attributes.submenu_style); + const style = getSubmenuStyle(className, attributes.submenu_style); return (
    @@ -87,7 +145,7 @@ const renderView = (attributes, setAttributes) => { characterLimit={60} multiline="false" /> - {menuItems.length > 0 ? + {flatHeaders.length > 0 ? :
    {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')} @@ -100,6 +158,6 @@ const renderView = (attributes, setAttributes) => { export const SubmenuEditor = ({ attributes, setAttributes, isSelected, className }) => ( {isSelected && renderEdit(attributes, setAttributes)} - {renderView({ className, ...attributes }, setAttributes)} + {renderView(attributes, setAttributes, className)} ); diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index 8b56e6b92..3a5d33d4f 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -1,20 +1,30 @@ -import { Fragment, useEffect } from '@wordpress/element'; -import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions'; +import { useEffect } from '@wordpress/element'; +import { getSubmenuStyle } from './getSubmenuStyle'; import { SubmenuItems } from './SubmenuItems'; -import { useSubmenuItemsLoad } from './useSubmenuItemsLoad'; +import { makeHierarchical } from './makeHierarchical'; +import { getHeadingsFromDom } from './getHeadingsFromDom'; export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => { - const { menuItems } = useSubmenuItemsLoad(levels, false); + const enableBackTop = () => { + const backTop = document.querySelector('.back-top'); + if (!backTop) { + return; + } + backTop.style.display = 'block'; + }; - useEffect(() => addSubmenuActions(menuItems), [menuItems]); + // Enable back top on initial render. + useEffect(enableBackTop, []); + const headings = getHeadingsFromDom(levels); + const menuItems = makeHierarchical(headings); const style = getSubmenuStyle(className, submenu_style); return ( -
    -

    {title}

    - +
    +

    { title }

    +
    ); -} +}; diff --git a/assets/src/blocks/Submenu/SubmenuItems.js b/assets/src/blocks/Submenu/SubmenuItems.js index e375ba08a..a2296d153 100644 --- a/assets/src/blocks/Submenu/SubmenuItems.js +++ b/assets/src/blocks/Submenu/SubmenuItems.js @@ -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 }) => ( -
  • - {link ? + return items.map(({ anchor, text, style, shouldLink, children }) => ( +
  • + {shouldLink ? { - event.preventDefault(); - onSubmenuLinkClick(id); - }} + href={`#${anchor}`} > {text} diff --git a/assets/src/blocks/Submenu/generateAnchor.js b/assets/src/blocks/Submenu/generateAnchor.js new file mode 100644 index 000000000..8d6d0957d --- /dev/null +++ b/assets/src/blocks/Submenu/generateAnchor.js @@ -0,0 +1,2 @@ +export const generateAnchor = text => text.toLowerCase().trim().replace(/ /g, '-') + diff --git a/assets/src/blocks/Submenu/getHeadingsFromDom.js b/assets/src/blocks/Submenu/getHeadingsFromDom.js new file mode 100644 index 000000000..24616a401 --- /dev/null +++ b/assets/src/blocks/Submenu/getHeadingsFromDom.js @@ -0,0 +1,26 @@ +import { generateAnchor } from './generateAnchor'; + +const getHeadingLevel = heading => Number(heading.tagName.replace('H', '')); + +export const getHeadingsFromDom = (selectedLevels) => { + const container = document.querySelector('div.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, + }); + }); +} + diff --git a/assets/src/blocks/Submenu/getSubmenuStyle.js b/assets/src/blocks/Submenu/getSubmenuStyle.js new file mode 100644 index 000000000..08d844481 --- /dev/null +++ b/assets/src/blocks/Submenu/getSubmenuStyle.js @@ -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'; +}; diff --git a/assets/src/blocks/Submenu/makeHierarchical.js b/assets/src/blocks/Submenu/makeHierarchical.js new file mode 100644 index 000000000..599f9049f --- /dev/null +++ b/assets/src/blocks/Submenu/makeHierarchical.js @@ -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; + }, []); +}; diff --git a/assets/src/blocks/Submenu/submenuFunctions.js b/assets/src/blocks/Submenu/submenuFunctions.js deleted file mode 100644 index 51a3a9dc5..000000000 --- a/assets/src/blocks/Submenu/submenuFunctions.js +++ /dev/null @@ -1,98 +0,0 @@ -// 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'; -}; - -export const addSubmenuActions = submenu => { - if (submenu && Array.isArray(submenu)) { - for (let i = 0; i < submenu.length; i++) { - const menu = submenu[i]; - addTargetIds(menu); - formatChildren(menu); - } - - // Add "back to top" button behaviour - let backtop = document.querySelector('a.back-top'); - const submenuBlock = document.querySelector('section.submenu-block'); - - if (submenuBlock) { - // If back to top button doesn't exist yet, we need to create it - if (!backtop) { - backtop = document.createElement('a'); - backtop.href = '#'; - backtop.className = 'back-top'; - document.body.appendChild(backtop); - } - addBackToTopBehaviour(backtop); - } - } -}; - -// Add onscroll function and proper positioning for back to top behaviour -const addBackToTopBehaviour = backtop => { - const cookies = document.getElementById('set-cookie'); - window.onscroll = () => { - if (window.pageYOffset > 400 && backtop.style.display !== 'block') { - backtop.style.display = 'block'; - if (cookies) { - const cookiesStyles = window.getComputedStyle(cookies); - if (cookiesStyles && cookiesStyles.display !== 'none') { - backtop.style.bottom = '120px'; - } else { - backtop.style.bottom = '50px'; - } - } - } else if (window.pageYOffset <= 400 && backtop.style.display !== 'none') { - backtop.style.display = 'none'; - } - }; -}; - -/** - * Format submenu entry children. - * - * @param menu Submenu entry - */ -const formatChildren = menu => { - if (menu.children && Array.isArray(menu.children)) { - for (let k = 0; k < menu.children.length; k++) { - const child = menu.children[k]; - addTargetIds(child); - formatChildren(child); - } - } -}; - -/** - * Add ids to the items, to be able to scroll to them. - * - * @param item Submenu menu item - */ -const addTargetIds = item => { - if (item.link) { - const headings = getHeadings(item.type); - if (headings) { - headings.forEach(heading => { - if (heading.textContent === item.text && !heading.id) { - heading.id = item.id; - } - }); - } - } -}; - -export const getHeadings = (headings, className = 'page-template') => { - // We need to make sure it's a div element, - // since for 'page-template' className we have it on the body too - const page = document.querySelector(`div.${className}`); - return page ? [...page.querySelectorAll(headings)] : null; -}; diff --git a/assets/src/blocks/Submenu/useSubmenuItemsLoad.js b/assets/src/blocks/Submenu/useSubmenuItemsLoad.js deleted file mode 100644 index 31b68fe13..000000000 --- a/assets/src/blocks/Submenu/useSubmenuItemsLoad.js +++ /dev/null @@ -1,50 +0,0 @@ -import { useState, useEffect } from '@wordpress/element'; -import { getHeadings } from './submenuFunctions'; - -export const useSubmenuItemsLoad = (levels, isEditing) => { - - const [menuItems, setMenuItems] = useState([]); - - const getHeadingNumber = tag => Number(tag.tagName.replace('H', '')); - - const loadMenuItems = () => { - // Get all heading tags that we need to query - const headings = levels.map(level => `h${level.heading}`); - const tags = getHeadings(headings, isEditing ? 'editor-styles-wrapper' : 'page-template'); - if (!tags) { - return []; - } - return tags.reduce((menuItems, tag, index) => { - const headingNumber = getHeadingNumber(tag); - let previousHeadingNumber = 0; - if (index > 0) { - previousHeadingNumber = getHeadingNumber(tags[index - 1]); - } - // Get the properties that we need to create the new menu item - const correspondingLevel = levels.find(level => level.heading === headingNumber); - const id = tag.id || tag.textContent.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, ''); // equivalent of WP sanitize_title function - const menuItem = { - text: tag.textContent, - id: `${id}-h${headingNumber}-${index}`, - style: correspondingLevel.style, - link: correspondingLevel.link, - type: `h${headingNumber}`, - children: [] - }; - if (previousHeadingNumber && headingNumber > previousHeadingNumber) { - // In this case we need to add this menuItem to the children of the previous one - menuItems[menuItems.length - 1].children.push(menuItem); - } else { - menuItems.push(menuItem); - } - return menuItems; - }, []); - }; - - useEffect(() => { - const items = loadMenuItems(); - setMenuItems(items); - }, [levels]); - - return { menuItems }; -}; From bcdf68b794b905ac902d7490d859ac50e7592039 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Mon, 24 Aug 2020 17:12:11 +0200 Subject: [PATCH 05/20] PLANET-4811 Only show H2 if there's a title --- assets/src/blocks/Submenu/SubmenuEditor.js | 2 +- assets/src/blocks/Submenu/SubmenuFrontend.js | 4 +++- assets/src/blocks/Submenu/makeHierarchical.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index 2d48eecaf..342ac4544 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -145,7 +145,7 @@ const renderView = (attributes, setAttributes, className) => { characterLimit={60} multiline="false" /> - {flatHeaders.length > 0 ? + {menuItems.length > 0 ? :
    {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')} diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index 3a5d33d4f..a6df8d806 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -23,7 +23,9 @@ export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => return (
    -

    { title }

    + { !!title && ( +

    { title }

    + ) }
    ); diff --git a/assets/src/blocks/Submenu/makeHierarchical.js b/assets/src/blocks/Submenu/makeHierarchical.js index 599f9049f..6b6ff3752 100644 --- a/assets/src/blocks/Submenu/makeHierarchical.js +++ b/assets/src/blocks/Submenu/makeHierarchical.js @@ -17,9 +17,9 @@ export const makeHierarchical = headings => { const menuItem = { text: content, - style: style, + style, children: [], - parent: parent, + parent, level, shouldLink, anchor, From 5edd0753c51cfe91cf9e16730c83d96f890e1fde Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Mon, 14 Sep 2020 18:30:21 +0200 Subject: [PATCH 06/20] PLANET-4811 No need for smooth scrolling JS * Setting scroll-behavior: smooth on html does a much better job at this than custom js. --- assets/src/styles/blocks/Submenu.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/src/styles/blocks/Submenu.scss b/assets/src/styles/blocks/Submenu.scss index 70d39c2cb..c69f33aba 100644 --- a/assets/src/styles/blocks/Submenu.scss +++ b/assets/src/styles/blocks/Submenu.scss @@ -113,6 +113,10 @@ } } +html { + scroll-behavior: smooth; +} + .back-top, div.page-template > .back-top { background-image: url("../../public/images/arrow-up.svg"); From 7502abaa2fcef5df47c5058001abca8e039b5ddb Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Thu, 17 Sep 2020 12:45:47 +0200 Subject: [PATCH 07/20] PLANET-4811 support classic block * Classic blocks aka "core/freeform" need to be parsed in order to extract the same headers as would happen on the frontend. This parsing is probably not very efficient, though the impact should only be when there is a classic block in a post, which is discouraged. * Move extractHeaders to a separate file. PLANET-4811 Correct header to heading --- assets/src/blocks/Submenu/SubmenuEditor.js | 57 +------------- assets/src/blocks/Submenu/generateAnchor.js | 4 +- .../blocks/Submenu/getHeadingsFromBlocks.js | 76 +++++++++++++++++++ 3 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 assets/src/blocks/Submenu/getHeadingsFromBlocks.js diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index 342ac4544..507a4a3bf 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -5,62 +5,11 @@ import { SubmenuItems } from './SubmenuItems'; import { InspectorControls } from '@wordpress/block-editor'; import { getSubmenuStyle } from './getSubmenuStyle'; import { makeHierarchical } from './makeHierarchical'; -import { generateAnchor } from './generateAnchor'; +import { getHeadingsFromBlocks} from './getHeadingsFromBlocks'; import { useSelect } from '@wordpress/data'; const { __ } = wp.i18n; const { RichText } = wp.blockEditor; - -// 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 extractHeaders = (blocks, selectedLevels) => { - const headers = []; - 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); - - headers.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; - } - headers.push({ - level, - content: block.attributes[fieldName], - }); - } - }); - - return headers; -} - const renderEdit = (attributes, setAttributes) => { function addLevel() { setAttributes({ levels: attributes.levels.concat({ heading: 0, link: false, style: 'none' }) }); @@ -127,9 +76,9 @@ const renderView = (attributes, setAttributes, className) => { return ({ blocks: select('core/editor').getBlocks() }); }); - const flatHeaders = extractHeaders(blocks, attributes.levels); + const flatHeadings = getHeadingsFromBlocks(blocks, attributes.levels); - const menuItems = makeHierarchical(flatHeaders); + const menuItems = makeHierarchical(flatHeadings); const style = getSubmenuStyle(className, attributes.submenu_style); diff --git a/assets/src/blocks/Submenu/generateAnchor.js b/assets/src/blocks/Submenu/generateAnchor.js index 8d6d0957d..c5c06db31 100644 --- a/assets/src/blocks/Submenu/generateAnchor.js +++ b/assets/src/blocks/Submenu/generateAnchor.js @@ -1,2 +1,4 @@ -export const generateAnchor = text => text.toLowerCase().trim().replace(/ /g, '-') +export const generateAnchor = (text, previousHeadings) => { + return text.toLowerCase().trim().replace(/ /g, '-'); +}; diff --git a/assets/src/blocks/Submenu/getHeadingsFromBlocks.js b/assets/src/blocks/Submenu/getHeadingsFromBlocks.js new file mode 100644 index 000000000..307c512c9 --- /dev/null +++ b/assets/src/blocks/Submenu/getHeadingsFromBlocks.js @@ -0,0 +1,76 @@ +import { generateAnchor } from './generateAnchor'; + +// We can put the other blocks that can have a heading 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}, +]; + +export const getHeadingsFromBlocks = (blocks, selectedLevels) => { + const headings = []; + 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); + + headings.push({ + level: blockLevel, + content: block.attributes.content, + anchor, + style: levelConfig.style, + shouldLink: levelConfig.link, + }); + + return; + } + + if (block.name === 'core/freeform') { + const parser = new DOMParser(); + const selector = selectedLevels.map(({heading})=>`h${heading}`).join(); + const doc = parser.parseFromString(block.attributes.content, 'text/html'); + + const classicHeadings = doc.querySelectorAll(selector); + + headings.push(...[...classicHeadings].map(h => { + const blockLevel = parseInt(h.tagName.replace('H', '')); + const levelConfig = selectedLevels.find(selected => selected.heading === blockLevel); + + return ({ + level: blockLevel, + content: h.innerText, + anchor: generateAnchor(h.innerText), + 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; + } + headings.push({ + level, + content: block.attributes[fieldName], + }); + } + }); + + return headings; +} + From 0f8f28233b22305f50bfb98f97fd0318b7667fe9 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Thu, 17 Sep 2020 14:06:18 +0200 Subject: [PATCH 08/20] PLANET-4811 Ensure anchors are unique on a page --- assets/src/blocks/Submenu/generateAnchor.js | 12 ++++++++++-- assets/src/blocks/Submenu/getHeadingsFromBlocks.js | 7 ++++--- assets/src/blocks/Submenu/getHeadingsFromDom.js | 6 +++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/assets/src/blocks/Submenu/generateAnchor.js b/assets/src/blocks/Submenu/generateAnchor.js index c5c06db31..3e951b38e 100644 --- a/assets/src/blocks/Submenu/generateAnchor.js +++ b/assets/src/blocks/Submenu/generateAnchor.js @@ -1,4 +1,12 @@ -export const generateAnchor = (text, previousHeadings) => { - return text.toLowerCase().trim().replace(/ /g, '-'); +export const generateAnchor = (text, previousAnchors) => { + const anchor = text.toLowerCase().trim().replace(/[^a-zA-Z\d:\u00C0-\u00FF]/g, '-'); + + let i = 0, unique = anchor; + + while (previousAnchors.includes(unique)) { + unique = `${anchor}-${++i}` + } + + return unique; }; diff --git a/assets/src/blocks/Submenu/getHeadingsFromBlocks.js b/assets/src/blocks/Submenu/getHeadingsFromBlocks.js index 307c512c9..49f3ae0fb 100644 --- a/assets/src/blocks/Submenu/getHeadingsFromBlocks.js +++ b/assets/src/blocks/Submenu/getHeadingsFromBlocks.js @@ -2,7 +2,6 @@ import { generateAnchor } from './generateAnchor'; // We can put the other blocks that can have a heading 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}, ]; @@ -19,7 +18,7 @@ export const getHeadingsFromBlocks = (blocks, selectedLevels) => { return; } - const anchor = block.attributes.anchor || generateAnchor(block.attributes.content); + const anchor = block.attributes.anchor || generateAnchor(block.attributes.content, headings.map(h => h.anchor)); headings.push({ level: blockLevel, @@ -43,10 +42,12 @@ export const getHeadingsFromBlocks = (blocks, selectedLevels) => { const blockLevel = parseInt(h.tagName.replace('H', '')); const levelConfig = selectedLevels.find(selected => selected.heading === blockLevel); + const anchor = h.id || generateAnchor(block.attributes.content, headings.map(h => h.anchor)); + return ({ level: blockLevel, content: h.innerText, - anchor: generateAnchor(h.innerText), + anchor, style: levelConfig.style, shouldLink: levelConfig.link, }); diff --git a/assets/src/blocks/Submenu/getHeadingsFromDom.js b/assets/src/blocks/Submenu/getHeadingsFromDom.js index 24616a401..c73a7980a 100644 --- a/assets/src/blocks/Submenu/getHeadingsFromDom.js +++ b/assets/src/blocks/Submenu/getHeadingsFromDom.js @@ -7,13 +7,17 @@ export const getHeadingsFromDom = (selectedLevels) => { // Get all heading tags that we need to query const headingsSelector = selectedLevels.map(level => `:not(.submenu-block) h${level.heading}`); + const usedAnchors = []; + return [...container.querySelectorAll(headingsSelector)].map(heading=> { const levelConfig = selectedLevels.find((selected) => selected.heading === getHeadingLevel(heading)) if (!heading.id) { - heading.id = generateAnchor(heading.textContent); + heading.id = generateAnchor(heading.textContent, usedAnchors); } + usedAnchors.push(heading.id); + return ({ content: heading.textContent, level: levelConfig.heading, From 2f59c45c79a97cb431021345486952b012625cf6 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Thu, 17 Sep 2020 17:38:23 +0200 Subject: [PATCH 09/20] PLANET-4811 Fix too low z-index on wrapper * Otherwise it would have the same z-index as the paragraphs it's floating next to, in the side style. That made the links unclickable. --- assets/src/styles/blocks/Submenu.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/src/styles/blocks/Submenu.scss b/assets/src/styles/blocks/Submenu.scss index c69f33aba..7db6b80a2 100644 --- a/assets/src/styles/blocks/Submenu.scss +++ b/assets/src/styles/blocks/Submenu.scss @@ -1,3 +1,8 @@ +// On outer element as otherwise it will not be clickable when floating on the right. +div[data-render="planet4-blocks/submenu"] { + z-index: 4; +} + .submenu-block { background-color: rgba(255, 255, 255, 0.9); box-shadow: 5px 5px 5px 0 rgba(128, 128, 128, 0.5); From 9e312242e565da4eb90f58bae5dd51c68dc4640b Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Mon, 21 Sep 2020 10:31:09 +0200 Subject: [PATCH 10/20] PLANET-4811 Add scroll padding * Prevents the navbar covering the element we're scrolling to. We probably want to add this site-wide, but probably best handled distinctly so we can check if this works everywhere. * For now this padding is tuned for non-logged in users, as the admin area needs some additional padding which is more tricky to detect. The scroll-padding-top needs to be on html but on that element we can't know if we're logged in or not. --- assets/src/blocks/Submenu/SubmenuEditor.js | 2 +- assets/src/blocks/Submenu/SubmenuFrontend.js | 2 +- assets/src/styles/blocks/Submenu.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index 507a4a3bf..3d0a12e48 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -74,7 +74,7 @@ const renderView = (attributes, setAttributes, className) => { const { blocks } = useSelect(select => { return ({ blocks: select('core/editor').getBlocks() }); - }); + }, null); const flatHeadings = getHeadingsFromBlocks(blocks, attributes.levels); diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index a6df8d806..ea9a6101d 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -11,7 +11,7 @@ export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => if (!backTop) { return; } - backTop.style.display = 'block'; + backTop.classList.remove('d-none'); }; // Enable back top on initial render. diff --git a/assets/src/styles/blocks/Submenu.scss b/assets/src/styles/blocks/Submenu.scss index 7db6b80a2..f3242b05c 100644 --- a/assets/src/styles/blocks/Submenu.scss +++ b/assets/src/styles/blocks/Submenu.scss @@ -120,6 +120,7 @@ div[data-render="planet4-blocks/submenu"] { html { scroll-behavior: smooth; + scroll-padding-top: 4rem; } .back-top, @@ -134,7 +135,6 @@ div.page-template > .back-top { background-color: #074365; text-align: center; box-shadow: 0 2px 5px rgba(0, 0, 0, .25); - display: none; z-index: 1000; @include x-large-and-up { From 8a460476f3015b8bd21fc0e5ba51a18fb7df8d11 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Mon, 21 Sep 2020 14:51:31 +0200 Subject: [PATCH 11/20] PLANET-4811 Hide backtop when at top --- assets/src/blocks/Submenu/SubmenuFrontend.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/src/blocks/Submenu/SubmenuFrontend.js b/assets/src/blocks/Submenu/SubmenuFrontend.js index ea9a6101d..471173370 100644 --- a/assets/src/blocks/Submenu/SubmenuFrontend.js +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -11,7 +11,13 @@ export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => if (!backTop) { return; } - backTop.classList.remove('d-none'); + document.addEventListener('scroll', () => { + if (document.documentElement.scrollTop > 400) { + backTop.classList.remove('d-none'); + } else { + backTop.classList.add('d-none'); + } + }); }; // Enable back top on initial render. From e3bfcad7902bfd6bee6ed093f54cc09bf17719b6 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Fri, 25 Sep 2020 11:40:37 +0200 Subject: [PATCH 12/20] PLANET-4811 Remove text-decoration rule * This was overriding the link hover styles, and is not needed when not hovering. --- assets/src/styles/blocks/Submenu.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/src/styles/blocks/Submenu.scss b/assets/src/styles/blocks/Submenu.scss index f3242b05c..4b69e3e7d 100644 --- a/assets/src/styles/blocks/Submenu.scss +++ b/assets/src/styles/blocks/Submenu.scss @@ -28,7 +28,6 @@ div[data-render="planet4-blocks/submenu"] { a { color: $menu-blue; - text-decoration: none; } ul { From 4b5ad168d667ac6fc8eb83def410f2cf995baae4 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Fri, 25 Sep 2020 18:21:21 +0200 Subject: [PATCH 13/20] PLANET-4811 Remove html support, wasn't working --- assets/src/blocks/Submenu/SubmenuBlock.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/blocks/Submenu/SubmenuBlock.js b/assets/src/blocks/Submenu/SubmenuBlock.js index 011790ebe..d10c8da2c 100644 --- a/assets/src/blocks/Submenu/SubmenuBlock.js +++ b/assets/src/blocks/Submenu/SubmenuBlock.js @@ -40,6 +40,7 @@ export class SubmenuBlock { }, supports: { multiple: false, // Use the block just once per post. + html: false, }, styles: [ { From 5df3639088d9a7f9f6d25ae50e3ac993b049d3d0 Mon Sep 17 00:00:00 2001 From: inwerpsel Date: Tue, 29 Sep 2020 11:07:43 +0200 Subject: [PATCH 14/20] PLANET-4811 Use next level for new level * Pre populate the added level with the last level+1. * Disable options that are a lower or equal level than the previous level. --- assets/src/blocks/Submenu/SubmenuEditor.js | 13 +++++++++++- assets/src/blocks/Submenu/SubmenuLevel.js | 24 +++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/assets/src/blocks/Submenu/SubmenuEditor.js b/assets/src/blocks/Submenu/SubmenuEditor.js index 3d0a12e48..6ed547d58 100644 --- a/assets/src/blocks/Submenu/SubmenuEditor.js +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -12,7 +12,9 @@ const { __ } = wp.i18n; const { RichText } = wp.blockEditor; const renderEdit = (attributes, setAttributes) => { function addLevel() { - setAttributes({ levels: attributes.levels.concat({ heading: 0, link: false, style: 'none' }) }); + const [previousLastLevel] = attributes.levels.slice(-1); + const newLevel = previousLastLevel.heading + 1; + setAttributes({ levels: attributes.levels.concat({ heading: newLevel, link: false, style: 'none' }) }); } function onHeadingChange(index, value) { @@ -37,6 +39,14 @@ const renderEdit = (attributes, setAttributes) => { setAttributes({ levels: attributes.levels.slice(0, -1) }); } + function getMinLevel(attributes, index) { + if (index === 0) { + return null; + } + + return attributes.levels[index-1].heading; + } + return ( @@ -48,6 +58,7 @@ const renderEdit = (attributes, setAttributes) => { onStyleChange={onStyleChange} index={i} key={i} + minLevel={getMinLevel(attributes, i)} /> ))}