@@ -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