4 * A bunch of shorthand to put together common parts of an HTML page.
7 const { lazy
} = require('@squeep/lazy');
10 * Some fields may have values outside normal dates, handle them here.
12 * @param {String} otherwise
14 const dateOrNot
= (date
, otherwise
) => {
18 if (typeof date
=== 'number') {
19 date
= new Date(date
);
21 const dateMs
= date
.getTime();
22 if (!Number
.isFinite(dateMs
)
26 return date
.toString();
31 * Why is rendering a Date as a string this complicated?
32 * @param {Date|Number} date
33 * @param {String=} pInf
34 * @param {String=} nInf
35 * @param {String=} otherwise
37 const dateFormat
= (date
, pInf
= 'Never', nInf
= 'Forever', otherwise
= '') => {
38 const isDatableType
= ['number', 'string'].includes(typeof date
);
46 || (!(date
instanceof Date
) && !isDatableType
)) {
51 date
= new Date(date
);
53 const parts
= dateFormat
._dtf
.formatToParts(date
);
54 return parts
.map((p
) => p
.value
).join('');
56 lazy(dateFormat
, '_dtf', () => {
57 const dateTimeFormatOptions
= {
61 return new Intl
.DateTimeFormat(undefined, dateTimeFormatOptions
);
67 * @param {Number} seconds
70 const secondsToPeriod
= (seconds
) => {
74 const nextResult
= (factor
, label
) => {
75 const r
= factor
? value
% factor : value
;
77 result
.push(`${r} ${label}${r != 1 ? 's' : ''}`);
79 value
= factor
? Math
.floor(value
/ factor
) : value
;
82 nextResult(60, 'second');
83 nextResult(60, 'minute');
84 nextResult(24, 'hour');
85 nextResult(30, 'day');
86 nextResult(undefined, 'month');
89 return result
.join(' ');
94 * Render the preamble <head> for an HTML page.
95 * @param {Number} pagePathLevel number of paths below root this page is
97 * @param {Object} options
98 * @param {String[]=} options.headElements
99 * @param {String=} options.pageTitle
102 function htmlHead(pagePathLevel
, ctx
, options
) {
103 const rootPathPfx
= '../'.repeat(pagePathLevel
);
109 \t\t<meta charset="utf-8">
110 \t\t<meta name="viewport" content="width=device-width,initial-scale=1">
111 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css" title="Default">
112 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css" title="Site Specific">
113 ${headElements.map((e) => '\t\t' + e).join('\n')}
114 \t\t<title>${pageTitle}</title>
120 * Render the main content of an HTML page.
121 * @param {Number} pagePathLevel
122 * @param {Object} ctx
123 * @param {Object} options
124 * @param {Object} options.bodyAttributes
125 * @param {String[]} main
128 function htmlBody(pagePathLevel
, ctx
, options
, main
= []) {
132 const firefoxFix
= '\n<script>0</script>'; // This fixes a layout rendering flash on load in Firefox; do not know why this works, but it does.
134 \t<body${elementAttributes(bodyAttributes)}>${firefoxFix}
135 ${htmlHeader(pagePathLevel, ctx, options)}
136 ${htmlMessages(ctx, options)}
140 ${htmlFooter(ctx, options)}
146 * Render a navigation link for the header section.
147 * @param {Object} nav
148 * @param {String} nav.href
149 * @param {String} nav.class
150 * @param {String} nav.text
153 function renderNavLink(nav
) {
154 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
159 * Render the navigation section of the page header.
160 * @param {Object} ctx
161 * @param {Object} options
162 * @param {Object[]=} options.navLinks
165 function htmlNav(ctx
, options
) {
167 const spacer
= '\t'.repeat(indent
);
171 return navLinks
.length
? `${spacer}<nav>
172 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
173 ${spacer}</nav>` : '';
178 * Render the banner and navigation header.
179 * @param {Number} pagePathLevel
180 * @param {Object} ctx
181 * @param {Object} options
182 * @param {String[]=} options.logoUrl
183 * @param {String[]=} options.pageTitle
186 function htmlHeader(pagePathLevel
, ctx
, options
) {
187 const rootPathPfx
= '../'.repeat(pagePathLevel
);
192 const logoImg
= logoUrl
? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
194 \t\t\t<h1>${logoImg}${pageTitle}</h1>
195 ${htmlNav(ctx, options)}
201 * Render the bottom boilerplate.
202 * @param {Object} ctx
203 * @param {Object} options
204 * @param {String[]} options.footerEntries
207 function htmlFooter(ctx
, options
) {
209 const spacer
= '\t'.repeat(indent
);
214 return footerEntries
.length
? `${spacer}<footer>
215 ${OL(footerEntries, indent + 1)}
216 ${spacer}</footer>` : '';
221 * Convert an object into element attributes.
222 * @param {Object} attributes
225 function elementAttributes(attributes
) {
226 const attr
= Object
.entries(attributes
).map(([name
, value
]) => `${name}="${value}"`).join(' ');
227 return attr
? ' ' + attr : '';
232 * Wrap an item in a list item element.
233 * @param {String} item
234 * @param {Number} indent
235 * @param {Object} attributes
238 function LI(item
, indent
= 0, attributes
= {}) {
239 const spacer
= '\t'.repeat(indent
);
240 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
245 * Wrap a list of items in an unordered list.
246 * @param {String[]} items
247 * @param {Number} indent
248 * @param {Object} attributes
249 * @param {(item) => Object} itemAttributeGenerator
252 function UL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
253 const spacer
= '\t'.repeat(indent
);
254 return `${spacer}<ul${elementAttributes(attributes)}>
255 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
261 * Wrap a list of items in an ordered list.
262 * @param {String[]} items
263 * @param {Number} indent
264 * @param {Object} attributes
265 * @param {(item) => Object} itemAttributeGenerator
269 function OL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
270 const spacer
= '\t'.repeat(indent
);
271 return `${spacer}<ol${elementAttributes(attributes)}>
272 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
278 * Show any error or notice messages from context.
279 * @param {Object} ctx
280 * @param {String[]=} ctx.errors
281 * @param {String[]=} ctx.notifications
282 * @param {Object} options
283 * @param {String=} options.errorHeading
284 * @param {String[]=} options.errorContent
285 * @param {String=} options.notificationHeading
286 * @param {String[]=} options.notificationContent
288 function htmlMessages(ctx
, options
) {
289 const errorHeading
= options
&& options
.errorHeading
? `
290 \t<h2>${options.errorHeading}</h2>` : '';
291 const errorContent
= options
&& options
.errorContent
&& options
.errorContent
.length
? '\n' + options
.errorContent
.map(((content
) => `\t${content}`)).join('\n') : '';
292 const notificationHeading
= options
&& options
.notificationHeading
? `\n\t<h2>${options.notificationHeading}</h2>` : '';
293 const notificationContent
= options
&& options
.notificationContent
&& options
.notificationContent
.length
? '\n' + options
.notificationContent
.map(((content
) => `\t${content}`)).join('\n') : '';
294 const errors
= ctx
&& ctx
.errors
&& ctx
.errors
.length
? `
295 <section class="error">${errorHeading}${errorContent}
298 const notifications
= ctx
&& ctx
.notifications
&& ctx
.notifications
.length
? `
299 <section class="notification">${notificationHeading}${notificationContent}
300 ${UL(ctx.notifications, 1)}
302 return [errors
, notifications
].join('\n');
307 * Render all parts of an HTML page. Adds user logout nav link automatically.
308 * @param {Number} pagePathLevel - relative path-distance to base
309 * @param {Object} ctx
310 * @param {Object=} ctx.session
311 * @param {String=} ctx.session.authenticatedIdentifier
312 * @param {String=} ctx.session.authenticatedProfile
313 * @param {String[]=} ctx.errors
314 * @param {String[]=} ctx.notifications
315 * @param {Object} options
316 * @param {String=} options.pageTitle
317 * @param {String=} options.logoUrl
318 * @param {Object[]=} options.bodyAttributes
319 * @param {String[]=} options.headElements
320 * @param {Object[]=} options.navLinks
321 * @param {String[]=} options.footerEntries
322 * @param {String=} options.errorHeading
323 * @param {String[]=} options.errorContent
324 * @param {String=} options.notificationHeading
325 * @param {String[]=} options.notificationContent
326 * @param {String[]} main - contents
329 function htmlPage(pagePathLevel
, ctx
, options
, main
= []) {
330 const user
= (ctx
&& ctx
.session
&& ctx
.session
.authenticatedProfile
) || (ctx
&& ctx
.session
&& ctx
.session
.authenticatedIdentifier
);
332 if (!options
.navLinks
) {
333 options
.navLinks
= [];
335 const logoutRedirect
= ctx
&& ctx
.url
? `?r=${encodeURIComponent(ctx.url)}` : '';
336 const logoutPath
= (pagePathLevel
> 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
337 options
.navLinks
.push({
338 text: `Logout (${user})`,
339 href: `${logoutPath}logout${logoutRedirect}`,
346 htmlHead(pagePathLevel
, ctx
, options
),
347 htmlBody(pagePathLevel
, ctx
, options
, main
),