1+ /**
2+ * Citation clipboard handler for Hugo Blox Builder
3+ * Copies BibTeX citation to clipboard when cite button is clicked
4+ */
5+
6+ import { hugoEnvironment , i18n } from '@params' ;
7+ import { showNotification } from './hb-notifier.js' ;
8+ import { copyToClipboardSync , ClipboardCache } from './hb-clipboard.js' ;
9+
10+ // Debug mode based on environment
11+ const isDebugMode = hugoEnvironment === 'development' ;
12+
13+ // Cache for citation content
14+ const citationCache = new ClipboardCache ( ) ;
15+
16+ // Initialize citation handlers when DOM is ready
17+ if ( document . readyState === 'loading' ) {
18+ document . addEventListener ( 'DOMContentLoaded' , initializeCitation ) ;
19+ } else {
20+ initializeCitation ( ) ;
21+ }
22+
23+ function initializeCitation ( ) {
24+ // Handle citation button clicks using event delegation
25+ document . addEventListener ( 'click' , handleCiteClick ) ;
26+
27+ // Prefetch citations on hover/focus for better UX and Safari compatibility
28+ document . addEventListener ( 'mouseover' , prefetchOnHover ) ;
29+ document . addEventListener ( 'focusin' , prefetchOnHover ) ;
30+
31+ // Prefetch all citations on page load
32+ prefetchAllCitations ( ) ;
33+ }
34+
35+ /**
36+ * Prefetch all citations on page load
37+ */
38+ function prefetchAllCitations ( ) {
39+ const citeButtons = document . querySelectorAll ( '.js-cite-clipboard[data-filename]' ) ;
40+ citeButtons . forEach ( button => {
41+ const filename = button . getAttribute ( 'data-filename' ) ;
42+ if ( filename && ! citationCache . has ( filename ) ) {
43+ // Fetch in background, don't await
44+ fetchAndCacheCitation ( filename ) ;
45+ }
46+ } ) ;
47+ }
48+
49+ /**
50+ * Prefetch citation on hover/focus
51+ * @param {Event } e - Hover or focus event
52+ */
53+ function prefetchOnHover ( e ) {
54+ const citeButton = e . target . closest ( '.js-cite-clipboard' ) ;
55+ if ( ! citeButton ) return ;
56+
57+ const filename = citeButton . getAttribute ( 'data-filename' ) ;
58+ if ( filename && ! citationCache . has ( filename ) ) {
59+ // Fetch in background, don't await
60+ fetchAndCacheCitation ( filename ) ;
61+ }
62+ }
63+
64+ /**
65+ * Fetch and cache citation content
66+ * @param {string } filename - Citation file URL
67+ * @returns {Promise<string|null> } - Citation content or null if failed
68+ */
69+ async function fetchAndCacheCitation ( filename ) {
70+ try {
71+ const response = await fetch ( filename ) ;
72+ if ( ! response . ok ) {
73+ throw new Error ( `Failed to fetch citation: ${ response . statusText } ` ) ;
74+ }
75+ const citation = await response . text ( ) ;
76+ citationCache . set ( filename , citation ) ;
77+ return citation ;
78+ } catch ( error ) {
79+ if ( isDebugMode ) {
80+ console . error ( `Failed to fetch citation ${ filename } :` , error ) ;
81+ }
82+ return null ;
83+ }
84+ }
85+
86+ /**
87+ * Handle cite button clicks - synchronous clipboard write for Safari compatibility
88+ * @param {Event } e - Click event
89+ */
90+ function handleCiteClick ( e ) {
91+ // Check if clicked element or its parent is a cite button
92+ const citeButton = e . target . closest ( '.js-cite-clipboard' ) ;
93+ if ( ! citeButton ) return ;
94+
95+ e . preventDefault ( ) ;
96+ e . stopPropagation ( ) ;
97+
98+ const filename = citeButton . getAttribute ( 'data-filename' ) ;
99+ if ( ! filename ) {
100+ if ( isDebugMode ) {
101+ console . error ( 'No filename specified for citation' ) ;
102+ }
103+ showNotification ( 'Citation file not found' , 'error' ) ;
104+ return ;
105+ }
106+
107+ // Check if citation is cached
108+ const cachedCitation = citationCache . get ( filename ) ;
109+
110+ if ( cachedCitation ) {
111+ // Citation is cached, copy immediately (synchronous for Safari)
112+ copyToClipboardSync ( cachedCitation ) . then ( success => {
113+ if ( success ) {
114+ showNotification ( i18n ?. copied || 'Citation copied!' , 'success' ) ;
115+ updateButtonText ( citeButton ) ;
116+ } else {
117+ showNotification ( 'Failed to copy citation' , 'error' ) ;
118+ }
119+ } ) ;
120+ } else {
121+ // Not cached, need to fetch first (will fail in Safari with strict mode)
122+ fetchAndCopyWithFallback ( filename , citeButton ) ;
123+ }
124+ }
125+
126+ /**
127+ * Fetch and copy with fallback (for browsers that allow async clipboard)
128+ * @param {string } filename - Citation file URL
129+ * @param {HTMLElement } button - Cite button element
130+ */
131+ async function fetchAndCopyWithFallback ( filename , button ) {
132+ try {
133+ const citation = await fetchAndCacheCitation ( filename ) ;
134+ if ( citation ) {
135+ // Try to copy (might fail in Safari due to lost user activation)
136+ const success = await copyToClipboardSync ( citation ) ;
137+ if ( success ) {
138+ showNotification ( i18n ?. copied || 'Citation copied!' , 'success' ) ;
139+ updateButtonText ( button ) ;
140+ } else {
141+ showNotification ( 'Failed to copy citation' , 'error' ) ;
142+ }
143+ } else {
144+ showNotification ( 'Failed to load citation' , 'error' ) ;
145+ }
146+ } catch ( error ) {
147+ if ( isDebugMode ) {
148+ console . error ( 'Failed to copy citation:' , error ) ;
149+ }
150+ // If it's a NotAllowedError, suggest hovering first
151+ if ( error . name === 'NotAllowedError' ) {
152+ showNotification ( 'Please hover over the button first, then click' , 'info' ) ;
153+ } else {
154+ showNotification ( 'Failed to copy citation' , 'error' ) ;
155+ }
156+ }
157+ }
158+
159+ /**
160+ * Update button text to show copied state
161+ * @param {HTMLElement } button - The cite button element
162+ */
163+ function updateButtonText ( button ) {
164+ const copiedText = i18n ?. copied || 'Copied!' ;
165+
166+ // Find text element to update (skip icon)
167+ const textElement = button . querySelector ( 'span' ) ;
168+ if ( ! textElement ) {
169+ if ( isDebugMode ) {
170+ console . warn ( 'Could not find text element in cite button' ) ;
171+ }
172+ return ;
173+ }
174+
175+ const originalText = textElement . textContent ;
176+ textElement . textContent = copiedText ;
177+
178+ // Add visual feedback
179+ button . classList . add ( 'opacity-70' ) ;
180+
181+ // Revert after 2 seconds
182+ setTimeout ( ( ) => {
183+ textElement . textContent = originalText ;
184+ button . classList . remove ( 'opacity-70' ) ;
185+ } , 2000 ) ;
186+ }
187+
188+ // Export functions for potential reuse
189+ export { handleCiteClick , prefetchAllCitations } ;
0 commit comments