chore: ♻️ remove jquery (#1964)

* add scroll-to-top and scroll class toggle with vanilla JS

* add language detection

* add  i18n

* Fix bug: there was no class hidden in CSS. Notice was hidden by hide(). Now HTML hidden attribute is used to hide notice using CSS. This improves A11y.

* fix flashing of i18n notice and improve a11y

* add menu highlighting on scroll

* ♻️ remove jquery

* defer app.js

Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>

* remove DOMContentLoaded and fix root margin to 25%

---------

Co-authored-by: bjohansebas <103585995+bjohansebas@users.noreply.github.com>
This commit is contained in:
shubham oulkar
2025-08-02 06:17:30 +05:30
committed by GitHub
parent 4c0838e636
commit 90e5a311da
6 changed files with 122 additions and 118 deletions

View File

@@ -64,8 +64,7 @@
{% else %}
<meta property="twitter:image" content="https://expressjs.com/images/og.png">
{% endif %}
<script data-cfasync="false" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script data-cfasync="false" src="/js/app.js"></script>
<script data-cfasync="false" defer src="/js/app.js"></script>
<script data-cfasync="false" defer src="/js/menu.js"></script>
<script data-cfasync="false" defer src="/js/copycode.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />

View File

@@ -7,4 +7,4 @@
{% assign notice_link_text = site.data.en.general.i18n_notice_link_text %}
{% endif %}
<p>{{ notice }} <a href="{{ page.url | replace: lang_path, '/en/' }}">{{ notice_link_text}}</a>.</p>
<div id="close-i18n-notice-box" title="Close"></div>
<div id="close-i18n-notice-box" title="Close" role="button" aria-label="Close notice" tabindex="0"></div>

View File

@@ -14,8 +14,8 @@
<div id="overlay"></div>
{% if page.lang != 'en' %}
<div id="i18n-notice-box" class="doc-notice">
{% include i18n-notice.html %}
<div id="i18n-notice-box" class="doc-notice" hidden>
{% include i18n-notice.html %}
</div>
{% endif %}

View File

@@ -15,7 +15,7 @@
<div id="overlay"></div>
{% if page.lang != 'en' %}
<div id="i18n-notice-box" class="doc-notice">
<div id="i18n-notice-box" class="doc-notice" hidden>
{% include i18n-notice.html %}
</div>
{% endif %}

View File

@@ -474,7 +474,7 @@ pre:has(code) button.failed {
}
}
/* top button */
/* scroll to top button */
.scroll #top {
opacity: .5;
@@ -611,6 +611,7 @@ table ul {
font-weight: 600;
}
/* i18n box */
.doc-notice {
padding-block: 1rem;
padding-inline: 2.5rem;
@@ -618,6 +619,11 @@ table ul {
border-radius: 0 6px 6px 0;
background: var(--notice-bg);
border-left: 3px solid var(--notice-accent);
margin-inline: auto;
margin-block-start: 2rem;
position: relative;
grid-area: i18n;
display: block;
}
.doc-notice a{
@@ -625,6 +631,10 @@ table ul {
text-decoration: underline;
}
.doc-notice[hidden] {
display: none;
}
.doc-info {
background: var(--info-bg);
border-left: 3px solid var(--info-accent);
@@ -645,13 +655,6 @@ table ul {
text-decoration: underline;
}
#i18n-notice-box {
margin-inline: auto;
margin-block-start: 2rem;
position: relative;
grid-area: i18n;
}
#close-i18n-notice-box {
position: absolute;
top: 3px;

210
js/app.js
View File

@@ -1,122 +1,124 @@
$(function(){
var doc = $(document);
const languageElement = document.getElementById('languageData');
const languagesData = languageElement ? JSON.parse(languageElement.dataset.languages) : [];
const langDisplay = document.getElementById('current-lang');
const i18nMsgBox = document.getElementById("i18n-notice-box");
const scrollToTopBtn = document.getElementById("top");
// top link
$('#top').click(function(e){
$('html, body').animate({scrollTop : 0}, 500);
return false;
});
// display current language in language picker component
if (langDisplay) {
const currentLanguage = window.location.pathname.split('/')[1];
const matchedLang = languagesData.find(lang => lang.code === currentLanguage);
langDisplay.textContent = matchedLang ? matchedLang.name : 'English';
}
// scrolling links
var added;
doc.scroll(function(e){
if (doc.scrollTop() > 5) {
if (added) return;
added = true;
$('body').addClass('scroll');
} else {
$('body').removeClass('scroll');
added = false;
}
})
// menu bar
var headings = $('h2, h3').map(function(i, el){
return {
top: $(el).offset().top - 200,
id: el.id
}
});
function closest() {
var h;
var top = $(window).scrollTop();
var i = headings.length;
while (i--) {
h = headings[i];
if (top >= h.top) return h;
}
}
var currentApiPrefix;
var parentMenuSelector;
var lastApiPrefix;
if (document.readyState !== 'loading') {
const languageElement = document.getElementById('languageData');
const languagesData = languageElement ? JSON.parse(languageElement.dataset.languages) : [];
const langDisplay = document.getElementById('current-lang');
if (langDisplay) {
const currentLanguage = window.location.pathname.split('/')[1];
const matchedLang = languagesData.find(lang => lang.code === currentLanguage);
langDisplay.textContent = matchedLang ? matchedLang.name : 'English';
}
}
$(document).scroll(function() {
var h = closest();
if (!h) return;
currentApiPrefix = h.id.split('.')[0];
parentMenuSelector = '#'+ currentApiPrefix + '-menu';
$(parentMenuSelector).addClass('active');
if (lastApiPrefix && (lastApiPrefix != currentApiPrefix)) {
$('#'+ lastApiPrefix + '-menu').removeClass('active');
}
$('#menu li a').removeClass('active');
var a = $('a[href="#' + h.id + '"]');
a.addClass('active');
lastApiPrefix = currentApiPrefix.split('.')[0];
})
// i18n notice
if (readCookie('i18nClose')) {
$('#i18n-notice-box').hide();
$("#i18n-notice-box").addClass("hidden");
}
else {
$('#close-i18n-notice-box').on('click', function () {
$('#i18n-notice-box').hide();
$("#i18n-notice-box").addClass("hidden");
createCookie('i18nClose', 1);
// scroll to top of the page
if (scrollToTopBtn) {
scrollToTopBtn.addEventListener("click", function (e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth"
})
})
}
// add/remove class 'scroll' on scroll by 5px
const scrollTarget = document.querySelector('.logo-container');
const scrollObserver = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) {
document.body.classList.add('scroll');
} else {
document.body.classList.remove('scroll');
}
},
{
root: null,
threshold: 0,
rootMargin: '0px 0px 0px 0px'
}
})
);
if (scrollTarget) scrollObserver.observe(scrollTarget);
// heighlight current Menu on scroll
const headings = Array.from(document.querySelectorAll("h2, h3"));
const menuLinks = document.querySelectorAll("#menu li a");
const observerOptions = {
root: null,
rootMargin: "-10% 0px -65% 0px",
threshold: 1,
};
const menuObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const currentApiPrefix = entry.target.id.split(".")[0];
const parentMenuSelector = `#${currentApiPrefix}-menu`;
const parentMenuEl = document.querySelector(parentMenuSelector);
// open submenu on scroll
if (parentMenuEl) parentMenuEl.classList.add("active");
// Remove active class from last menu item
const lastActiveMenu = document.querySelector(".active[id$='-menu']");
if (lastActiveMenu && lastActiveMenu.id !== parentMenuEl.id) {
lastActiveMenu.classList.remove("active");
}
// Update active link
menuLinks.forEach((link) => link.classList.remove("active"));
const activeLink = document.querySelector(`a[href="#${entry.target.id}"]`);
if (activeLink && !activeLink.classList.contains("active")) activeLink.classList.add("active");
}
});
}, observerOptions);
headings.forEach((heading) => menuObserver.observe(heading));
// i18n message box : this box appears hidden for all page.lang != 'en'
const isI18nCookie = readCookie('i18nClose');
if (i18nMsgBox && !isI18nCookie) {
const closeI18nBtn = document.getElementById("close-i18n-notice-box");
// show notice box
i18nMsgBox.hidden = false;
// close notice box
if (closeI18nBtn) {
closeI18nBtn.addEventListener("click", () => {
// hide notice
i18nMsgBox.hidden = true;
// set session cookie
createCookie('i18nClose', 1);
});
// keyboard a11y
closeI18nBtn.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
closeI18nBtn.click();
}
});
}
};
function createCookie(name, value, days) {
var expires;
let expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
} else {
expires = "";
const date = new Date();
date.setTime(date.getTime() + (days * 864e5));
expires = "; expires=" + date.toUTCString();
}
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}${expires}; path=/; SameSite=Lax; Secure`;
}
function readCookie(name) {
var nameEQ = encodeURIComponent(name) + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
const nameEQ = encodeURIComponent(name) + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}
function eraseCookie(name) {
createCookie(name, "", -1);
}