Subject: Re: bug#69132: [ELPA] Remove jQuery from elpa.gnu.org

From: Philip Kaludercic <[email-masked]>
Date: Thu, 15 Feb 2024 11:07:59 +0000

Philip Kaludercic <[email-masked]> writes:

> Stefan Monnier <[email-masked]> writes:
>
>>> I was recently surprised to see that elpa.gnu.org uses a jQuery library,
>>> where it really isn't necessary.  Re-implementing the same functionality
>>> can be done in a few more lines of plain Javascript, without the need
>>> for any minified code.  Tested with relatively recent versions of
>>> Firefox and Chromium, so perhaps it would be nice if someone with an
>>> older browser could check if I didn't make any bold assumptions. 
>>
>> Works for me, but I'm not using an "older browser" 🙁
>
> I have a few older machines that I could try it out on.
>
>>> diff --git a/html/javascript/package-search.js b/html/javascript/package-search.js
>>> index 47134045eb..92f29703a3 100644
>>> --- a/html/javascript/package-search.js
>>> +++ b/html/javascript/package-search.js
>>> @@ -1,6 +1,35 @@
>>> -$(document).ready(function() {
>>> -	$('table').filterTable({
>>> -		label: '',
>>> -		placeholder: 'Search packages...'
>>> -	});
>>> +window.addEventListener("load", function (event) {
>>> +	const table = document.getElementById("packages");
>>> +
>>> +	const search = document.createElement("input");
>>> +	search.setAttribute("placeholder", "Search packages...");
>>> +	search.setAttribute("type", "search");
>> [...]
>>
>> I know very little about Javascript, but I'm curious: can't we add the
>> `input` element directly in the HTML file, and skip those first 4 lines or so?
>
> Of course, I just replicated what the jquery plugin did.  I suppose the
> idea was to not have a broken search field labelled "Search
> packages...", if there is no Javascript to make it work in the first
> place.
>
>>> diff --git a/html/layout.css b/html/layout.css
>>> index 4f5c50d393..434220f8c1 100644
>>> --- a/html/layout.css
>>> +++ b/html/layout.css
>>
>> A rationale/description of this part of the change would be welcome.
>>
>>> From 73a9335634f54b71e9723047f327cf782cfb7244 Mon Sep 17 00:00:00 2001
>>> From: Philip Kaludercic <[email-masked]>
>>> Date: Wed, 14 Feb 2024 20:37:37 +0100
>>> Subject: [PATCH] Update HTML generation for new filter implementation
>>>
>>> ---
>>>  elpa-admin.el | 27 +++++++++++----------------
>>>  1 file changed, 11 insertions(+), 16 deletions(-)
>>
>> Same here.
>
> The idea is just to not use CSS classes to pinpoint specific Elements in
> the DOM.  That is what IDs are for (ideally the "ID -> elements" map
> should be injective).  I needed that to use document.getElementById in
> the new script.
>
> Should I update the commit messages to explain the change?

I have updated patches with a few more cases that I had initially
forgotten and split the one for the main branch into two:

>From 96fee0fc402559401a376b237ebe65bd9710255b Mon Sep 17 00:00:00 2001
From: Philip Kaludercic <[email-masked]>
Date: Thu, 15 Feb 2024 11:43:38 +0100
Subject: [PATCH] Generate HTML5 tags instead of <div>s with custom classes

---
 elpa-admin.el | 27 +++++++++++----------------
 1 file changed, 11 insertions(+), 16 deletions(-)

diff --git a/elpa-admin.el b/elpa-admin.el
index 7831c794ae..f166c6f137 100644
--- a/elpa-admin.el
+++ b/elpa-admin.el
@@ -1553,24 +1553,19 @@ readme file has an unconventional name"
 
         <div class=\"wrapper\">
 
-            <div class=\"header small\">
-                <div class=\"container\">
+            <header class=\"small\">
                     <h1>%s</h1>
-                </div>
-            </div>
+            </header>
 
-            <div class=\"container\">\n"
+            <main>\n"
           title (or head-extra "") (or header title)))
 
 (defvar elpaa--index-javascript-headers "
-        <script src=\"../javascript/jquery.min.js\" type=\"text/javascript\"></script>
-        <script src=\"../javascript/jquery.filtertable.min.js\" type=\"text/javascript\"></script>
         <script src=\"../javascript/package-search.js\" type=\"text/javascript\"></script>")
 
 (defun elpaa--html-footer ()
   (format "\n
-        <div class=\"footer\">
-            <div class=\"container\">
+        <footer>
                 <p>Last refreshed on %s</p>
                 <p>Copyright 2016-%s <a href=\"https://fsf.org\">Free Software Foundation</a>, Inc.</p>
                 <p>Design provided by <a href=\"https://nicolas.petton.fr\">Nicolas Petton</a></p>
@@ -1580,8 +1575,7 @@ readme file has an unconventional name"
                   International License.
                 </p>
                 <p><a href=\"/jslicense.html\" data-jslicense=\"1\">JavaScript Licenses</a></p>
-            </div>
-        </div>
+        </footer>
 
 </body>\n"
           (format-time-string "%Y-%b-%d %R %Z" nil t)
@@ -1950,7 +1944,7 @@ arbitrary code."
                   "<div class=\"splice news\">\n"
                   (elpaa--section-to-html news)
                   "\n</div>\n")))
-      (insert "</div>\n")
+      (insert "</main>\n")
       (insert (elpaa--html-footer))
       (write-region (point-min) (point-max) (concat name ".html")))))
 
