Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EWPP-2204: Update epic branch for Simple teaser. #1120

Merged
Merged
137 changes: 91 additions & 46 deletions js/inpage_navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,63 +19,41 @@
*/
Drupal.behaviors.eclInPageNavigation = {
attach: function attach(context, settings) {
// Loop through all the elements marked as source areas.
Array.prototype.forEach.call(document.querySelectorAll('[data-inpage-navigation-source-area]'), function (area) {
var selectors = area.getAttribute('data-inpage-navigation-source-area');
const inpage_navigations = document.querySelectorAll('.oe-theme-ecl-inpage-navigation');
if (inpage_navigations.length === 0) {
return;
}

// Loop through all the elements that are referenced by the specified selector(s), and mark them as source
// elements. We cannot collect the elements at this stage, as multiple nested areas can be present in the page.
// This could lead to scenarios where elements are collected multiple times, or not collected following the
// order of appearance in the page.
// The :scope pseudo-class is needed to make sure that the selectors are applied inside the parent.
// @see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll#user_notes
Array.prototype.forEach.call(area.querySelectorAll(':scope ' + selectors), function (element) {
element.setAttribute('data-inpage-navigation-source-element', '');
});
});
let containers = [].slice.call(document.querySelectorAll('.inpage-navigation-container'));
// If no specifically defined containers are present, we use the main content as one.
if (containers.length === 0) {
containers.push(document.querySelector('#main-content'));
}

var items_markup = '';
// Collect all the elements marked as source. Now the elements will be unique and ordered correctly.
Array.prototype.forEach.call(document.querySelectorAll('[data-inpage-navigation-source-element]'), function (element) {
var title = element.textContent.trim();
containers.forEach(function (container) {
const nav = container.querySelector(':scope .oe-theme-ecl-inpage-navigation');

// Skip elements with empty content.
if (title.length === 0) {
// Bail out if no inpage navigation element is present.
if (nav === null) {
return;
}

// Generate an unique ID if not present.
if (!element.hasAttribute('id')) {
var id = Drupal.eclInPageNavigation.slug(title);
// If an empty ID is generated, skip this element.
if (id === false) {
return;
}

element.setAttribute('id', id);
}

// Cleanup the markup from the helper attribute added above.
element.removeAttribute('data-inpage-navigation-source-element');

items_markup += Drupal.theme('oe_theme_inpage_navigation_item', element.getAttribute('id'), title);
});
let items_markup = Drupal.eclInPageNavigation.generateItems(container, function (id, text) {
return Drupal.theme('oe_theme_inpage_navigation_item', id, text);
});

// Loop through all the inpage navigation marked with our special class. The auto-initialisation is disabled on
// them, as initialisation should be run only after the items are added. Otherwise JS callbacks won't be applied
// correctly.
Array.prototype.forEach.call(document.querySelectorAll('.oe-theme-ecl-inpage-navigation'), function (block) {
if (items_markup.length === 0) {
// When there are no items, execute the callback to handle the block.
Drupal.eclInPageNavigation.handleEmptyInpageNavigation(block);
Drupal.eclInPageNavigation.handleEmptyInpageNavigation(nav);
return;
}

block.querySelector('ul').innerHTML = items_markup;
var instance = new ECL.InpageNavigation(block);
nav.querySelector('ul').innerHTML = items_markup;

const instance = new ECL.InpageNavigation(nav);
instance.init();
Drupal.eclInPageNavigation.instances.push(instance);
})
});
},
detach: function detach(context, settings, trigger) {
Drupal.eclInPageNavigation.instances.forEach(function (instance){
Expand Down Expand Up @@ -105,6 +83,73 @@
*/
instances: [],

/**
* @callback ItemRenderCallback
* @param {string} id
* The ID of the element this item points to.
* @param {string} text
* The text of the link.
*
* @return {string}
* The HTML of the item.
*/

/**
* Generates the inpage navigation items.
*
* @param {HTMLElement} container
* The element where to search for elements composing the inpage navigation.
* @param {ItemRenderCallback} item_cb
* The single item render callback.
*
* @returns {string}
* The HTML of the generated items.
*/
generateItems: function(container, item_cb) {
Array.prototype.forEach.call(container.querySelectorAll(':scope [data-inpage-navigation-source-area]'), function (area) {
let selectors = area.getAttribute('data-inpage-navigation-source-area');

// Loop through all the elements that are referenced by the specified selector(s), and mark them as source
// elements. We cannot collect the elements at this stage, as multiple nested areas can be present in the page.
// This could lead to scenarios where elements are collected multiple times, or not collected following the
// order of appearance in the page.
// The :scope pseudo-class is needed to make sure that the selectors are applied inside the parent.
// @see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll#user_notes
Array.prototype.forEach.call(area.querySelectorAll(':scope ' + selectors), function (element) {
element.setAttribute('data-inpage-navigation-source-element', '');
});
});

let items_markup = '';
// Collect all the elements marked as source. Now the elements will be unique and ordered correctly.
Array.prototype.forEach.call(container.querySelectorAll(':scope [data-inpage-navigation-source-element]'), function (element) {
let title = element.textContent.trim();

// Skip elements with empty content.
if (title.length === 0) {
return;
}

// Generate an unique ID if not present.
if (!element.hasAttribute('id')) {
let id = Drupal.eclInPageNavigation.slug(title);
// If an empty ID is generated, skip this element.
if (id === false) {
return;
}

element.setAttribute('id', id);
}

// Cleanup the markup from the helper attribute added above.
element.removeAttribute('data-inpage-navigation-source-element');

items_markup += item_cb(element.getAttribute('id'), title);
});

return items_markup;
},

/**
* Generates a unique slug from a text string.
*
Expand All @@ -119,7 +164,7 @@
* A unique slug, safe to use as ID for an element. False when the generated slug is empty.
*/
slug: function(value) {
var originalSlug = value
let originalSlug = value
.toLowerCase()
.trim()
// Remove html tags.
Expand All @@ -128,8 +173,8 @@
.replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '')
.replace(/\s/g, '-');

var slug = originalSlug;
var occurrenceAccumulator = 0;
let slug = originalSlug;
let occurrenceAccumulator = 0;

// If the slug string is empty, quit.
if (slug.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ declare(strict_types = 1);
*/
function oe_theme_inpage_navigation_test_theme($existing, $type, $theme, $path) {
return [
'oe_theme_inpage_navigation_test_content' => [
'render element' => 'element',
],
'oe_theme_inpage_navigation_test_no_elements' => [
'oe_theme_inpage_navigation_test' => [
'render element' => 'element',
],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ oe_theme_inpage_navigation_test.content:
path: '/oe-theme-inpage-navigation-test/content'
defaults:
_title: 'In-page navigation content page'
_controller: '\Drupal\oe_theme_inpage_navigation_test\Controller\InpageNavigationTestController::contentPage'
_controller: '\Drupal\oe_theme_inpage_navigation_test\Controller\InpageNavigationTestController::renderTemplate'
variation: 'content'
requirements:
_access: 'TRUE'

oe_theme_inpage_navigation_test.no_entries:
path: '/oe-theme-inpage-navigation-test/no-entries'
defaults:
_title: 'In-page navigation no entries page'
_controller: '\Drupal\oe_theme_inpage_navigation_test\Controller\InpageNavigationTestController::noEntriesPage'
_controller: '\Drupal\oe_theme_inpage_navigation_test\Controller\InpageNavigationTestController::renderTemplate'
variation: 'no_elements'
requirements:
_access: 'TRUE'

oe_theme_inpage_navigation_test.multiple:
path: '/oe-theme-inpage-navigation-test/multiple'
defaults:
_title: 'In-page navigation multiple containers page'
_controller: '\Drupal\oe_theme_inpage_navigation_test\Controller\InpageNavigationTestController::renderTemplate'
variation: 'multiple'
requirements:
_access: 'TRUE'
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,17 @@
class InpageNavigationTestController extends ControllerBase {

/**
* Generates a page with test content for the inpage navigation.
* Renders a test page using a specific variation of the test template.
*
* @return array
* The response render array.
*/
public function contentPage(): array {
return [
'#theme' => 'oe_theme_inpage_navigation_test_content',
];
}

/**
* Returns content that doesn't generate any entries for inpage navigation.
* @param string $variation
* The template variation to render.
*
* @return array
* The response render array.
*/
public function noEntriesPage(): array {
public function renderTemplate(string $variation): array {
return [
'#theme' => 'oe_theme_inpage_navigation_test_no_elements',
'#theme' => 'oe_theme_inpage_navigation_test__' . $variation,
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{#
/**
* @file
* Test template with multiple in-page navigation sections.
*/
#}
{% macro inpage_navigation(title) %}
{% set build = {
'#theme': 'oe_theme_helper_inpage_navigation_block',
'#title': title|default('Page contents'),
'#attached': {
'library': [
'oe_theme/inpage_navigation'
]
}
} %}
{{ build }}
{% endmacro %}

<div id="inpage-navigation-test-container">
<div class="inpage-navigation-container first-container">
{{ _self.inpage_navigation('First nav') }}

<div data-inpage-navigation-source-area="h3">
<h3>Area 1 title 1</h3>
<p>Lorem ipsum dolor sit.</p>
<h3>Area 1 title 2</h3>
<p>Cum eveniet itaque veniam?</p>
{# First occurrence of an heading with "Details" as text. #}
<h3>Details</h3>
<p>Beatae ducimus laborum sapiente?</p>
{# A title that should be ignored in this container. #}
<h2>Area 1 unused title</h2>
</div>
</div>

{# A div that contains elements that should be ignored. #}
<div>
<h2>Lorem ipsum dolor.</h2>
<h2>Adipisci, alias, expedita.</h2>
<h3>Lorem ipsum dolor.</h3>
<h3>Ab alias, iusto.</h3>
</div>

<div class="inpage-navigation-container second-container">
{{ _self.inpage_navigation('Second nav') }}

<div data-inpage-navigation-source-area="h2,h3">
<h2>Area 2 title 1</h2>
<p>Beatae ducimus laborum sapiente?</p>
<h3>Area 2 title 2</h3>
<p>Perspiciatis quo repellendus voluptate!</p>
{# Second occurrence of an heading with "Details" as text. #}
<h2>Details</h2>
<p>Ab dolor earum placeat!</p>
<h3>Area 2 title 4</h3>
<p>Nulla pariatur quaerat reprehenderit.</p>

{#
A "deeply" nested area, to simulate a standalone component with his own source area selectors.
For example, we are rendering a paragraphs entity reference field.
#}
<div class="rendered-paragraph-field" data-inpage-navigation-source-area="strong.title-like">
<strong class="title-like">Special title element</strong>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nostrum, tempora?</p>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,70 @@ public function testLibrary(): void {
$assert_session->elementExists('css', 'h1.ecl-page-header__title.empty-inpage-nav-test');
}

/**
* Tests when multiple inpage navigation containers are present in the page.
*/
public function testMultipleContainers(): void {
$this->drupalGet('/oe-theme-inpage-navigation-test/multiple');
$assert_session = $this->assertSession();

$main_content = $assert_session->elementExists('css', '#inpage-navigation-test-container');
// Each inpage navigation adds two extra IDs, one for a button and one for
// the ul itself. This means that we actually are expecting 8 IDs generated
// in total by the library.
$this->assertCount(12, $main_content->findAll('xpath', '//*[@id]'));

$first_container = $main_content->find('css', '.first-container');
$navigation = $assert_session->elementExists('css', '.oe-theme-ecl-inpage-navigation', $first_container);
$assert = new InPageNavigationAssert();
$expected = [
'title' => 'First nav',
'list' => [
[
'label' => 'Area 1 title 1',
'href' => '#area-1-title-1',
],
[
'label' => 'Area 1 title 2',
'href' => '#area-1-title-2',
],
[
'label' => 'Details',
'href' => '#details',
],
],
];
$assert->assertPattern($expected, $navigation->getOuterHtml());

$second_container = $main_content->find('css', '.second-container');
$navigation = $assert_session->elementExists('css', '.oe-theme-ecl-inpage-navigation', $second_container);
$assert = new InPageNavigationAssert();
$expected = [
'title' => 'Second nav',
'list' => [
[
'label' => 'Area 2 title 1',
'href' => '#area-2-title-1',
],
[
'label' => 'Area 2 title 2',
'href' => '#area-2-title-2',
],
[
'label' => 'Details',
'href' => '#details-1',
],
[
'label' => 'Area 2 title 4',
'href' => '#area-2-title-4',
],
[
'label' => 'Special title element',
'href' => '#special-title-element',
],
],
];
$assert->assertPattern($expected, $navigation->getOuterHtml());
}

}
Loading