4 * A bunch of shorthand to put together common parts of an HTML page.
7 const { lazy
} = require('@squeep/lazy-property');
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 * We handle the infinities because pg-promise might provide those in
33 * lieu of a Date object from timestamp fields.
34 * @param {Date|Number|String} date
35 * @param {String=} pInf
36 * @param {String=} nInf
37 * @param {String=} otherwise
39 const dateFormat
= (date
, pInf
= 'Never', nInf
= 'Forever', otherwise
= '') => {
40 const isDatableType
= ['number', 'string'].includes(typeof date
);
48 || Number
.isNaN(date
.valueOf())
49 || (!(date
instanceof Date
) && !isDatableType
)) {
54 date
= new Date(date
);
56 const parts
= dateFormat
._dtf
.formatToParts(date
);
57 return parts
.map((p
) => p
.value
).join('');
59 lazy(dateFormat
, '_dtf', () => {
60 const dateTimeFormatOptions
= {
64 return new Intl
.DateTimeFormat(undefined, dateTimeFormatOptions
);
70 * @param {Number} seconds
73 const secondsToPeriod
= (seconds
) => {
77 const nextResult
= (factor
, label
) => {
78 const r
= factor
? value
% factor : value
;
80 result
.push(`${r} ${label}${r != 1 ? 's' : ''}`);
82 value
= factor
? Math
.floor(value
/ factor
) : value
;
85 nextResult(60, 'second');
86 nextResult(60, 'minute');
87 nextResult(24, 'hour');
88 nextResult(30, 'day');
89 nextResult(undefined, 'month');
92 return result
.join(' ');
97 * Return array of strings prefixed with tabs.
98 * @param {Number} indent
99 * @param {String[]} list
101 const indented
= (indent
, list
) => {
102 const spacer
= '\t'.repeat(indent
);
103 return list
.map((l
) => `${spacer}${l}`);
108 * Render the preamble <head> for an HTML page.
109 * @param {Number} pagePathLevel number of paths below root this page is
110 * @param {Object} ctx
111 * @param {Object} options
112 * @param {String[]=} options.headElements
113 * @param {String=} options.pageTitle
116 function htmlHead(pagePathLevel
, ctx
, options
) {
117 const rootPathPfx
= '../'.repeat(pagePathLevel
);
123 \t\t<meta charset="utf-8">
124 \t\t<meta name="viewport" content="width=device-width,initial-scale=1">
125 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css" title="Default">
126 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css" title="Site Specific">
127 ${headElements.map((e) => '\t\t' + e).join('\n')}
128 \t\t<title>${pageTitle}</title>
134 * Render the main content of an HTML page.
135 * @param {Number} pagePathLevel
136 * @param {Object} ctx
137 * @param {Object} options
138 * @param {Object} options.bodyAttributes
139 * @param {String[]} main
142 function htmlBody(pagePathLevel
, ctx
, options
, main
= []) {
146 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.
148 \t<body${elementAttributes(bodyAttributes)}>${firefoxFix}
149 ${htmlHeader(pagePathLevel, ctx, options)}
150 ${htmlMessages(ctx, options)}
154 ${htmlFooter(ctx, options)}
160 * Render a navigation link for the header section.
161 * @param {Object} nav
162 * @param {String} nav.href
163 * @param {String} nav.class
164 * @param {String} nav.text
167 function renderNavLink(nav
) {
168 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
173 * Render the navigation section of the page header.
174 * @param {Object} ctx
175 * @param {Object} options
176 * @param {Object[]=} options.navLinks
179 function htmlNav(ctx
, options
) {
181 const spacer
= '\t'.repeat(indent
);
185 return navLinks
.length
? `${spacer}<nav>
186 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
187 ${spacer}</nav>` : '';
192 * Render the banner and navigation header.
193 * @param {Number} pagePathLevel
194 * @param {Object} ctx
195 * @param {Object} options
196 * @param {String[]=} options.logoUrl
197 * @param {String[]=} options.pageTitle
200 function htmlHeader(pagePathLevel
, ctx
, options
) {
201 const rootPathPfx
= '../'.repeat(pagePathLevel
);
206 const logoImg
= logoUrl
? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
208 \t\t\t<h1>${logoImg}${pageTitle}</h1>
209 ${htmlNav(ctx, options)}
215 * Render the bottom boilerplate.
216 * @param {Object} ctx
217 * @param {Object} options
218 * @param {String[]} options.footerEntries
221 function htmlFooter(ctx
, options
) {
223 const spacer
= '\t'.repeat(indent
);
228 return footerEntries
.length
? `${spacer}<footer>
229 ${OL(footerEntries, indent + 1)}
230 ${spacer}</footer>` : '';
235 * Convert an object into element attributes.
236 * @param {Object} attributes
239 function elementAttributes(attributes
) {
240 const attr
= Object
.entries(attributes
).map(([name
, value
]) => `${name}="${value}"`).join(' ');
241 return attr
? ' ' + attr : '';
246 * Wrap an item in a list item element.
247 * @param {String} item
248 * @param {Number} indent
249 * @param {Object} attributes
252 function LI(item
, indent
= 0, attributes
= {}) {
253 const spacer
= '\t'.repeat(indent
);
254 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
259 * Wrap a list of items in an unordered list.
260 * @param {String[]} items
261 * @param {Number} indent
262 * @param {Object} attributes
263 * @param {(item) => Object} itemAttributeGenerator
266 function UL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
267 const spacer
= '\t'.repeat(indent
);
268 return `${spacer}<ul${elementAttributes(attributes)}>
269 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
275 * Wrap a list of items in an ordered list.
276 * @param {String[]} items
277 * @param {Number} indent
278 * @param {Object} attributes
279 * @param {(item) => Object} itemAttributeGenerator
283 function OL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
284 const spacer
= '\t'.repeat(indent
);
285 return `${spacer}<ol${elementAttributes(attributes)}>
286 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
292 * Show any error or notice messages from context.
293 * @param {Object} ctx
294 * @param {String[]=} ctx.errors
295 * @param {String[]=} ctx.notifications
296 * @param {Object} options
297 * @param {String=} options.errorHeading
298 * @param {String[]=} options.errorContent
299 * @param {String=} options.notificationHeading
300 * @param {String[]=} options.notificationContent
302 function htmlMessages(ctx
, options
) {
303 const errorHeading
= options
&& options
.errorHeading
? `
304 \t<h2>${options.errorHeading}</h2>` : '';
305 const errorContent
= options
&& options
.errorContent
&& options
.errorContent
.length
? '\n' + options
.errorContent
.map(((content
) => `\t${content}`)).join('\n') : '';
306 const notificationHeading
= options
&& options
.notificationHeading
? `\n\t<h2>${options.notificationHeading}</h2>` : '';
307 const notificationContent
= options
&& options
.notificationContent
&& options
.notificationContent
.length
? '\n' + options
.notificationContent
.map(((content
) => `\t${content}`)).join('\n') : '';
308 const errors
= ctx
&& ctx
.errors
&& ctx
.errors
.length
? `
309 <section class="error">${errorHeading}${errorContent}
312 const notifications
= ctx
&& ctx
.notifications
&& ctx
.notifications
.length
? `
313 <section class="notification">${notificationHeading}${notificationContent}
314 ${UL(ctx.notifications, 1)}
316 return [errors
, notifications
].join('\n');
321 * Render all parts of an HTML page. Adds user logout nav link automatically.
322 * @param {Number} pagePathLevel - relative path-distance to base
323 * @param {Object} ctx
324 * @param {Object=} ctx.session
325 * @param {String=} ctx.session.authenticatedIdentifier
326 * @param {String=} ctx.session.authenticatedProfile
327 * @param {String[]=} ctx.errors
328 * @param {String[]=} ctx.notifications
329 * @param {Object} options
330 * @param {String=} options.pageTitle
331 * @param {String=} options.logoUrl
332 * @param {Object[]=} options.bodyAttributes
333 * @param {String[]=} options.headElements
334 * @param {Object[]=} options.navLinks
335 * @param {String[]=} options.footerEntries
336 * @param {String=} options.errorHeading
337 * @param {String[]=} options.errorContent
338 * @param {String=} options.notificationHeading
339 * @param {String[]=} options.notificationContent
340 * @param {String[]} main - contents
343 function htmlPage(pagePathLevel
, ctx
, options
, main
= []) {
344 const user
= (ctx
&& ctx
.session
&& ctx
.session
.authenticatedProfile
) || (ctx
&& ctx
.session
&& ctx
.session
.authenticatedIdentifier
);
346 if (!options
.navLinks
) {
347 options
.navLinks
= [];
349 const logoutRedirect
= ctx
&& ctx
.url
? `?r=${encodeURIComponent(ctx.url)}` : '';
350 const logoutPath
= (pagePathLevel
> 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
351 options
.navLinks
.push({
352 text: `Logout (${user})`,
353 href: `${logoutPath}logout${logoutRedirect}`,
360 htmlHead(pagePathLevel
, ctx
, options
),
361 htmlBody(pagePathLevel
, ctx
, options
, main
),