Skip to content

Commit d165cb7

Browse files
add link icon to section headers (#1755)
* add link icon to section headers * review suggestions * toc embed fix * Attempt to fix the broken on this page ToC for embedded content that includes headers --------- Co-authored-by: Rachel Elledge <[email protected]>
1 parent 4175764 commit d165cb7

File tree

4 files changed

+113
-2
lines changed

4 files changed

+113
-2
lines changed

assets/css/index.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ section.prose {
4747
@apply text-sm font-medium;
4848
}
4949

50+
/* Header link styles */
51+
.header-link {
52+
@apply text-slate-400 hover:text-slate-600 transition-all duration-200 no-underline cursor-pointer;
53+
text-decoration: none !important;
54+
vertical-align: baseline;
55+
}
56+
57+
.header-link:hover {
58+
@apply text-slate-600;
59+
text-decoration: none !important;
60+
}
61+
62+
.header-link svg {
63+
@apply w-4 h-4 inline-block;
64+
vertical-align: baseline;
65+
}
66+
67+
/* Ensure header links don't interfere with prose styling */
68+
.prose h1 .header-link,
69+
.prose h2 .header-link,
70+
.prose h3 .header-link,
71+
.prose h4 .header-link,
72+
.prose h5 .header-link,
73+
.prose h6 .header-link {
74+
@apply text-slate-400 hover:text-slate-600;
75+
text-decoration: none !important;
76+
}
77+
78+
/* Feedback state for copied links */
79+
.header-link.copied {
80+
@apply text-green-500;
81+
}
82+
5083
.prose p, .prose ol, .prose ul {
5184
@apply text-base;
5285
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{- $anchor := .Anchor | safeURL -}}
2+
{{- $level := .Level -}}
3+
{{- $text := .Text | safeHTML -}}
4+
<h{{ $level }} id="{{ $anchor }}" class="group relative">
5+
{{ $text }}
6+
<a href="#{{ $anchor }}" class="header-link opacity-0 group-hover:opacity-100 transition-opacity duration-200 ml-1 align-baseline" aria-label="Link to this section" title="Copy link to clipboard">
7+
<svg class="inline-block w-4 h-4 align-baseline" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
8+
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path>
9+
</svg>
10+
</a>
11+
</h{{ $level }}>

layouts/partials/docs-toc.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{{ $showEmbedHeaders := .Params.tocEmbedHeaders }}
55
{{ $headerRange := .Params.headerRange | default "[1-3]" }}
66
<!-- Ignore empty links with + -->
7-
{{ $headers := findRE ( print "<h" $headerRange ".*?>(.|\n])+?</h" $headerRange ">") .Content }}
7+
{{ $headers := findRE ( print "<h" $headerRange ".*?>(.|\n)+?</h" $headerRange ">") .Content }}
88
<!-- Must have at least one header to link to -->
99
{{ $has_headers := ge (len $headers) 1 }}
1010

@@ -31,7 +31,9 @@ <h2 class="font-medium my-3">On this page</h2>
3131
<!-- Close previous list item -->
3232
</li>
3333
{{ end }}
34-
<li><a href="#{{ $anchorID }}">{{ $header | plainify | safeHTML }}</a>
34+
<!-- Extract text content, excluding the header link icon -->
35+
{{ $headerText := $header | replaceRE `<a[^>]*class="header-link"[^>]*>.*?</a>` "" | plainify | safeHTML }}
36+
<li><a href="#{{ $anchorID }}">{{ $headerText }}</a>
3537
{{ $prevLevel = $level }}
3638
{{ end }}
3739
<!-- Close remaining open lists -->

static/js/index.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,74 @@ const mobileMenu = (() => {
9898
toggleMenu('products-mobile-menu', 'productsMobileMenuState')
9999
} else if (event.target.closest('[data-resources-mobile-menu-toggle]')) {
100100
toggleMenu('resources-mobile-menu', 'resourcesMobileMenuState')
101+
} else if (event.target.closest('.header-link')) {
102+
// Handle header link clicks
103+
event.preventDefault()
104+
copyHeaderLinkToClipboard(event.target.closest('.header-link'))
101105
}
102106
}
103107

108+
// Copy header link URL to clipboard
109+
function copyHeaderLinkToClipboard(linkElement) {
110+
const href = linkElement.getAttribute('href')
111+
const fullUrl = window.location.origin + window.location.pathname + href
112+
113+
// Update the URL hash to provide immediate visual feedback
114+
window.location.hash = href
115+
116+
// Copy to clipboard
117+
if (navigator.clipboard && navigator.clipboard.writeText) {
118+
navigator.clipboard.writeText(fullUrl).then(() => {
119+
showCopyFeedback(linkElement)
120+
}).catch(err => {
121+
console.error('Failed to copy link: ', err)
122+
fallbackCopyToClipboard(fullUrl, linkElement)
123+
})
124+
} else {
125+
// Fallback for older browsers
126+
fallbackCopyToClipboard(fullUrl, linkElement)
127+
}
128+
}
129+
130+
// Show visual feedback when link is copied
131+
function showCopyFeedback(linkElement) {
132+
const originalTitle = linkElement.getAttribute('title')
133+
134+
linkElement.setAttribute('title', 'Copied!')
135+
linkElement.classList.add('copied')
136+
137+
setTimeout(() => {
138+
linkElement.setAttribute('title', originalTitle)
139+
linkElement.classList.remove('copied')
140+
}, 2000)
141+
}
142+
143+
// Fallback copy method for older browsers
144+
function fallbackCopyToClipboard(text, linkElement) {
145+
const textArea = document.createElement('textarea')
146+
textArea.value = text
147+
textArea.style.position = 'fixed'
148+
textArea.style.left = '-999999px'
149+
textArea.style.top = '-999999px'
150+
document.body.appendChild(textArea)
151+
textArea.focus()
152+
textArea.select()
153+
154+
try {
155+
const successful = document.execCommand('copy')
156+
if (successful) {
157+
showCopyFeedback(linkElement)
158+
console.log('Link copied to clipboard (fallback)')
159+
} else {
160+
console.error('Fallback copy failed')
161+
}
162+
} catch (err) {
163+
console.error('Fallback copy failed: ', err)
164+
}
165+
166+
document.body.removeChild(textArea)
167+
}
168+
104169
function allowFocus(selector, state) {
105170
const container = document.querySelector(selector)
106171
const focusable = container.querySelectorAll('button, [href], input, select, textarea')

0 commit comments

Comments
 (0)