@@ -1959,15 +1953,16 @@ arbitrary code."
     (insert (elpaa--html-header
              (concat elpaa--name " ELPA Packages")
              nil elpaa--index-javascript-headers))
-    (insert "<table>\n")
-    (insert "<tr><th>Package</th><th>Version</th><th>Description</th></tr>\n")
+    (insert "<table id=\"packages\">\n")
+    (insert "<thead><tr><th>Package</th><th>Version</th><th>Description</th></tr></thead>\n")
+    (insert "<tbody>")
     (dolist (pkg pkgs)
       (insert (format "<tr><td><a href=\"%s.html\">%s</a></td><td>%s</td><td>%s</td></tr>\n"
                       (car pkg) (car pkg)
                       (package-version-join (aref (cdr pkg) 0))
                       (aref (cdr pkg) 2))))
-    (insert "                </table>
-            </div>
+    (insert "</tbody></table>
+            </main>
             <div class=\"push\"></div>
         </div>")
     (insert (elpaa--html-footer))
-- 
2.43.0

>From c84ce797e483acfb4327a18bfc3680d54806e4dd Mon Sep 17 00:00:00 2001
From: Philip Kaludercic <[email-masked]>
Date: Thu, 15 Feb 2024 11:42:06 +0100
Subject: [PATCH 1/2] Use more HTML5 tags instead of custom CSS classes

---
 html/layout.css | 92 ++++++++++++-------------------------------------
 1 file changed, 22 insertions(+), 70 deletions(-)

diff --git a/html/layout.css b/html/layout.css
index 4f5c50d393..434220f8c1 100644
--- a/html/layout.css
+++ b/html/layout.css
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010-2022 Free Software Foundation, Inc.
+/* Copyright (C) 2010-2022, 2024 Free Software Foundation, Inc.
 
 This file is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -10,71 +10,23 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details. */
 
-/* Begin of reset */
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed, 
-figure, figcaption, footer, header, hgroup, 
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-  margin: 0;
-  padding: 0;
-  border: 0;
-  font-size: 100%;
-  font: inherit;
-  vertical-align: baseline;
-}
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure, 
-footer, header, hgroup, menu, nav, section {
-  display: block;
-}
 body {
-  line-height: 1;
-}
-ol, ul {
-  list-style: none;
-}
-blockquote, q {
-  quotes: none;
-}
-blockquote:before, blockquote:after,
-q:before, q:after {
-  content: '';
-  content: none;
-}
-table {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-/* End of reset */
-
-body {
-  font-family: "Fira Sans", sans-serif;
+  font-family: system-ui, sans-serif;
   line-height: 1.5em;
   font-size: 1.2em;
   color: #333;
 }
 
 html, body {
-  height: 100%;
+  height: 100vw;
 }
-.wrapper {
+#wrapper {
   min-height: 100%;
   height: auto !important; /* This line and the next line are not necessary unless you need IE6 support */
   height: 100%;
   margin: 0 auto -50px; /* the bottom margin is the negative value of the footer's height */
 }
-.footer, .push {
+footer, .push {
   height: 90px; /* .push must be the same height as .footer */
 }
 
@@ -196,13 +148,13 @@ dd {
   font-size: 1.2em;
 }
 
-.container {
+header, main, footer {
   padding: 20px;
   width: 960px;
   margin: 0 auto;
 }
 
-.header {
+header {
   color: white;
   text-align: center;
   background-color: #bb3955;
@@ -210,27 +162,27 @@ dd {
   border-bottom: 6px solid #691240;
 }
 
-.header p {
+header p {
   margin: 0;
 }
 
-.header a {
+header a {
   color: rgb(240, 255, 135);
   text-decoration: none;
 }
 
-.header a:hover {
+header a:hover {
   color: rgb(240, 255, 135);
   text-decoration: underline;
   background: transparent;
 }
 
-.header.small {
+header.small {
   text-align: left;
   border-width: 3px;
 }
 
-.header.small h1 {
+header.small h1 {
   display: inline;
   font-size: 1.5em;
   margin: 0;
@@ -239,7 +191,7 @@ dd {
   padding-left: 120px;
 }
 
-.header.small .container {
+header.small .container {
   padding: 20px;
 }
 
@@ -260,9 +212,13 @@ input[type="search"][placeholder] {
   color: #666;
 }
 
-.filter-table .quick { margin-left: 0.5em; font-size: 0.8em; text-decoration: none; }
-.fitler-table .quick:hover { text-decoration: underline; }
-td.alt { background-color: #ffc; background-color: rgba(255, 255, 0, 0.2); }
+tr.invisible {
+    display: none;
+}
+td.alt {
+    background-color: #ffc;
+    background-color: rgba(255, 255, 0, 0.2);
+}
 
 td, th {
   padding: 6px;
@@ -279,7 +235,7 @@ td {
   border-bottom: 1px solid #ddd;
 }
 
-.footer {
+footer {
   border-top: 1px solid #999;
   text-align:center;
   font-size: 0.9em;
@@ -287,11 +243,7 @@ td {
   color: #666;
 }
 
-.footer .container {
-  padding: 15px 0;
-}
-
-.footer p {
+footer p {
   margin: 0;
   font-size: .8em;
   padding: 0;
-- 
2.43.0

>From 298239daf136896872dd79e0357ae3d7caf40c41 Mon Sep 17 00:00:00 2001
From: Philip Kaludercic <[email-masked]>
Date: Thu, 15 Feb 2024 11:42:12 +0100
Subject: [PATCH 2/2] Replace jQuery filtertable script with plain Javascript

---
 html/javascript/jquery.filtertable.min.js | 13 --------
 html/javascript/jquery.min.js             |  5 ---
 html/javascript/package-search.js         | 39 ++++++++++++++++++++---
 3 files changed, 34 insertions(+), 23 deletions(-)
 delete mode 100644 html/javascript/jquery.filtertable.min.js
 delete mode 100644 html/javascript/jquery.min.js

diff --git a/html/javascript/jquery.filtertable.min.js b/html/javascript/jquery.filtertable.min.js
deleted file mode 100644
index b9e243d596..0000000000
--- a/html/javascript/jquery.filtertable.min.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * jquery.filterTable
- *
- * This plugin will add a search filter to tables. When typing in the filter,
- * any rows that do not contain the filter will be hidden.
- *
- * Utilizes bindWithDelay() if available. https://github.com/bgrins/bindWithDelay
- *
- * @version v1.5.7
- * @author Sunny Walker, [email-masked]
- * @license MIT
- */
\ No newline at end of file
diff --git a/html/javascript/jquery.min.js b/html/javascript/jquery.min.js
deleted file mode 100644
index e836475870..0000000000
--- a/html/javascript/jquery.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */
diff --git a/html/javascript/package-search.js b/html/javascript/package-search.js
index 47134045eb..92f29703a3 100644
--- a/html/javascript/package-search.js
+++ b/html/javascript/package-search.js
@@ -1,6 +1,35 @@
-$(document).ready(function() {
-	$('table').filterTable({
-		label: '',
-		placeholder: 'Search packages...'
-	});
+window.addEventListener("load", function (event) {
+	const table = document.getElementById("packages");
+
+	const search = document.createElement("input");
+	search.setAttribute("placeholder", "Search packages...");
+	search.setAttribute("type", "search");
+	search.addEventListener("input", function(event) {
+		const query = event.target.value;
+
+		for (let i = 1; i < table.rows.length; i++) {
+			const row = table.rows.item(i);
+			row.classList.remove("invisible");
+
+			const name = row.childNodes.item(0);
+			const name_match_p = name.innerText.match(query);
+			name.classList.remove("alt");
+
+			const desc = row.childNodes.item(2);
+			const desc_match_p = desc.innerText.match(query);
+			desc.classList.remove("alt");
+
+			if (query) { // avoid matching the empty string
+				if (name_match_p || desc_match_p) {
+					if (name_match_p) { name.classList.add("alt"); }
+					if (desc_match_p) { desc.classList.add("alt"); }
+				} else {
+					row.classList.add("invisible");
+				}
+			}
+		}
+	})
+
+	const main = document.querySelector("main");
+	main.prepend(search);
 });
-- 
2.43.0