Merge pull request #1 from evanwon/copy-heading-links-to-clipboard
Clicking heading link icon now copies URL to clipboard
This commit is contained in:
commit
6ff8850dc4
@ -794,18 +794,50 @@ hr {
|
|||||||
|
|
||||||
.anchor-link {
|
.anchor-link {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
text-decoration: none;
|
padding: 8px;
|
||||||
margin-left: 0em;
|
margin-left: 4px;
|
||||||
|
color: var(--ghost-accent-color);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:hover .anchor-link,
|
@media (hover: hover) {
|
||||||
h2:hover .anchor-link,
|
.anchor-link {
|
||||||
h3:hover .anchor-link,
|
opacity: 0;
|
||||||
h4:hover .anchor-link,
|
}
|
||||||
h5:hover .anchor-link,
|
|
||||||
h6:hover .anchor-link {
|
h1:hover .anchor-link,
|
||||||
opacity: 1;
|
h2:hover .anchor-link,
|
||||||
text-decoration: none;
|
h3:hover .anchor-link,
|
||||||
|
h4:hover .anchor-link,
|
||||||
|
h5:hover .anchor-link,
|
||||||
|
h6:hover .anchor-link,
|
||||||
|
.anchor-link:focus {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.anchor-link {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor-link.copied {
|
||||||
|
animation: pulse 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.2); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom CTA
|
/* Custom CTA
|
||||||
|
@ -58,11 +58,75 @@ function initParallax() {
|
|||||||
var headings = document.querySelectorAll('.gh-content h1, .gh-content h2, .gh-content h3, gh-.content h4, .gh-content h5, .gh-content h6')
|
var headings = document.querySelectorAll('.gh-content h1, .gh-content h2, .gh-content h3, gh-.content h4, .gh-content h5, .gh-content h6')
|
||||||
headings.forEach((heading) => {
|
headings.forEach((heading) => {
|
||||||
heading.insertAdjacentHTML('beforeend', `
|
heading.insertAdjacentHTML('beforeend', `
|
||||||
<a href="#${heading.id}" class="anchor-link">
|
<button class="anchor-link" aria-label="Copy link to this section">
|
||||||
<svg width="1em" height="0.85em" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
|
<svg width="1em" height="0.85em" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
|
||||||
</a>
|
</button>
|
||||||
`)
|
`)
|
||||||
})
|
|
||||||
|
const anchor = heading.querySelector('.anchor-link');
|
||||||
|
anchor.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.hash = heading.id;
|
||||||
|
|
||||||
|
// Try modern clipboard API first
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(url.toString())
|
||||||
|
.then(() => {
|
||||||
|
showCopiedFeedback(anchor);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Fallback for clipboard API failure
|
||||||
|
fallbackCopy(url.toString(), anchor);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for browsers without clipboard API
|
||||||
|
fallbackCopy(url.toString(), anchor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCopiedFeedback(element) {
|
||||||
|
element.classList.add('copied');
|
||||||
|
// For screen readers
|
||||||
|
element.setAttribute('aria-label', 'Link copied to clipboard');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
element.classList.remove('copied');
|
||||||
|
element.setAttribute('aria-label', 'Copy link to this section');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text, element) {
|
||||||
|
// Create temporary input
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed'; // Avoid scrolling to bottom
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
|
||||||
|
// Handle iOS devices
|
||||||
|
if (navigator.userAgent.match(/ipad|iphone/i)) {
|
||||||
|
textArea.contentEditable = true;
|
||||||
|
textArea.readOnly = true;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(textArea);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
textArea.setSelectionRange(0, 999999);
|
||||||
|
} else {
|
||||||
|
textArea.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
showCopiedFeedback(element);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback copy failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', addAnchors)
|
document.addEventListener('DOMContentLoaded', addAnchors)
|
||||||
|
Loading…
Reference in New Issue
Block a user