Skip to content

Commit 17ca929

Browse files
committed
Update Citation modal to include both rendered text and html
Allow the user to copy either the formatted 'pretty' text or the raw html that creates the entry.
1 parent 298633d commit 17ca929

File tree

3 files changed

+224
-82
lines changed

3 files changed

+224
-82
lines changed

layouts/bibliography/single.html

Lines changed: 167 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -236,100 +236,185 @@ <h1>{{ .Title }}</h1>
236236
</div>
237237
</article>
238238
<!-- Citation modal -->
239-
<div id="citationModal" class="citation-modal" hidden>
240-
<div class="citation-dialog">
241-
<div class="citation-header">
242-
<strong>Citation</strong>
243-
<button type="button" class="citation-close" aria-label="Close">×</button>
244-
</div>
245-
<div class="citation-body">
239+
<div id="citationModal" class="citation-modal" hidden>
240+
<div class="citation-dialog">
241+
<div class="citation-header">
242+
<strong>Citation</strong>
243+
<button type="button" class="citation-close" aria-label="Close">×</button>
244+
</div>
245+
246+
<!-- Tabs -->
247+
<div class="citation-tabs" role="tablist" aria-label="Citation views">
248+
<button type="button" class="tab-btn is-active" data-tab="formatted" role="tab" aria-selected="true" aria-controls="tab-formatted">Formatted</button>
249+
<button type="button" class="tab-btn" data-tab="raw" role="tab" aria-selected="false" aria-controls="tab-raw">HTML</button>
250+
</div>
251+
252+
<div class="citation-body">
253+
<!-- Formatted view -->
254+
<div id="tab-formatted" class="tab-panel is-active" role="tabpanel" aria-labelledby="tabbtn-formatted">
246255
<div id="citationContent">Loading…</div>
247256
</div>
248-
<div class="citation-actions">
249-
<button type="button" id="copyCitation">Copy</button>
250-
<button type="button" class="citation-close">Close</button>
257+
258+
<!-- Raw HTML view -->
259+
<div id="tab-raw" class="tab-panel" role="tabpanel" aria-labelledby="tabbtn-raw">
260+
<pre class="codebox"><code id="citationRaw" class="language-html"></code></pre>
251261
</div>
252262
</div>
253-
<div class="citation-backdrop"></div>
263+
264+
<div class="citation-actions">
265+
<button type="button" id="copyCitation">Copy Text</button>
266+
<button type="button" id="copyCitationHtml">Copy HTML</button>
267+
<button type="button" class="citation-close">Close</button>
268+
</div>
254269
</div>
270+
<div class="citation-backdrop"></div>
271+
</div>
255272

