From ccccbb0d9ddea634f1452ac5a9eb13ab6b3bd9f0 Mon Sep 17 00:00:00 2001 From: Tero Elonen Date: Wed, 8 May 2024 17:33:05 +0300 Subject: [PATCH 1/3] UHF-9739: Update table of contents to work on news pages when there is news update content available --- .../helfi_toc/assets/js/tableOfContents.js | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/modules/helfi_toc/assets/js/tableOfContents.js b/modules/helfi_toc/assets/js/tableOfContents.js index 925844c79..c24e47a32 100644 --- a/modules/helfi_toc/assets/js/tableOfContents.js +++ b/modules/helfi_toc/assets/js/tableOfContents.js @@ -24,6 +24,7 @@ const anchors = []; const tableOfContents = document.getElementById('helfi-toc-table-of-contents'); + const tableOfContentsNewsUpdates = document.getElementById('helfi-toc-table-of-contents-news-updates'); const tableOfContentsList = document.querySelector('#helfi-toc-table-of-contents-list > ul'); const mainContent = document.querySelector('main.layout-main-wrapper'); const reservedElems = document.querySelectorAll('[id]'); @@ -34,7 +35,7 @@ // Exclude elements from TOC that are not content: // e.g. TOC, sidebar, cookie compliency-banner etc. - const exclusions = + let exclusions = '' + ':not(.layout-sidebar-first *)' + ':not(.layout-sidebar-second *)' + @@ -44,6 +45,14 @@ ':not(.embedded-content-cookie-compliance *)' + ':not(.react-and-share-cookie-compliance *)'; + // On a news page we need to concentrate on the news update container. + if (tableOfContentsNewsUpdates) { + exclusions += + ':not(.components--upper *)' + + ':not(.block--news-of-interest *)' + + ':not(#helfi-toc-table-of-contents-news-updates *)'; + } + const titleComponents = [ `h2${exclusions}`, `h3${exclusions}`, @@ -166,6 +175,15 @@ anchors.push(anchorName); + // On updating news there is published date under the title that we want to display in the + // table of contents news update version. For normal table of contents this remains empty. + let contentPublishDate = ''; + + if (tableOfContentsNewsUpdates && content.nextSibling && content.nextElementSibling.nodeName === 'TIME') { + let contentPublishDateStamp = new Date(content.nextElementSibling.dateTime); + contentPublishDate = `${contentPublishDateStamp.getDate()}.${contentPublishDateStamp.getMonth() + 1}.${contentPublishDateStamp.getFullYear()}`; + } + // Create table of contents if component is enabled. if (tableOfContentsList && nodeName === 'h2') { let listItem = document.createElement('li'); @@ -176,6 +194,15 @@ link.href = `#${anchorName}`; link.textContent = content.textContent.trim(); + // Add content publish date and its wrapper to list items only if they exist. + if (contentPublishDate) { + let publishDate = document.createElement('time'); + publishDate.dateTime = content.nextElementSibling.dateTime; + publishDate.textContent = contentPublishDate; + + listItem.appendChild(publishDate); + } + listItem.appendChild(link); tableOfContentsList.appendChild(listItem); } @@ -184,16 +211,24 @@ content.setAttribute('tabindex', '-1'); // Set tabindex to -1 to avoid issues with screen readers. }); - if (tableOfContents) { + function updateTOC(tocElement) { // Remove loading text and noscript element. - const removeElements = tableOfContents.parentElement.querySelectorAll('.js-remove'); + const removeElements = tocElement.parentElement.querySelectorAll('.js-remove'); removeElements.forEach(function (element) { element.remove(); }); // Update toc visible. - tableOfContents.setAttribute('data-js', 'true'); + tocElement.setAttribute('data-js', 'true'); } + + // Select which type of TOC should be displayed. + if (tableOfContentsNewsUpdates) { + updateTOC(tableOfContentsNewsUpdates); + } else { + updateTOC(tableOfContents); + } + }, }; })(Drupal, once, drupalSettings); From a99922fd5d454b334ddeafba5ddc75a0440b73bf Mon Sep 17 00:00:00 2001 From: Tero Elonen Date: Mon, 13 May 2024 15:27:14 +0300 Subject: [PATCH 2/3] UHF-9739: Updating the comments on the javascript so that it is easier to understand the extended table of contents functionality --- modules/helfi_toc/assets/js/tableOfContents.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/helfi_toc/assets/js/tableOfContents.js b/modules/helfi_toc/assets/js/tableOfContents.js index c24e47a32..17d954a34 100644 --- a/modules/helfi_toc/assets/js/tableOfContents.js +++ b/modules/helfi_toc/assets/js/tableOfContents.js @@ -23,7 +23,10 @@ } const anchors = []; + // This is the normal TOC that is used on pages that have the option to enable TOC. const tableOfContents = document.getElementById('helfi-toc-table-of-contents'); + // This is the special container for updating news that displays an alternative version of the TOC. + // It is automatically on and only used on front page instance. const tableOfContentsNewsUpdates = document.getElementById('helfi-toc-table-of-contents-news-updates'); const tableOfContentsList = document.querySelector('#helfi-toc-table-of-contents-list > ul'); const mainContent = document.querySelector('main.layout-main-wrapper'); @@ -45,7 +48,7 @@ ':not(.embedded-content-cookie-compliance *)' + ':not(.react-and-share-cookie-compliance *)'; - // On a news page we need to concentrate on the news update container. + // On a updating news page we need to ignore everything else other than the news update container. if (tableOfContentsNewsUpdates) { exclusions += ':not(.components--upper *)' + From f11b67b7c138796d8d2244c883b94a7b12cf930d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Kalij=C3=A4rvi?= Date: Fri, 17 May 2024 10:41:50 +0300 Subject: [PATCH 3/3] UHF-9739: Refactored table of contents javascript to be used globally for instance specific needs. --- .../helfi_toc/assets/js/tableOfContents.js | 325 +++++++++--------- 1 file changed, 166 insertions(+), 159 deletions(-) diff --git a/modules/helfi_toc/assets/js/tableOfContents.js b/modules/helfi_toc/assets/js/tableOfContents.js index 17d954a34..d5b8246a7 100644 --- a/modules/helfi_toc/assets/js/tableOfContents.js +++ b/modules/helfi_toc/assets/js/tableOfContents.js @@ -1,76 +1,71 @@ 'use strict'; -(function (Drupal, once, drupalSettings) { - Drupal.behaviors.table_of_contents = { - attach: function attach() { - - function findAvailableId(name, reserved, anchors, count) { - let newName = name; - if (count > 0) { // Only when headings are not unique on page we want to add counter - newName += `-${count}`; - } - if (reserved.includes(newName)) { - return findAvailableId(name, reserved, anchors, count + 1); - } - - if (anchors.includes(newName)) { - if (count === 0) { - count += 1; // When reserved heading is visible on page, lets start counting from 2 instead of 1 - } - return findAvailableId(name, reserved, anchors, count + 1); - } - return newName; - } - - const anchors = []; - // This is the normal TOC that is used on pages that have the option to enable TOC. - const tableOfContents = document.getElementById('helfi-toc-table-of-contents'); - // This is the special container for updating news that displays an alternative version of the TOC. - // It is automatically on and only used on front page instance. - const tableOfContentsNewsUpdates = document.getElementById('helfi-toc-table-of-contents-news-updates'); - const tableOfContentsList = document.querySelector('#helfi-toc-table-of-contents-list > ul'); - const mainContent = document.querySelector('main.layout-main-wrapper'); - const reservedElems = document.querySelectorAll('[id]'); - const reserved = []; // Let's list current id's here to avoid creating duplicates - reservedElems.forEach(function (elem) { - reserved.push(elem.id); - }); - - // Exclude elements from TOC that are not content: - // e.g. TOC, sidebar, cookie compliency-banner etc. - let exclusions = - '' + - ':not(.layout-sidebar-first *)' + - ':not(.layout-sidebar-second *)' + - ':not(.tools__container *)' + - ':not(.breadcrumb__container *)' + - ':not(#helfi-toc-table-of-contents *)' + - ':not(.embedded-content-cookie-compliance *)' + - ':not(.react-and-share-cookie-compliance *)'; - - // On a updating news page we need to ignore everything else other than the news update container. - if (tableOfContentsNewsUpdates) { - exclusions += - ':not(.components--upper *)' + - ':not(.block--news-of-interest *)' + - ':not(#helfi-toc-table-of-contents-news-updates *)'; - } +((Drupal, once, drupalSettings) => { + + // Global table of contents object. + Drupal.tableOfContents = { + + // List of reserved ids. + reservedIds: [], + + // List of anchors. + anchors: [], + + // Exclude elements from TOC that are not content: + // e.g. TOC, sidebar, cookie compliance banner etc. + exclusions: () => { + return '' + + ':not(.layout-sidebar-first *)' + + ':not(.layout-sidebar-second *)' + + ':not(.tools__container *)' + + ':not(.breadcrumb__container *)' + + ':not(#helfi-toc-table-of-contents *)' + + ':not(.embedded-content-cookie-compliance *)' + + ':not(.react-and-share-cookie-compliance *)' + }, - const titleComponents = [ + // List of heading tags with exclusions. + titleComponents: (exclusions = Drupal.tableOfContents.exclusions()) => { + return [ `h2${exclusions}`, `h3${exclusions}`, `h4${exclusions}`, `h5${exclusions}`, `h6${exclusions}`, ]; + }, - const mainLanguages = [ - 'en', - 'fi', - 'sv', - ]; + // Find available ID for the anchor link. + findAvailableId: (name, count) => { + let newName = name; + + // Add postfix to the name if heading is not unique. + if (count > 0) { + newName += `-${count}`; + } + + if (Drupal.tableOfContents.reservedIds.includes(newName)) { + return Drupal.tableOfContents.findAvailableId(name, count + 1); + } - const swaps = { + if (Drupal.tableOfContents.anchors.includes(newName)) { + // When reserved heading is visible on page, lets start counting from 2 instead of 1 + if (count === 0) { + count += 1; + } + return Drupal.tableOfContents.findAvailableId(name,count + 1); + } + return newName; + }, + + // Main languages. + mainLanguages: () => { + return ['en', 'fi', 'sv']; + }, + + // Locale conversions. + localeConversions: () => { + return { '0': '[°₀۰0]', '1': '[¹₁۱1]', '2': '[²₂۲2]', @@ -82,110 +77,148 @@ '8': '[⁸₈۸8]', '9': '[⁹₉۹9]', 'a': '[àáảãạăắằẳẵặâấầẩẫậāąåαάἀἁἂἃἄἅἆἇᾀᾁᾂᾃᾄᾅᾆᾇὰᾰᾱᾲᾳᾴᾶᾷаأအာါǻǎªაअاaä]', + 'aa': '[عआآ]', + 'ae': '[æǽ]', + 'ai': '[ऐ]', 'b': '[бβبဗბbब]', 'c': '[çćčĉċc©]', + 'ch': '[чჩჭچ]', 'd': '[ďðđƌȡɖɗᵭᶁᶑдδدضဍဒდdᴅᴆ]', + 'dj': '[ђđ]', + 'dz': '[џძ]', 'e': '[éèẻẽẹêếềểễệëēęěĕėεέἐἑἒἓἔἕὲеёэєəဧေဲეएإئe]', + 'ei': '[ऍ]', 'f': '[фφفƒფf]', 'g': '[ĝğġģгґγဂგگg]', + 'gh': '[غღ]', + 'gx': '[ĝ]', 'h': '[ĥħηήحهဟှჰh]', + 'hx': '[ĥ]', 'i': '[íìỉĩịîïīĭįıιίϊΐἰἱἲἳἴἵἶἷὶῐῑῒῖῗіїиဣိီည်ǐიइیii̇ϒ]', + 'ii': '[ई]', + 'ij': '[ij]', 'j': '[ĵјჯجj]', + 'jx': '[ĵ]', 'k': '[ķĸкκقكကკქکk]', + 'kh': '[хخხ]', 'l': '[łľĺļŀлλلလლlल]', + 'lj': '[љ]', 'm': '[мμمမმm]', 'n': '[ñńňņʼnŋνнنနნn]', - 'o': '[óòỏõọôốồổỗộơớờởỡợøōőŏοὀὁὂὃὄὅὸόоوθိုǒǿºოओoöө]', - 'p': '[пπပპپp]', - 'q': '[ყq]', - 'r': '[ŕřŗрρرრr]', - 's': '[śšşсσșςسصစſსsŝ]', - 't': '[ťţтτțتطဋတŧთტt]', - 'u': '[úùủũụưứừửữựûūůűŭųµуဉုူǔǖǘǚǜუउuўü]', - 'v': '[вვϐv]', - 'w': '[ŵωώဝွw]', - 'x': '[χξx]', - 'y': '[ýỳỷỹỵÿŷйыυϋύΰيယyῠῡὺ]', - 'z': '[źžżзζزဇზz]', - 'aa': '[عआآ]', - 'ae': '[æǽ]', - 'ai': '[ऐ]', - 'ch': '[чჩჭچ]', - 'dj': '[ђđ]', - 'dz': '[џძ]', - 'ei': '[ऍ]', - 'gh': '[غღ]', - 'ii': '[ई]', - 'ij': '[ij]', - 'kh': '[хخხ]', - 'lj': '[љ]', 'nj': '[њ]', + 'o': '[óòỏõọôốồổỗộơớờởỡợøōőŏοὀὁὂὃὄὅὸόоوθိုǒǿºოओoöө]', 'oe': '[öœؤ]', 'oi': '[ऑ]', 'oii': '[ऒ]', + 'p': '[пπပპپp]', 'ps': '[ψ]', + 'q': '[ყq]', + 'r': '[ŕřŗрρرრr]', + 's': '[śšşсσșςسصစſსsŝ]', 'sh': '[шშش]', 'shch': '[щ]', 'ss': '[ß]', 'sx': '[ŝ]', + 't': '[ťţтτțتطဋတŧთტt]', 'th': '[þϑثذظ]', 'ts': '[цცწ]', + 'u': '[úùủũụưứừửữựûūůűŭųµуဉုူǔǖǘǚǜუउuўü]', 'ue': '[ü]', 'uu': '[ऊ]', + 'v': '[вვϐv]', + 'w': '[ŵωώဝွw]', + 'x': '[χξx]', + 'y': '[ýỳỷỹỵÿŷйыυϋύΰيယyῠῡὺ]', 'ya': '[я]', 'yu': '[ю]', + 'z': '[źžżзζزဇზz]', 'zh': '[жჟژ]', - 'gx': '[ĝ]', - 'hx': '[ĥ]', - 'jx': '[ĵ]', }; + }, - // Craft table of contents. - once('table-of-contents', titleComponents.join(','), mainContent) - .forEach(function (content) { - let name = content.textContent - .toLowerCase() - .trim(); - - // To ensure backwards compatibility, this is done only to "other" languages. - if (!mainLanguages.includes(drupalSettings.path.currentLanguage)) { - Object.keys(swaps).forEach((swap) => { - name = name.replace(new RegExp(swaps[swap], 'g'), swap); - }); - } - else { - name = name - .replace(/ä/gi, 'a') - .replace(/ö/gi, 'o') - .replace(/å/gi, 'a'); - } + // A function to create table of content elements. + createTableOfContentElements: (content) => { + // Remove loading text and noscript element. + let name = content.textContent + .toLowerCase() + .trim(); + + // To ensure backwards compatibility, this is done only to "other" languages. + if (!Drupal.tableOfContents.mainLanguages().includes(drupalSettings.path.currentLanguage)) { + Object.keys(Drupal.tableOfContents.localeConversions()).forEach((swap) => { + name = name.replace(new RegExp(Drupal.tableOfContents.localeConversions()[swap], 'g'), swap); + }); + } + else { + name = name + .replace(/ä/gi, 'a') + .replace(/ö/gi, 'o') + .replace(/å/gi, 'a'); + } - name = name - // Replace any remaining non-word character including whitespace with '-'. - // This leaves only characters matching [A-Za-z0-9-_] to the name. - .replace(/\W/g, '-') - // Use underscore at the end of the string: 'example-1' -> 'example_1'. - .replace(/-(\d+)$/g, '_$1'); + name = name + // Replace any remaining non-word character including whitespace with '-'. + // This leaves only characters matching [A-Za-z0-9-_] to the name. + .replace(/\W/g, '-') + // Use underscore at the end of the string: 'example-1' -> 'example_1'. + .replace(/-(\d+)$/g, '_$1'); - let nodeName = content.nodeName.toLowerCase(); - if (nodeName === 'button') { - nodeName = content.parentElement.nodeName.toLowerCase(); - } + let nodeName = content.nodeName.toLowerCase(); + if (nodeName === 'button') { + nodeName = content.parentElement.nodeName.toLowerCase(); + } - const anchorName = content.id - ? content.id - : findAvailableId(name, reserved, anchors, 0); + const anchorName = content.id + ? content.id + : Drupal.tableOfContents.findAvailableId(name, 0); - anchors.push(anchorName); + Drupal.tableOfContents.anchors.push(anchorName); - // On updating news there is published date under the title that we want to display in the - // table of contents news update version. For normal table of contents this remains empty. - let contentPublishDate = ''; + // Create anchor links. + content.setAttribute('id', anchorName); + content.setAttribute('tabindex', '-1'); // Set tabindex to -1 to avoid issues with screen readers. - if (tableOfContentsNewsUpdates && content.nextSibling && content.nextElementSibling.nodeName === 'TIME') { - let contentPublishDateStamp = new Date(content.nextElementSibling.dateTime); - contentPublishDate = `${contentPublishDateStamp.getDate()}.${contentPublishDateStamp.getMonth() + 1}.${contentPublishDateStamp.getFullYear()}`; - } + return { + nodeName: nodeName, + anchorName: anchorName, + } + }, + + // A function to reveal table of contents. + updateTOC: (tocElement) => { + // Remove loading text and noscript element. + const removeElements = tocElement.parentElement.querySelectorAll('.js-remove'); + removeElements.forEach(function (element) { + element.remove(); + }); + + // Update toc visible. + tocElement.setAttribute('data-js', 'true'); + }, + } + + // Attach table of contents. + Drupal.behaviors.tableOfContents = { + attach: function attach() { + const tableOfContents = document.getElementById('helfi-toc-table-of-contents'); + + // Bail if table of contents is not enabled. + if (!tableOfContents) { + return; + } + + const tableOfContentsList = document.querySelector('#helfi-toc-table-of-contents-list > ul'); + const mainContent = document.querySelector('main.layout-main-wrapper'); + const reservedElems = document.querySelectorAll('[id]'); + reservedElems.forEach(function (elem) { + Drupal.tableOfContents.reservedIds.push(elem.id); + }); + + // Craft table of contents. + once('table-of-contents', Drupal.tableOfContents.titleComponents().join(','), mainContent) + .forEach((content) => { + + const { nodeName, anchorName} = Drupal.tableOfContents.createTableOfContentElements(content, []); // Create table of contents if component is enabled. if (tableOfContentsList && nodeName === 'h2') { @@ -197,41 +230,15 @@ link.href = `#${anchorName}`; link.textContent = content.textContent.trim(); - // Add content publish date and its wrapper to list items only if they exist. - if (contentPublishDate) { - let publishDate = document.createElement('time'); - publishDate.dateTime = content.nextElementSibling.dateTime; - publishDate.textContent = contentPublishDate; - - listItem.appendChild(publishDate); - } - listItem.appendChild(link); tableOfContentsList.appendChild(listItem); } - // Create anchor links. - content.setAttribute('id', anchorName); - content.setAttribute('tabindex', '-1'); // Set tabindex to -1 to avoid issues with screen readers. - }); - - function updateTOC(tocElement) { - // Remove loading text and noscript element. - const removeElements = tocElement.parentElement.querySelectorAll('.js-remove'); - removeElements.forEach(function (element) { - element.remove(); - }); - - // Update toc visible. - tocElement.setAttribute('data-js', 'true'); - } + } + ); - // Select which type of TOC should be displayed. - if (tableOfContentsNewsUpdates) { - updateTOC(tableOfContentsNewsUpdates); - } else { - updateTOC(tableOfContents); + if (tableOfContents) { + Drupal.tableOfContents.updateTOC(tableOfContents); } - }, }; })(Drupal, once, drupalSettings);