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..6a204802c --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuEditor.js @@ -0,0 +1,105 @@ +import { Fragment } from '@wordpress/element'; +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'; + +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 { menuItems } = useSubmenuItemsLoad(attributes.levels, true); + + const style = getSubmenuStyle(attributes.className, attributes.submenu_style); + + return ( +
+ setAttributes({ title })} + keepPlaceholderOnFocus={true} + withoutInteractiveFormatting + characterLimit={60} + multiline="false" + /> + {menuItems.length > 0 ? + : +
+ {__('The submenu block produces no output on the editor.', 'planet4-blocks-backend')} +
+ } +
+ ); +} + +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..8b56e6b92 --- /dev/null +++ b/assets/src/blocks/Submenu/SubmenuFrontend.js @@ -0,0 +1,20 @@ +import { Fragment, useEffect } from '@wordpress/element'; +import { getSubmenuStyle, addSubmenuActions } from './submenuFunctions'; +import { SubmenuItems } from './SubmenuItems'; +import { useSubmenuItemsLoad } from './useSubmenuItemsLoad'; + +export const SubmenuFrontend = ({ title, className, levels, submenu_style }) => { + + const { menuItems } = useSubmenuItemsLoad(levels, false); + + useEffect(() => addSubmenuActions(menuItems), [menuItems]); + + const style = getSubmenuStyle(className, submenu_style); + + return ( +
+

{title}

+ +
+ ); +} 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/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 && + + } +
  • + )); + } + + return menuItems.length > 0 && ( +
    + +
    + ); +} 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..51a3a9dc5 --- /dev/null +++ b/assets/src/blocks/Submenu/submenuFunctions.js @@ -0,0 +1,98 @@ +// 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 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 }; +}; diff --git a/assets/src/frontendIndex.js b/assets/src/frontendIndex.js index 6d5ae91e1..7b1730754 100644 --- a/assets/src/frontendIndex.js +++ b/assets/src/frontendIndex.js @@ -1,11 +1,13 @@ import { SpreadsheetFrontend } from './blocks/Spreadsheet/SpreadsheetFrontend'; import { CounterFrontend } from './blocks/Counter/CounterFrontend'; import { ArticlesFrontend } from './blocks/Articles/ArticlesFrontend'; +import { SubmenuFrontend } from './blocks/Submenu/SubmenuFrontend'; const COMPONENTS = { 'planet4-blocks/spreadsheet': SpreadsheetFrontend, 'planet4-blocks/counter': CounterFrontend, 'planet4-blocks/articles': ArticlesFrontend, + '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/assets/src/styles/styleguide b/assets/src/styles/styleguide index 680fbf3ee..14c0181dd 160000 --- a/assets/src/styles/styleguide +++ b/assets/src/styles/styleguide @@ -1 +1 @@ -Subproject commit 680fbf3ee4b6cabef04edd3f6f6ad33c4b9c76b5 +Subproject commit 14c0181ddcb19f68f56c5b777227457e72404edc diff --git a/classes/blocks/class-submenu.php b/classes/blocks/class-submenu.php index 8d98fc44e..e7fa1cccf 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,163 +77,12 @@ public function __construct() { } /** - * Get all the data that will be needed to render the block correctly. - * - * @param array $attributes This is the array of fields of this block. - * - * @return array The data to be passed in the View. - */ - public function prepare_data( $attributes ): 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(); - } - - $menu = []; - if ( ! is_null( $post ) && isset( $attributes['levels'] ) ) { - $content = $post->post_content; - $menu = $this->parse_post_content( $content, $attributes['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; - } - - /** - * 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 = $this->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 $this->build_menu( 1, $nodes, $heading_meta ); - } - - /** - * Extract shortcode attributes for given heading level. - * - * @param array $level Block level attributes. + * Required by the `Base_Block` class. * - * @return array|null associative array or null if menu level is not configured + * @param array $fields Unused, required by the abstract function. */ - 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 = $this->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'] ); - - // 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; + public function prepare_data( $fields ): array { + return []; } } diff --git a/classes/command/class-shortcode-to-gutenberg.php b/classes/command/class-shortcode-to-gutenberg.php index 3f914f2e0..891d50bd7 100644 --- a/classes/command/class-shortcode-to-gutenberg.php +++ b/classes/command/class-shortcode-to-gutenberg.php @@ -32,7 +32,6 @@ public function init() { 'shortcake_columns', 'shortcake_carousel_header', 'shortcake_cookies', - 'shortcake_counter', 'shortcake_enblock', 'shortcake_gallery', 'shortcake_happy_point', @@ -40,7 +39,6 @@ public function init() { 'shortcake_newcovers', 'shortcake_social_media', 'shortcake_split_two_columns', - '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 aeba7f470..7280ced6f 100644 --- a/classes/command/converters/class-shortcode-converter-factory.php +++ b/classes/command/converters/class-shortcode-converter-factory.php @@ -34,8 +34,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/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 199faa3c0..c405871f2 100644 --- a/tests/unit/test-shortcode-converter.php +++ b/tests/unit/test-shortcode-converter.php @@ -40,7 +40,6 @@ public function setUp() { * @dataProvider media_shortcodes_provider * @dataProvider social_media_shortcodes_provider * @dataProvider split_two_columns_shortcodes_provider - * @dataProvider submenu_shortcodes_provider * @dataProvider timeline_shortcodes_provider * @dataProvider take_action_boxout_shortcodes_provider */ @@ -602,66 +601,6 @@ public function split_two_columns_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. *