-
Notifications
You must be signed in to change notification settings - Fork 152
Expand file tree
/
Copy pathfooter.js
More file actions
191 lines (162 loc) · 7.24 KB
/
footer.js
File metadata and controls
191 lines (162 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
(function() {
// Record analytics visit
(function recordAnalytics() {
const STORAGE_KEY = 'tools_analytics';
const slug = window.location.pathname; // path only, no fragment or query string
const timestamp = Date.now();
try {
const analytics = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
analytics.push({ slug, timestamp });
localStorage.setItem(STORAGE_KEY, JSON.stringify(analytics));
} catch (e) {
// Silently fail if localStorage is unavailable
}
})();
// Get the current filename from the URL
let pathname = window.location.pathname;
let filename = pathname.split('/').pop() || 'index.html';
// Add .html if missing
if (!filename.endsWith('.html')) {
filename += '.html';
}
// Get the page name for the "About" link (from pathname, without .html)
const pageName = filename.replace('.html', '');
// Parse an RGB/RGBA color string and return {r, g, b, a}
function parseColor(colorStr) {
const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (match) {
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: match[4] !== undefined ? parseFloat(match[4]) : 1
};
}
return null;
}
// Calculate relative luminance (0 = black, 1 = white)
function getLuminance(r, g, b) {
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
// Get the effective background color, checking body and html
function getEffectiveBackgroundColor() {
// Check body first
const bodyBg = window.getComputedStyle(document.body).backgroundColor;
const bodyColor = parseColor(bodyBg);
if (bodyColor && bodyColor.a > 0.1) {
return bodyColor;
}
// Check html element
const htmlBg = window.getComputedStyle(document.documentElement).backgroundColor;
const htmlColor = parseColor(htmlBg);
if (htmlColor && htmlColor.a > 0.1) {
return htmlColor;
}
// Default to white (browser default)
return { r: 255, g: 255, b: 255, a: 1 };
}
// Check if a color has good contrast against the background
function hasGoodContrast(textColor, bgColor) {
const textLum = getLuminance(textColor.r, textColor.g, textColor.b);
const bgLum = getLuminance(bgColor.r, bgColor.g, bgColor.b);
const contrast = Math.abs(textLum - bgLum);
return contrast > 0.3; // Minimum contrast threshold
}
// Find the most common text color that has good contrast with the background
function getBestTextColor(bgColor) {
const colorCounts = {};
const elements = document.body.querySelectorAll('*');
elements.forEach(el => {
if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'NOSCRIPT') {
return;
}
const hasDirectText = Array.from(el.childNodes).some(
node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0
);
if (hasDirectText) {
const style = window.getComputedStyle(el);
const color = style.color;
const parsed = parseColor(color);
if (parsed && parsed.a > 0.1 && hasGoodContrast(parsed, bgColor)) {
colorCounts[color] = (colorCounts[color] || 0) + 1;
}
}
});
// Find the most common color with good contrast
let mostCommon = null;
let maxCount = 0;
for (const [color, count] of Object.entries(colorCounts)) {
if (count > maxCount) {
maxCount = count;
mostCommon = color;
}
}
return mostCommon;
}
const bgColor = getEffectiveBackgroundColor();
const bgLuminance = getLuminance(bgColor.r, bgColor.g, bgColor.b);
const isDark = bgLuminance < 0.5;
// Try to find a good text color from the page
let textColor = getBestTextColor(bgColor);
// If no suitable color found, use sensible defaults based on background
if (!textColor) {
textColor = isDark ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)';
}
// Check if body uses flex or grid layout that could break footer positioning
const bodyStyle = window.getComputedStyle(document.body);
const bodyDisplay = bodyStyle.display;
const needsLayoutFix = (bodyDisplay === 'flex' || bodyDisplay === 'grid');
if (needsLayoutFix) {
// Wrap existing body content to preserve the original layout behavior
const wrapper = document.createElement('div');
wrapper.style.cssText = `
display: ${bodyDisplay};
flex: 1 1 auto;
flex-direction: ${bodyStyle.flexDirection};
align-items: ${bodyStyle.alignItems};
justify-content: ${bodyStyle.justifyContent};
flex-wrap: ${bodyStyle.flexWrap};
gap: ${bodyStyle.gap};
width: 100%;
min-height: inherit;
`;
// Move all existing children (except scripts at the end) into the wrapper
while (document.body.firstChild) {
wrapper.appendChild(document.body.firstChild);
}
// Reset body to a vertical flex column
document.body.style.display = 'flex';
document.body.style.flexDirection = 'column';
document.body.style.alignItems = 'stretch';
document.body.style.justifyContent = 'flex-start';
document.body.appendChild(wrapper);
}
// Create the footer element
const footer = document.createElement('footer');
footer.style.cssText = 'flex-shrink: 0; width: 100%; box-sizing: border-box;';
footer.innerHTML = `
<hr style="margin: 2rem 0 1rem 0; border: none; border-top: 1px solid ${textColor};">
<nav style="font-family: system-ui, -apple-system, sans-serif; font-size: 12px; text-align: center; font-style: normal; padding-bottom: 1rem;">
<a href="/" style="color: ${textColor}; text-decoration: underline; margin-right: 1.5rem;">Home</a>
<a href="/colophon#${filename}" style="color: ${textColor}; text-decoration: underline; margin-right: 1.5rem;">About ${pageName}</a>
<a href="https://github.com/simonw/tools/blob/main/${filename}" style="color: ${textColor}; text-decoration: underline; margin-right: 1.5rem;">View source</a>
<a href="https://github.com/simonw/tools/commits/main/${filename}" style="color: ${textColor}; text-decoration: underline;" id="footer-changes-link">Changes</a>
</nav>
`;
document.body.appendChild(footer);
// Fetch dates.json and update the Changes link with the last updated date
fetch('/dates.json')
.then(response => response.json())
.then(dates => {
const date = dates[filename];
if (date) {
const link = document.getElementById('footer-changes-link');
if (link) {
link.textContent = `Updated ${date}`;
}
}
})
.catch(() => {
// Silently fail - keep "Changes" as fallback
});
})();