256-
<style>
257-
.citation-modal[hidden] { display: none; }
258-
.citation-modal { position: fixed; inset: 0; z-index: 1050; }
259-
.citation-dialog {
260-
position: absolute; top: 10%; left: 50%; transform: translateX(-50%);
261-
max-width: 720px; width: calc(100% - 2rem);
262-
background: #fff; border-radius: 6px; box-shadow: 0 10px 30px rgba(0,0,0,.2);
263-
overflow: hidden;
264-
}
265-
.citation-header { display:flex; justify-content:space-between; align-items:center; padding:.75rem 1rem; border-bottom:1px solid #eee; }
266-
.citation-close { background:none; border:0; font-size:1.25rem; line-height:1; cursor:pointer; }
267-
.citation-body { padding:1rem; max-height:50vh; overflow:auto; }
268-
#citationContent { font-size:1rem; line-height:1.4; }
269-
.citation-actions { display:flex; gap:.5rem; justify-content:flex-end; padding:.75rem 1rem; border-top:1px solid #eee; }
270-
</style>
271273

272-
<script>
273-
(function () {
274-
function openModal() { document.getElementById('citationModal').hidden = false; }
275-
function closeModal() { document.getElementById('citationModal').hidden = true; }
274+
<style>
275+
.citation-modal[hidden] { display: none; }
276+
.citation-modal { position: fixed; inset: 0; z-index: 1050; }
277+
.citation-dialog {
278+
position: absolute; top: 10%; left: 50%; transform: translateX(-50%);
279+
max-width: 720px; width: calc(100% - 2rem);
280+
background: #fff; border-radius: 6px; box-shadow: 0 10px 30px rgba(0,0,0,.2);
281+
overflow: hidden;
282+
}
283+
.citation-header { display:flex; justify-content:space-between; align-items:center; padding:.75rem 1rem; border-bottom:1px solid #eee; }
284+
.citation-close { background:none; border:0; font-size:1.25rem; line-height:1; cursor:pointer; }
285+
.citation-tabs { display:flex; gap:.25rem; padding:.5rem 1rem; border-bottom:1px solid #eee; }
286+
.tab-btn {
287+
border: 1px solid #ddd; background:#f8f9fa; border-radius:4px; padding:.35rem .6rem; cursor:pointer;
288+
}
289+
.tab-btn.is-active { background:#fff; border-color:#bbb; }
290+
.citation-body { padding:1rem; max-height:50vh; overflow:auto; }
291+
#citationContent { font-size:1rem; line-height:1.4; }
292+
.codebox {
293+
background:#f6f8fa; border:1px solid #e1e4e8; border-radius:6px;
294+
padding:.75rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
295+
font-size:.9rem; line-height:1.4; white-space:pre-wrap; word-break:break-word;
296+
}
297+
.tab-panel { display:none; }
298+
.tab-panel.is-active { display:block; }
299+
.citation-actions { display:flex; gap:.5rem; justify-content:flex-end; padding:.75rem 1rem; border-top:1px solid #eee; }
300+
</style>
276301

277-
document.addEventListener('click', function (e) {
278-
if (e.target.matches('#citeBtn')) {
279-
e.preventDefault();
280-
const btn = e.target;
281-
const gid = btn.dataset.gid;
282-
const key = btn.dataset.key;
283-
const style = btn.dataset.style || 'apa';
284-
const url = `https://api.zotero.org/groups/${encodeURIComponent(gid)}/items/${encodeURIComponent(key)}?format=bib&style=${encodeURIComponent(style)}&linkwrap=1`;
302+
<script src="/js/vendor/prettier/standalone.js" defer></script>
303+
<script src="/js/vendor/prettier/plugins/html.js" defer></script>
285304

286-
const box = document.getElementById('citationContent');
287-
box.textContent = 'Loading…';
288-
openModal();
305+
<script>
306+
(function () {
307+
const modal = document.getElementById('citationModal');
308+
const contentBox = document.getElementById('citationContent');
309+
const rawBox = document.getElementById('citationRaw');
289310

290-
fetch(url, { headers: { 'Accept': 'text/html' }})
291-
.then(r => {
292-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
293-
return r.text();
294-
})
295-
.then(html => {
296-
// API returns HTML (span with formatted citation)
297-
box.innerHTML = html;
298-
})
299-
.catch(err => {
300-
box.textContent = `Failed to load citation (${err})`;
301-
});
302-
}
303-
if (e.target.matches('.citation-close')) {
304-
e.preventDefault();
305-
closeModal();
306-
}
307-
if (e.target.matches('#copyCitation')) {
308-
e.preventDefault();
309-
const el = document.getElementById('citationContent');
310-
const text = el.innerText.trim();
311-
if (navigator.clipboard && window.isSecureContext) {
312-
navigator.clipboard.writeText(text).then(() => {
313-
e.target.textContent = 'Copied';
314-
setTimeout(() => e.target.textContent = 'Copy', 1200);
315-
});
316-
} else {
317-
const ta = document.createElement('textarea');
318-
ta.value = text; document.body.appendChild(ta);
319-
ta.select(); try { document.execCommand('copy'); } catch(e){}
320-
document.body.removeChild(ta);
321-
}
322-
}
323-
});
311+
function openModal() { modal.hidden = false; }
312+
function closeModal() { modal.hidden = true; }
324313

325-
// Close when clicking backdrop or pressing Escape
326-
document.getElementById('citationModal').addEventListener('click', function (e) {
327-
if (e.target.classList.contains('citation-backdrop')) closeModal();
314+
function setActiveTab(name) {
315+
document.querySelectorAll('.tab-btn').forEach(btn => {
316+
const isActive = btn.dataset.tab === name;
317+
btn.classList.toggle('is-active', isActive);
318+
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
328319
});
329-
document.addEventListener('keydown', function (e) {
330-
if (e.key === 'Escape') closeModal();
320+
document.querySelectorAll('.tab-panel').forEach(panel => {
321+
const isActive = panel.id === ('tab-' + name);
322+
panel.classList.toggle('is-active', isActive);
331323
});
332-
})();
333-
</script>
324+
}
325+
326+
function copyTextToClipboard(text, button) {
327+
const done = () => {
328+
if (button) {
329+
const original = button.textContent;
330+
button.textContent = 'Copied';
331+
setTimeout(() => (button.textContent = original), 1200);
332+
}
333+
};
334+
if (navigator.clipboard && window.isSecureContext) {
335+
navigator.clipboard.writeText(text).then(done).catch(done);
336+
} else {
337+
const ta = document.createElement('textarea');
338+
ta.value = text; document.body.appendChild(ta);
339+
ta.select(); try { document.execCommand('copy'); } catch(e) {}
340+
document.body.removeChild(ta); done();
341+
}
342+
}
343+
344+
document.addEventListener('click', function (e) {
345+
// Open + fetch
346+
if (e.target.matches('#citeBtn')) {
347+
e.preventDefault();
348+
const btn = e.target;
349+
const gid = btn.dataset.gid;
350+
const key = btn.dataset.key;
351+
const style = btn.dataset.style || 'apa';
352+
// Use Zotero API; format=bib returns HTML bibliography item(s)
353+
const url = `https://api.zotero.org/groups/${encodeURIComponent(gid)}/items/${encodeURIComponent(key)}?format=bib&style=${encodeURIComponent(style)}&linkwrap=1`;
354+
355+
contentBox.textContent = 'Loading…';
356+
rawBox.textContent = '';
357+
setActiveTab('formatted');
358+
openModal();
359+
360+
fetch(url, { headers: { 'Accept': 'text/html' }})
361+
.then(r => {
362+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
363+
return r.text();
364+
})
365+
.then(html => {
366+
contentBox.innerHTML = html;
367+
try {
368+
const formatted = window.prettier.format(html, { parser: "html", plugins: window.prettierPlugins, tabWidth: 2 });
369+
// prettier.format may return a string or a Promise depending on plugin loading; normalize to a Promise
370+
return Promise.resolve(formatted).then(pretty => {
371+
rawBox.textContent = pretty;
372+
return html;
373+
});
374+
} catch (err) {
375+
// synchronous error formatting
376+
rawBox.textContent = String(err);
377+
return html;
378+
}
379+
})
380+
.catch(err => {
381+
const msg = `Failed to load citation (${err})`;
382+
contentBox.textContent = msg;
383+
rawBox.textContent = msg;
384+
});
385+
}
386+
387+
// Close
388+
if (e.target.matches('.citation-close')) {
389+
e.preventDefault();
390+
closeModal();
391+
}
392+
393+
// Copy buttons
394+
if (e.target.matches('#copyCitation')) {
395+
e.preventDefault();
396+
copyTextToClipboard(contentBox.innerText.trim(), e.target);
397+
}
398+
if (e.target.matches('#copyCitationHtml')) {
399+
e.preventDefault();
400+
copyTextToClipboard(rawBox.textContent.trim(), e.target);
401+
}
402+
403+
// Tabs
404+
if (e.target.matches('.tab-btn')) {
405+
e.preventDefault();
406+
setActiveTab(e.target.dataset.tab);
407+
}
408+
});
409+
410+
// Close when clicking backdrop or pressing Escape
411+
modal.addEventListener('click', function (e) {
412+
if (e.target.classList.contains('citation-backdrop')) closeModal();
413+
});
414+
document.addEventListener('keydown', function (e) {
415+
if (e.key === 'Escape') closeModal();
416+
});
417+
})();
418+
</script>
334419

335420
{{ end }}

0 commit comments

Comments
 (0)