4 * A bunch of shorthand to put together common parts of an HTML page.
8 * Some fields may have values outside normal dates, handle them here.
10 * @param {String} otherwise
12 const dateOrNot
= (date
, otherwise
) => {
16 if (typeof date
=== 'number') {
17 date
= new Date(date
);
19 const dateMs
= date
.getTime();
20 if (!Number
.isFinite(dateMs
)
24 return date
.toString();
30 * @param {Number} seconds
33 const secondsToPeriod
= (seconds
) => {
37 const nextResult
= (factor
, label
) => {
38 const r
= factor
? value
% factor : value
;
40 result
.push(`${r} ${label}${r != 1 ? 's' : ''}`);
42 value
= factor
? Math
.floor(value
/ factor
) : value
;
45 nextResult(60, 'second');
46 nextResult(60, 'minute');
47 nextResult(24, 'hour');
48 nextResult(30, 'day');
49 nextResult(undefined, 'month');
52 return result
.join(' ');
57 * Render the preamble <head> for an HTML page.
58 * @param {Number} pagePathLevel number of paths below root this page is
60 * @param {Object} options
61 * @param {String[]=} options.headElements
62 * @param {String=} options.pageTitle
65 function htmlHead(pagePathLevel
, ctx
, options
) {
66 const rootPathPfx
= '../'.repeat(pagePathLevel
);
72 \t\t<meta charset="utf-8">
73 \t\t<meta name="viewport" content="width=device-width,initial-scale=1">
74 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css">
75 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css">
76 ${headElements.map((e) => '\t\t' + e).join('\n')}
77 \t\t<title>${pageTitle}</title>
83 * Render the main content of an HTML page.
84 * @param {Number} pagePathLevel
86 * @param {Object} options
87 * @param {Object} options.bodyAttributes
88 * @param {String[]} main
91 function htmlBody(pagePathLevel
, ctx
, options
, main
= []) {
95 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.
97 \t<body${elementAttributes(bodyAttributes)}>${firefoxFix}
98 ${htmlHeader(pagePathLevel, ctx, options)}
99 ${htmlMessages(ctx, options)}
103 ${htmlFooter(ctx, options)}
109 * Render a navigation link for the header section.
110 * @param {Object} nav
111 * @param {String} nav.href
112 * @param {String} nav.class
113 * @param {String} nav.text
116 function renderNavLink(nav
) {
117 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
122 * Render the navigation section of the page header.
123 * @param {Object} ctx
124 * @param {Object} options
125 * @param {Object[]=} options.navLinks
128 function htmlNav(ctx
, options
) {
130 const spacer
= '\t'.repeat(indent
);
134 return navLinks
.length
? `${spacer}<nav>
135 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
136 ${spacer}</nav>` : '';
141 * Render the banner and navigation header.
142 * @param {Number} pagePathLevel
143 * @param {Object} ctx
144 * @param {Object} options
145 * @param {String[]=} options.logoUrl
146 * @param {String[]=} options.pageTitle
149 function htmlHeader(pagePathLevel
, ctx
, options
) {
150 const rootPathPfx
= '../'.repeat(pagePathLevel
);
155 const logoImg
= logoUrl
? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
157 \t\t\t<h1>${logoImg}${pageTitle}</h1>
158 ${htmlNav(ctx, options)}
164 * Render the bottom boilerplate.
165 * @param {Object} ctx
166 * @param {Object} options
167 * @param {String[]} options.footerEntries
170 function htmlFooter(ctx
, options
) {
172 const spacer
= '\t'.repeat(indent
);
177 return footerEntries
.length
? `${spacer}<footer>
178 ${OL(footerEntries, indent + 1)}
179 ${spacer}</footer>` : '';
184 * Convert an object into element attributes.
185 * @param {Object} attributes
188 function elementAttributes(attributes
) {
189 const attr
= Object
.entries(attributes
).map(([name
, value
]) => `${name}="${value}"`).join(' ');
190 return attr
? ' ' + attr : '';
195 * Wrap an item in a list item element.
196 * @param {String} item
197 * @param {Number} indent
198 * @param {Object} attributes
201 function LI(item
, indent
= 0, attributes
= {}) {
202 const spacer
= '\t'.repeat(indent
);
203 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
208 * Wrap a list of items in an unordered list.
209 * @param {String[]} items
210 * @param {Number} indent
211 * @param {Object} attributes
212 * @param {(item) => Object} itemAttributeGenerator
215 function UL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
216 const spacer
= '\t'.repeat(indent
);
217 return `${spacer}<ul${elementAttributes(attributes)}>
218 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
224 * Wrap a list of items in an ordered list.
225 * @param {String[]} items
226 * @param {Number} indent
227 * @param {Object} attributes
228 * @param {(item) => Object} itemAttributeGenerator
232 function OL(items
, indent
= 0, attributes
= {}, itemAttributeGenerator
= () => {}) {
233 const spacer
= '\t'.repeat(indent
);
234 return `${spacer}<ol${elementAttributes(attributes)}>
235 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
241 * Show any error or notice messages from context.
242 * @param {Object} ctx
243 * @param {String[]=} ctx.errors
244 * @param {String[]=} ctx.notifications
245 * @param {Object} options
246 * @param {String=} options.errorHeading
247 * @param {String[]=} options.errorContent
248 * @param {String=} options.notificationHeading
249 * @param {String[]=} options.notificationContent
251 function htmlMessages(ctx
, options
) {
252 const errorHeading
= options
&& options
.errorHeading
? `
253 \t<h2>${options.errorHeading}</h2>` : '';
254 const errorContent
= options
&& options
.errorContent
&& options
.errorContent
.length
? '\n' + options
.errorContent
.map(((content
) => `\t${content}`)).join('\n') : '';
255 const notificationHeading
= options
&& options
.notificationHeading
? `\n\t<h2>${options.notificationHeading}</h2>` : '';
256 const notificationContent
= options
&& options
.notificationContent
&& options
.notificationContent
.length
? '\n' + options
.notificationContent
.map(((content
) => `\t${content}`)).join('\n') : '';
257 const errors
= ctx
&& ctx
.errors
&& ctx
.errors
.length
? `
258 <section class="error">${errorHeading}${errorContent}
261 const notifications
= ctx
&& ctx
.notifications
&& ctx
.notifications
.length
? `
262 <section class="notification">${notificationHeading}${notificationContent}
263 ${UL(ctx.notifications, 1)}
265 return [errors
, notifications
].join('\n');
270 * Render all parts of an HTML page. Adds user logout nav link automatically.
271 * @param {Number} pagePathLevel - relative path-distance to base
272 * @param {Object} ctx
273 * @param {Object=} ctx.session
274 * @param {String=} ctx.session.authenticatedIdentifier
275 * @param {String=} ctx.session.authenticatedProfile
276 * @param {String[]=} ctx.errors
277 * @param {String[]=} ctx.notifications
278 * @param {Object} options
279 * @param {String=} options.pageTitle
280 * @param {String=} options.logoUrl
281 * @param {Object[]=} options.bodyAttributes
282 * @param {String[]=} options.headElements
283 * @param {Object[]=} options.navLinks
284 * @param {String[]=} options.footerEntries
285 * @param {String=} options.errorHeading
286 * @param {String[]=} options.errorContent
287 * @param {String=} options.notificationHeading
288 * @param {String[]=} options.notificationContent
289 * @param {String[]} main - contents
292 function htmlPage(pagePathLevel
, ctx
, options
, main
= []) {
293 const user
= (ctx
&& ctx
.session
&& ctx
.session
.authenticatedProfile
) || (ctx
&& ctx
.session
&& ctx
.session
.authenticatedIdentifier
);
295 if (!options
.navLinks
) {
296 options
.navLinks
= [];
298 const logoutRedirect
= ctx
&& ctx
.url
? `?r=${encodeURIComponent(ctx.url)}` : '';
299 const logoutPath
= (pagePathLevel
> 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
300 options
.navLinks
.push({
301 text: `Logout (${user})`,
302 href: `${logoutPath}logout${logoutRedirect}`,
309 htmlHead(pagePathLevel
, ctx
, options
),
310 htmlBody(pagePathLevel
, ctx
, options
, main
),