Skip to content

Commit

Permalink
Merge pull request #1121 from openeuropa/update-EPIC-EWPP-2106-Carousel
Browse files Browse the repository at this point in the history
EWPP-2106: Update Carousel epic branch.
  • Loading branch information
sergepavle authored Jun 15, 2022
2 parents b39c75a + 0ea1b00 commit c0b0b0e
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 73 deletions.
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

0 comments on commit c0b0b0e

Please sign in to comment.