vlsuite-1.0.x-dev/modules/vlsuite_layout/src/VLSuiteLayoutHeadingsMenuTrait.php
modules/vlsuite_layout/src/VLSuiteLayoutHeadingsMenuTrait.php
<?php
namespace Drupal\vlsuite_layout;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html;
/**
* VLSuite layout headings menu trait.
*/
trait VLSuiteLayoutHeadingsMenuTrait {
/**
* Headings menu build alter.
*
* @param array $regions
* Regions.
* @param array $build
* Build.
* @param string $menu_block_region_id
* Menu block region id (where is located).
* @param string $headings_block_region_id
* Headings block region id (where is located).
*/
public function headingsMenuBuildAlter(array $regions, array &$build, $menu_block_region_id, $headings_block_region_id) {
if ($this->headingsMenuBuildRequirements($regions, $menu_block_region_id, $headings_block_region_id)) {
$this->headingsMenuBuildAlterExecute($build, $menu_block_region_id, $headings_block_region_id);
}
}
/**
* Headings menu build requirements.
*
* First check provided region names.
* Then check menu block in region.
* Then check text block in region.
*
* @param array $regions
* Regions.
* @param string $menu_block_region_id
* Menu block region id (where is located).
* @param string $headings_block_region_id
* Headings block region id (where is located).
*/
protected function headingsMenuBuildRequirements(array $regions, $menu_block_region_id, $headings_block_region_id) {
if (empty($regions[$menu_block_region_id]) || empty($regions[$headings_block_region_id])) {
return FALSE;
}
$menu_block_region_requirement = FALSE;
foreach ($regions[$menu_block_region_id] as $block) {
if (($block['#plugin_id'] ?? NULL) == 'vlsuite_block_headings_menu') {
$menu_block_region_requirement = TRUE;
break;
}
}
$headings_block_region_requirement = FALSE;
// @note For now just compatible with field block of type processed text.
foreach ($regions[$headings_block_region_id] as $uuid => $block) {
if (empty($block['content'])) {
continue;
}
foreach (Element::children($block['content']) as $delta) {
if (($regions[$headings_block_region_id][$uuid]['content'][$delta]['#type'] ?? NULL) == 'processed_text') {
$headings_block_region_requirement = TRUE;
break;
}
}
}
return $menu_block_region_requirement && $headings_block_region_requirement;
}
/**
* Headings menu build alter execute.
*
* @param array $build
* Build.
* @param string $menu_block_region_id
* Menu block region id (where is located).
* @param string $headings_block_region_id
* Headings block region id (where is located).
*/
protected function headingsMenuBuildAlterExecute(array &$build, $menu_block_region_id, $headings_block_region_id) {
$anchors_tree = [];
foreach ($build[$headings_block_region_id] as $uuid => $block) {
if (empty($block['content'])) {
continue;
}
foreach (Element::children($block['content']) as $delta) {
if (($build[$headings_block_region_id][$uuid]['content'][$delta]['#type'] ?? NULL) == 'processed_text') {
$html_dom = Html::load($build[$headings_block_region_id][$uuid]['content'][$delta]['#text']);
$anchors_tree = array_merge($anchors_tree, $this->headingsMenuProcessHtmlDomText($html_dom));
$build[$headings_block_region_id][$uuid]['content'][$delta]['#text'] = Html::serialize($html_dom);
}
}
}
foreach ($build[$menu_block_region_id] as $uuid => $block) {
if (($block['#plugin_id'] ?? NULL) == 'vlsuite_block_headings_menu') {
$build[$menu_block_region_id][$uuid]['content']['headings_menu_nav']['#anchors_tree'] = $anchors_tree;
}
}
$build['#theme_wrappers']['container']['#attributes']['class'][] = 'vlsuite-layout-headings-menu';
}
/**
* Headings menu process html DOM text to build mapping.
*
* @param \DOMDocument $html_dom
* Html DOM.
*
* @return array
* Result.
*/
protected function headingsMenuProcessHtmlDomText(\DOMDocument $html_dom) {
$xpath = new \DOMXPath($html_dom);
$mapping = [];
$parents = [];
$deep_level = 0;
foreach ($xpath->query('//h1|//h2|//h3|//h4|//h5|//h6') as $heading) {
$deep_level = empty($deep_level) ? $heading->nodeName[1] : $deep_level;
// Deeper level, create empty array where items will be added & parent.
if ($heading->nodeName[1] > $deep_level) {
$ref = &NestedArray::getValue($mapping, $parents);
$ref[] = [];
$parents[] = array_key_last($ref);
$deep_level = $heading->nodeName[1];
}
// Higher level, remove last parent.
elseif ($heading->nodeName[1] < $deep_level) {
$deep_level = $heading->nodeName[1];
array_pop($parents);
}
// Add new link at defined parents deep level.
$ref = &NestedArray::getValue($mapping, $parents);
$ref += $this->headingsMenuProcessHtmlDomHeading($heading);
}
return $mapping;
}
/**
* Headings menu process html DOM heading element.
*
* @param \DOMElement $html_dom_heading
* Heading dom element.
*
* @return array
* Result [id => Title].
*/
protected function headingsMenuProcessHtmlDomHeading(\DOMElement $html_dom_heading) {
$id_attr = $html_dom_heading->getAttribute('id');
if (empty($id_attr)) {
$id_attr = Html::getUniqueId(\Drupal::service('transliteration')->transliterate($html_dom_heading->textContent));
$html_dom_heading->setAttribute('id', $id_attr);
}
return [$id_attr => trim($html_dom_heading->textContent)];
}
}
