deb3a37fc183a4af3cecdea6dda250d4d8193bab
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 * 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 * Render the preamble <head> for an HTML page.
98 * @param {Number} pagePathLevel number of paths below root this page is
100 * @param {Object} options
101 * @param {String[]=} options.headElements
102 * @param {String=} options.pageTitle
105 function htmlHead(pagePathLevel
, ctx
, options
) {
106 const rootPathPfx
= '../'.repeat(pagePathLevel
);
112 \t\t<meta charset="utf-8">
113 \t\t<meta name="viewport" content="width=device-width,initial-scale=1">
114 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css" title="Default">
115 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css" title="Site Specific">
116 ${headElements.map((e) => '\t\t' + e).join('\n')}
117 \t\t<title>${pageTitle}</title>
123 * Render the main content of an HTML page.
124 * @param {Number} pagePathLevel
125 * @param {Object} ctx
126 * @param {Object} options
127 * @param {Object} options.bodyAttributes
128 * @param {String[]} main
131 function htmlBody(pagePathLevel
, ctx
, options
, main
= []) {
135 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.
137 \t<body${elementAttributes(bodyAttributes)}>${firefoxFix}
138 ${htmlHeader(pagePathLevel, ctx, options)}
139 ${htmlMessages(ctx, options)}
143 ${htmlFooter(ctx, options)}
149 * Render a navigation link for the header section.
150 * @param {Object} nav
151 * @param {String} nav.href
152 * @param {String} nav.class
153 * @param {String} nav.text
156 function renderNavLink(nav
) {
157 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
162 * Render the navigation section of the page header.
163 * @param {Object} ctx
164 * @param {Object} options
165 * @param {Object[]=} options.navLinks
168 function htmlNav(ctx
, options
) {
170 const spacer
= '\t'.repeat(indent
);
174 return navLinks
.length
? `${spacer}<nav>
175 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
176 ${spacer}</nav>` : '';
181 * Render the banner and navigation header.
182 * @param {Number} pagePathLevel
183 * @param {Object} ctx
184 * @param {Object} options
185 * @param {String[]=} options.logoUrl
186 * @param {String[]=} options.pageTitle
189 function htmlHeader(pagePathLevel
, ctx
, options
) {
190 const rootPathPfx
= '../'.repeat(pagePathLevel
);
195 const logoImg
= logoUrl
? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
197 \t\t\t<h1>${logoImg}${pageTitle}</h1>
198 ${htmlNav(ctx, options)}
204 * Render the bottom boilerplate.
205 * @param {Object} ctx
206 * @param {Object} options
207 * @param {String[]} options.footerEntries
210 function htmlFooter(ctx
, options
) {
212 const spacer
= '\t'.repeat(indent
);
217 return footerEntries
.length
? `${spacer}<footer>
218 ${OL(footerEntries, indent + 1)}
219 ${spacer}</footer>` : '';
224 * Convert an object into element attributes.
225 * @param {Object} attributes
228 function elementAttributes(attributes
) {
229 const attr
= Object
.entries(attributes
).map(([name
, value
]) => `${name}="${value}"`).join(' ');
230 return attr
? ' ' + attr : '';
235 * Wrap an item in a list item element.
236 * @param {String} item
237 * @param {Number} indent
238 * @param {Object} attributes
241 function LI(item
, indent
= 0, attributes
= {}) {
242 const spacer
= '\t'.repeat(indent
);
243 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
248 * Wrap a list of items in an unordered list.
249 * @param {String[]} items
250 * @param {Number} indent
251 * @param {Object} attributes
252 * @param {(item) => Object} itemAttributeGenerator
255 function UL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
256 const spacer
= '\t'.repeat(indent
);
257 return `${spacer}<ul${elementAttributes(attributes)}>
258 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
264 * Wrap a list of items in an ordered list.
265 * @param {String[]} items
266 * @param {Number} indent
267 * @param {Object} attributes
268 * @param {(item) => Object} itemAttributeGenerator
272 function OL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
273 const spacer
= '\t'.repeat(indent
);
274 return `${spacer}<ol${elementAttributes(attributes)}>
275 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
281 * Show any error or notice messages from context.
282 * @param {Object} ctx
283 * @param {String[]=} ctx.errors
284 * @param {String[]=} ctx.notifications
285 * @param {Object} options
286 * @param {String=} options.errorHeading
287 * @param {String[]=} options.errorContent
288 * @param {String=} options.notificationHeading
289 * @param {String[]=} options.notificationContent
291 function htmlMessages(ctx
, options
) {
292 const errorHeading
= options
&& options
.errorHeading
? `
293 \t<h2>${options.errorHeading}</h2>` : '';
294 const errorContent
= options
&& options
.errorContent
&& options
.errorContent
.length
? '\n' + options
.errorContent
.map(((content
) => `\t${content}`)).join('\n') : '';
295 const notificationHeading
= options
&& options
.notificationHeading
? `\n\t<h2>${options.notificationHeading}</h2>` : '';
296 const notificationContent
= options
&& options
.notificationContent
&& options
.notificationContent
.length
? '\n' + options
.notificationContent
.map(((content
) => `\t${content}`)).join('\n') : '';
297 const errors
= ctx
&& ctx
.errors
&& ctx
.errors
.length
? `
298 <section class="error">${errorHeading}${errorContent}
301 const notifications
= ctx
&& ctx
.notifications
&& ctx
.notifications
.length
? `
302 <section class="notification">${notificationHeading}${notificationContent}
303 ${UL(ctx.notifications, 1)}
305 return [errors
, notifications
].join('\n');
310 * Render all parts of an HTML page. Adds user logout nav link automatically.
311 * @param {Number} pagePathLevel - relative path-distance to base
312 * @param {Object} ctx
313 * @param {Object=} ctx.session
314 * @param {String=} ctx.session.authenticatedIdentifier
315 * @param {String=} ctx.session.authenticatedProfile
316 * @param {String[]=} ctx.errors
317 * @param {String[]=} ctx.notifications
318 * @param {Object} options
319 * @param {String=} options.pageTitle
320 * @param {String=} options.logoUrl
321 * @param {Object[]=} options.bodyAttributes
322 * @param {String[]=} options.headElements
323 * @param {Object[]=} options.navLinks
324 * @param {String[]=} options.footerEntries
325 * @param {String=} options.errorHeading
326 * @param {String[]=} options.errorContent
327 * @param {String=} options.notificationHeading
328 * @param {String[]=} options.notificationContent
329 * @param {String[]} main - contents
332 function htmlPage(pagePathLevel
, ctx
, options
, main
= []) {
333 const user
= (ctx
&& ctx
.session
&& ctx
.session
.authenticatedProfile
) || (ctx
&& ctx
.session
&& ctx
.session
.authenticatedIdentifier
);
335 if (!options
.navLinks
) {
336 options
.navLinks
= [];
338 const logoutRedirect
= ctx
&& ctx
.url
? `?r=${encodeURIComponent(ctx.url)}` : '';
339 const logoutPath
= (pagePathLevel
> 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
340 options
.navLinks
.push({
341 text: `Logout (${user})`,
342 href: `${logoutPath}logout${logoutRedirect}`,
349 htmlHead(pagePathLevel
, ctx
, options
),
350 htmlBody(pagePathLevel
, ctx
, options
, main
),