feat: add line numbers to code blocks, enhance search overlay functionality, and improve navbar search visibility
This commit is contained in:
1 parent
fa211d243f
commit
64737582a5
5 files changed
+114
-17
No files matched your search
+54
-4
@@ -1862,6 +1862,22 @@ pre code.hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Line numbers (highlightjs-line-numbers plugin builds a table inside <code>) */
|
||||
.hljs-ln { border-collapse: collapse; }
|
||||
.hljs-ln td { padding: 0; border: 0; vertical-align: top; }
|
||||
.hljs-ln-numbers {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
padding-right: 0.9em !important;
|
||||
color: var(--text-muted);
|
||||
border-right: 1px solid var(--border-color);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.hljs-ln-code { padding-left: 0.9em !important; }
|
||||
.hljs-ln-n::before { content: attr(data-line-number); }
|
||||
|
||||
/* ========================================
|
||||
ANCHOR OFFSET — clear the fixed navbar on ToC / hash jumps
|
||||
Without this, jumping to a heading lands it at y=0, hidden behind the
|
||||
@@ -2277,11 +2293,21 @@ button:focus:not(:focus-visible) {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 1rem 1.2rem;
|
||||
border: none;
|
||||
.search-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0 1.1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-icon { flex: 0 0 auto; color: var(--text-muted); }
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.95rem 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
@@ -2291,6 +2317,30 @@ button:focus:not(:focus-visible) {
|
||||
|
||||
.search-input::placeholder { color: var(--text-muted); }
|
||||
|
||||
.search-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem 1.1rem;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-hint kbd {
|
||||
padding: 0.05em 0.4em;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.95em;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Navbar search button — hidden until JS reveals it (no on-page search in view) */
|
||||
.nav-search { display: none; }
|
||||
.nav-search.is-visible { display: block; }
|
||||
|
||||
.search-results {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
+55
-10
@@ -694,6 +694,9 @@
|
||||
pre.parentNode.classList.contains('code-block')) return;
|
||||
|
||||
var code = pre.querySelector('code');
|
||||
// Capture now — before highlighting/line-numbers rewrite the DOM (the
|
||||
// line-numbers table would otherwise mangle textContent on copy).
|
||||
var sourceText = (code ? code.textContent : pre.textContent) || '';
|
||||
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'code-block';
|
||||
@@ -720,8 +723,7 @@
|
||||
|
||||
var resetTimer = null;
|
||||
btn.addEventListener('click', function () {
|
||||
var text = (code ? code.textContent : pre.textContent) || '';
|
||||
copyText(text).then(function () {
|
||||
copyText(sourceText).then(function () {
|
||||
btn.classList.add('is-copied');
|
||||
btn.innerHTML = SVG_CHECK + '<span class="code-copy-label">' + T.copied + '</span>';
|
||||
btn.setAttribute('aria-label', T.copied);
|
||||
@@ -799,6 +801,17 @@
|
||||
Array.prototype.forEach.call(codes, function (code) {
|
||||
try { window.hljs.highlightElement(code); } catch (e) {}
|
||||
});
|
||||
// Line numbers (self-hosted plugin). Skips single-line blocks by default.
|
||||
var ln = document.createElement('script');
|
||||
ln.src = BASE + 'js/highlightjs-line-numbers.min.js';
|
||||
ln.onload = function () {
|
||||
if (!window.hljs || !window.hljs.lineNumbersBlock) return;
|
||||
Array.prototype.forEach.call(codes, function (code) {
|
||||
if (code.closest && code.closest('.tab-group')) return; // keep tab copy clean
|
||||
try { window.hljs.lineNumbersBlock(code, { singleLine: false }); } catch (e) {}
|
||||
});
|
||||
};
|
||||
document.head.appendChild(ln);
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
@@ -1179,13 +1192,20 @@
|
||||
var el = document.getElementById('blowdit-search-index');
|
||||
if (el) { try { data = JSON.parse(el.textContent) || []; } catch (e) { data = []; } }
|
||||
|
||||
var ICON = '<svg class="search-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" ' +
|
||||
'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ' +
|
||||
'aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>';
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'search-overlay';
|
||||
overlay.innerHTML =
|
||||
'<div class="search-box" role="dialog" aria-modal="true" aria-label="' + T.search + '">' +
|
||||
'<input type="text" class="search-input" autocomplete="off" spellcheck="false" placeholder="' + T.searchPlaceholder + '" aria-label="' + T.search + '">' +
|
||||
'<div class="search-field">' + ICON +
|
||||
'<input type="text" class="search-input" autocomplete="off" spellcheck="false" placeholder="' + T.searchPlaceholder + '" aria-label="' + T.search + '">' +
|
||||
'</div>' +
|
||||
'<ul class="search-results"></ul>' +
|
||||
'<div class="search-empty" hidden>' + T.noResults + '</div>' +
|
||||
'<div class="search-hint"><kbd>↑</kbd><kbd>↓</kbd> <kbd>↵</kbd> <kbd>esc</kbd></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
@@ -1193,13 +1213,12 @@
|
||||
var results = overlay.querySelector('.search-results');
|
||||
var empty = overlay.querySelector('.search-empty');
|
||||
var active = -1;
|
||||
var current = [];
|
||||
|
||||
function open() {
|
||||
overlay.classList.add('is-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
input.value = '';
|
||||
render([]);
|
||||
update();
|
||||
setTimeout(function () { input.focus(); }, 30);
|
||||
}
|
||||
function close() {
|
||||
@@ -1208,13 +1227,12 @@
|
||||
}
|
||||
function isOpen() { return overlay.classList.contains('is-open'); }
|
||||
|
||||
function score(item, q) {
|
||||
function matches(item, q) {
|
||||
var hay = (item.t + ' ' + (item.c || '') + ' ' + (item.g || []).join(' ') + ' ' + (item.d || '')).toLowerCase();
|
||||
return hay.indexOf(q) !== -1;
|
||||
}
|
||||
|
||||
function render(list) {
|
||||
current = list;
|
||||
active = list.length ? 0 : -1;
|
||||
results.innerHTML = '';
|
||||
empty.hidden = !(input.value.trim() && list.length === 0);
|
||||
@@ -1231,14 +1249,14 @@
|
||||
|
||||
function update() {
|
||||
var q = input.value.trim().toLowerCase();
|
||||
if (!q) { render([]); return; }
|
||||
render(data.filter(function (item) { return score(item, q); }).slice(0, 8));
|
||||
if (!q) { render(data.slice(0, 6)); return; } // recent posts as suggestions
|
||||
render(data.filter(function (item) { return matches(item, q); }).slice(0, 8));
|
||||
}
|
||||
|
||||
function move(delta) {
|
||||
var items = results.querySelectorAll('.search-result');
|
||||
if (!items.length) return;
|
||||
items[active] && items[active].classList.remove('is-active');
|
||||
if (items[active]) items[active].classList.remove('is-active');
|
||||
active = (active + delta + items.length) % items.length;
|
||||
items[active].classList.add('is-active');
|
||||
items[active].scrollIntoView({ block: 'nearest' });
|
||||
@@ -1257,10 +1275,37 @@
|
||||
});
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
|
||||
|
||||
// Navbar button(s) open the overlay.
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.js-search-open'), function (b) {
|
||||
b.addEventListener('click', function (e) { e.preventDefault(); open(); });
|
||||
});
|
||||
|
||||
// Reproduce search on the existing on-page boxes (hero + sidebar): focusing
|
||||
// or clicking them opens the overlay instead of the plain server search.
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.home-search input, .plugin-search input, .plugin-search .form-control'), function (inp) {
|
||||
['focus', 'click', 'mousedown'].forEach(function (ev) {
|
||||
inp.addEventListener(ev, function (e) { e.preventDefault(); inp.blur(); open(); });
|
||||
});
|
||||
});
|
||||
|
||||
// Navbar loupe: show only when no on-page search box is visible (avoids a
|
||||
// duplicate). Appears once the hero/sidebar search scrolls out of view.
|
||||
var navItems = document.querySelectorAll('.nav-search');
|
||||
var pageSearch = document.querySelector('.home-search, .sidebar-container .plugin-search');
|
||||
function setNav(v) {
|
||||
Array.prototype.forEach.call(navItems, function (n) { n.classList.toggle('is-visible', v); });
|
||||
}
|
||||
if (navItems.length) {
|
||||
if (!pageSearch || !('IntersectionObserver' in window)) {
|
||||
setNav(true); // nothing on-page to duplicate -> always available
|
||||
} else {
|
||||
setNav(false);
|
||||
new IntersectionObserver(function (entries) {
|
||||
setNav(!entries[0].isIntersecting);
|
||||
}, { rootMargin: '-64px 0px 0px 0px' }).observe(pageSearch);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose for the keyboard-shortcuts handler.
|
||||
window.BLOWDIT_SEARCH = { open: open, close: close, isOpen: isOpen };
|
||||
})();
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
!function(r,o){"use strict";var e,i="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",a=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),a=parseInt(r.dataset.lineNumber);if(o==a)return n;var i,l=t.textContent,s=r.textContent;for(a<o&&(i=o,o=a,a=i,i=l,l=s,s=i);0!==n.indexOf(l);)l=l.slice(1);for(;-1===n.lastIndexOf(s);)s=s.slice(0,-1);for(var c=l,u=function(e){for(var n=e;"TABLE"!==n.nodeName;)n=n.parentNode;return n}(t),d=o+1;d<a;++d){var f=p('.{0}[{1}="{2}"]',[h,m,d]);c+="\n"+u.querySelector(f).textContent}return c+="\n"+s}function n(e){try{var n=o.querySelectorAll("code.hljs,code.nohighlight");for(var t in n)n.hasOwnProperty(t)&&(n[t].classList.contains("nohljsln")||d(n[t],e))}catch(e){r.console.error("LineNumbers error: ",e)}}function d(e,n){"object"==typeof e&&r.setTimeout(function(){e.innerHTML=f(e,n)},0)}function f(e,n){var t,r,o=(t=e,{singleLine:function(e){return!!e.singleLine&&e.singleLine}(r=(r=n)||{}),startFrom:function(e,n){var t=1;isFinite(n.startFrom)&&(t=n.startFrom);var r=function(e,n){return e.hasAttribute(n)?e.getAttribute(n):null}(e,"data-ln-start-from");return null!==r&&(t=function(e,n){if(!e)return n;var t=Number(e);return isFinite(t)?t:n}(r,1)),t}(t,r)});return function e(n){var t=n.childNodes;for(var r in t){var o;t.hasOwnProperty(r)&&(o=t[r],0<(o.textContent.trim().match(a)||[]).length&&(0<o.childNodes.length?e(o):v(o.parentNode)))}}(e),function(e,n){var t=g(e);""===t[t.length-1].trim()&&t.pop();if(1<t.length||n.singleLine){for(var r="",o=0,a=t.length;o<a;o++)r+=p('<tr><td class="{0} {1}" {3}="{5}"><div class="{2}" {3}="{5}"></div></td><td class="{0} {4}" {3}="{5}">{6}</td></tr>',[l,s,c,m,h,o+n.startFrom,0<t[o].length?t[o]:" "]);return p('<table class="{0}">{1}</table>',[i,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r<t.length;r++){o+=p('<span class="{0}">{1}</span>\n',[n,0<t[r].length?t[r]:" "])}e.innerHTML=o.trim()}}function g(e){return 0===e.length?[]:e.split(a)}function p(e,t){return e.replace(/\{(\d+)\}/g,function(e,n){return void 0!==t[n]?t[n]:e})}r.hljs?(r.hljs.initLineNumbersOnLoad=function(e){"interactive"===o.readyState||"complete"===o.readyState?n(e):r.addEventListener("DOMContentLoaded",function(){n(e)})},r.hljs.lineNumbersBlock=d,r.hljs.lineNumbersValue=function(e,n){if("string"!=typeof e)return;var t=document.createElement("code");return t.innerHTML=e,f(t,n)},(e=o.createElement("style")).type="text/css",e.innerHTML=p(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[i,c,m]),o.getElementsByTagName("head")[0].appendChild(e)):r.console.error("highlight.js not detected!"),document.addEventListener("copy",function(e){var n,t=window.getSelection();!function(e){for(var n=e;n;){if(n.className&&-1!==n.className.indexOf("hljs-ln-code"))return 1;n=n.parentNode}}(t.anchorNode)||(n=-1!==window.navigator.userAgent.indexOf("Edge")?u(t):t.toString(),e.clipboardData.setData("text/plain",n),e.preventDefault())})}(window,document);
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
"releaseDate": "2026-06-03",
|
||||
"license": "MIT",
|
||||
"compatible": "3.22.0",
|
||||
"notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, <mark>, external-link icon, footnote/definition-list styling)."
|
||||
"notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, <mark>, external-link icon, footnote/definition-list styling). Code blocks gained line numbers; the on-page search boxes (hero + sidebar) now open the same instant overlay, and the navbar search icon only appears once they scroll out of view."
|
||||
}
|
||||
+3
-2
@@ -59,8 +59,9 @@
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Search (opens the overlay; also bound to the "/" key) -->
|
||||
<li class="nav-item ml-md-2 mt-2 mt-md-0">
|
||||
<!-- Search (opens the overlay; also bound to the "/" key). Shown by
|
||||
blowdit.js only when no on-page search box is visible. -->
|
||||
<li class="nav-item nav-search ml-md-2 mt-2 mt-md-0">
|
||||
<button type="button" class="theme-toggle js-search-open" aria-label="<?php echo $L->get('Search'); ?>" title="<?php echo $L->get('Search'); ?> (/)">
|
||||
<?php echo blowdit_icon('search'); ?>
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user