Skip to content

Commit 55bd03a

Browse files
authored
Merge pull request #132 from w3c/feature/#469-heading-anchors
Feature/#469 heading anchors
2 parents db5cb50 + 2574196 commit 55bd03a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1901
-1567
lines changed

assets-src/js/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {formErrorSummary} from "./main/form-error-summary";
66
import {navigation} from "./main/navigation";
77
import {responsiveTables} from "./main/responsive-tables";
88
import {flashes} from "./main/flashes";
9+
import {headingAnchors} from './main/heading-anchors';
910

1011
function domLoadedActions() {
1112
accountMenu();
@@ -15,6 +16,7 @@ function domLoadedActions() {
1516
formErrorSummary();
1617
responsiveTables();
1718
flashes();
19+
headingAnchors();
1820

1921
/* Create a navDoubleLevel object and initiate double-level navigation for a <ul> with the correct data-component attribute */
2022
const navDoubleIntro = document.querySelector('ul[data-component="nav-double-intro"]');
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Add anchor links to any H2 - H6 within the <main> element that:
3+
* - are not children of a <nav> element
4+
* - do not have an ancestor with the data-anchor="no" attribute, with
5+
* the `hidden` attribute or with the .visuallyhidden class
6+
* - do not themselves have the data-anchor="no" attribute, the `hidden`
7+
* attribute or the .visuallyhidden class
8+
*
9+
* If a heading does not already possess an ID, use regular expressions on
10+
* the textContent of the heading to generate a string that is valid to
11+
* use for both the heading ID and the anchor href. Supports non-Latin
12+
* scripts by matching any Unicode letter - \p{L} - or number - \p{N}. The
13+
* u flag enables Unicode matching, to support characters from any script.
14+
*
15+
* Otherwise, generate the anchor using the existing heading ID value.
16+
*/
17+
18+
import {translate} from './translations';
19+
20+
let headingAnchors = function () {
21+
22+
let languageCode = document.documentElement.lang;
23+
24+
// Only add heading anchor links on "full" sites
25+
if (languageCode === 'en' || languageCode === 'ja' || languageCode === 'zh-hans') {
26+
27+
let headingsArray= Array.from(document.querySelectorAll('main h2, main h3, main h4, main h5, main h6'));
28+
29+
if (headingsArray.length > 0) {
30+
31+
// Filter out headings that:
32+
// - Are not children of <nav>
33+
// - Do not have an ancestor with the data-anchor="no" attribute
34+
// - Do not have an ancestor with the `hidden` attribute
35+
// - Do not have an ancestor with the `.visuallyhidden` class
36+
// - Do not themselves have the data-anchor="no" attribute
37+
// - Do not themselves have the `hidden` attribute
38+
// - Do not themselves have the `.visuallyhidden` class
39+
let targetedHeadings = headingsArray.filter(function(heading) {
40+
let insideNav = heading.closest('nav') !== null;
41+
let parentHasDataAttribute = heading.closest('[data-anchor="no"]') !== null;
42+
let hiddenParent = heading.closest('[hidden]') !== null;
43+
let visuallyhiddenParent = heading.closest('.visuallyhidden') !== null;
44+
let hasDataAttribute = heading.getAttribute('data-anchor') === 'no';
45+
let isHidden = heading.getAttribute('hidden');
46+
let isVisuallyhidden = heading.classList.contains('visuallyhidden');
47+
48+
return !insideNav && !parentHasDataAttribute && !hiddenParent && !visuallyhiddenParent && !hasDataAttribute && !isHidden && !isVisuallyhidden;
49+
});
50+
51+
if (targetedHeadings.length > 0) {
52+
targetedHeadings.forEach(function(heading) {
53+
54+
let anchor = document.createElement('a');
55+
let anchorHref;
56+
57+
// If the heading already has an ID, use this for constructing the anchor
58+
if (heading.getAttribute('id')) {
59+
anchorHref = heading.id;
60+
} else {
61+
// If the heading does not already have an ID, generate anchor href from the heading text. Steps are:
62+
// - Remove leading/trailing spaces
63+
// - Use RegEx to remove invalid characters but keep all Unicode letters/numbers
64+
// - Use RegEx to replace spaces with hyphens
65+
// - convert to lowercase as per URL policy
66+
anchorHref = heading.textContent
67+
.trim()
68+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
69+
.replace(/\s+/g, '-')
70+
.toLowerCase();
71+
heading.id = anchorHref;
72+
}
73+
74+
anchor.setAttribute('href', '#' + anchorHref);
75+
anchor.setAttribute('class', 'heading-anchor');
76+
anchor.innerHTML = '<span aria-hidden="true">&sect;</span>'
77+
+'<span class="visuallyhidden">'
78+
+ translate.translate('anchor', languageCode) + '</span>';
79+
heading.textContent += '\xa0';
80+
heading.appendChild(anchor);
81+
82+
});
83+
}
84+
85+
}
86+
87+
}
88+
89+
}
90+
91+
export {headingAnchors};

assets-src/js/main/translations.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const translate = {
4040
},
4141
'en': {
4242
'admin': 'Admin',
43+
'anchor': 'anchor',
4344
'backToMainMenu': 'Back to main menu',
4445
'cancelReply': 'Cancel reply',
4546
'controlsDescription': 'carousel controls',
@@ -116,6 +117,7 @@ const translate = {
116117
},
117118
'ja': {
118119
'admin': 'アドミン',
120+
'anchor': '__anchor',
119121
'backToMainMenu': 'メインメニューに戻る',
120122
'cancelReply': '返信をキャンセル',
121123
'controlsDescription': 'コントロールの説明',
@@ -154,6 +156,7 @@ const translate = {
154156
},
155157
'zh-hans': {
156158
'admin': '管理',
159+
'anchor': '__anchor',
157160
'backToMainMenu': '返回主目录',
158161
'cancelReply': '取消回复',
159162
'controlsDescription': '轮播图控件',

assets-src/styles/sass/30-base/_links.scss

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ a.with-icon--after {
1818
border: 0;
1919
color: $link-color;
2020
cursor: pointer;
21-
padding-left: rem(2);
22-
padding-right: rem(2);
21+
padding-inline: rem(2);
2322
text-decoration: underline; /* 1 */
2423
text-decoration-skip-ink: auto; // Not supported by Safari
2524
text-underline-offset: 0.25em; // Supported by Safari
@@ -48,4 +47,27 @@ a.with-icon--after {
4847
color: $black;
4948
outline-width: 0; /* 2 */
5049
}
50+
}
51+
52+
.heading-anchor {
53+
@extend a, :not([class]);
54+
55+
color: $storm-gray;
56+
font-weight: normal;
57+
opacity: 0.82;
58+
text-underline-offset: auto;
59+
60+
&:visited {
61+
color: $storm-gray;
62+
}
63+
64+
&:hover {
65+
color: $storm-gray;
66+
opacity: 1;
67+
}
68+
69+
&:focus {
70+
color: $black;
71+
opacity: 1;
72+
}
5173
}

assets-src/styles/sass/30-base/_print.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ footer {
723723
.banner,
724724
#lang-nav,
725725
.logo-link .visuallyhidden,
726+
.heading-anchor,
726727
[data-trigger],
727728
[data-toggle],
728729
#global-nav ul,

design-system-config.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
'Styles' => '/styles/',
1818
'Layouts' => '/layouts/',
1919
'Components' => '/components/',
20-
'Third party plugins' => '/third-party-plugins/',
20+
'Scripts' => '/scripts/',
2121
'Templates' => '/templates/',
2222
],
2323
'templates_path' => 'design-system-templates',

design-system-templates/base.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
{% block breadcrumbs %}
6969
{% endblock %}
7070

71-
<main id="main">
71+
<main id="main" {% block extra_main_attributes %}{% endblock %}>
7272
{% block content %}
7373
{% endblock %}
7474
</main>

design-system-templates/components/activity.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
</div>
3838
</li>
3939
<li>
40-
<div class="card card--event workshop" data-component="card">
40+
<div class="card card--event workshop" data-component="card" data-anchor="no">
4141
<div class="card__text">
4242
<h3 class="card__heading"><a href="path/to/page" class="card__link">How do you want to pay?</a></h3>
4343
<p class="txt-pluto with-icon--before"><img class="icon" src="/dist/assets/svg/calendar.svg" width="15" height="15" alt aria-hidden="true" /> <time datetime="2020-11-15">15 November 2020</time></p>
@@ -46,7 +46,7 @@
4646
</div>
4747
</li>
4848
<li>
49-
<div class="card card--event talk" data-component="card">
49+
<div class="card card--event talk" data-component="card" data-anchor="no">
5050
<div class="card__text">
5151
<h3 class="card__heading"><a href="path/to/page" class="card__link">Cognitive AI - mimicking how we think</a></h3>
5252
<p class="txt-pluto with-icon--before"><img class="icon" src="/dist/assets/svg/calendar.svg" width="15" height="15" alt aria-hidden="true" /> <time datetime="2020-12-20">20 December 2020</time></p>

design-system-templates/components/card-link-multiple.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="event-list">
2-
<div class="card card--event meeting" data-component="card">
2+
<div class="card card--event meeting" data-component="card" data-anchor="no">
33
<div class="card__text">
44
<div class="l-sidebar">
55
<div>

design-system-templates/components/collapsible-containers.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="component--collapsibles" data-component="collapsibles">
1+
<div class="component--collapsibles" data-component="collapsibles" data-anchor="no">
22
<div>
33
<h3 data-heading="collapsibles">AI KR (Artificial Intelligence Knowledge Representation) <span>Community group</span></h3>
44
<div>

0 commit comments

Comments
 (0)