X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Ftemplate-helper.js;h=c72e0ed348415d2c8adf6f1366b1639330819f65;hb=HEAD;hp=00937a0d554318e026b2083190379cead7eaa71c;hpb=4ab85bf22ece77cccd4e92435208bca5858816f6;p=squeep-html-template-helper diff --git a/lib/template-helper.js b/lib/template-helper.js index 00937a0..f2f4efe 100644 --- a/lib/template-helper.js +++ b/lib/template-helper.js @@ -4,10 +4,24 @@ * A bunch of shorthand to put together common parts of an HTML page. */ +const { lazy } = require('@squeep/lazy-property'); + + +/** + * Set up expected fields for how we handle error reporting + * and whatnot. + * @param {object} ctx context + */ +const initContext = (ctx) => { + ctx.errors = []; + ctx.notifications = []; +}; + /** * Some fields may have values outside normal dates, handle them here. - * @param {Date} date - * @param {String} otherwise + * @param {Date|number} date potential Date or epoch milliseconds + * @param {string} otherwise returned if date is not a date + * @returns {string} date or otherwise */ const dateOrNot = (date, otherwise) => { if (!date) { @@ -25,10 +39,77 @@ const dateOrNot = (date, otherwise) => { }; +/** + * Why is rendering a Date as a string this complicated? + * We handle the infinities because pg-promise might provide those in + * lieu of a Date object from timestamp fields. + * @param {Date|number|string} date Date, epoch milliseconds, or date string + * @param {string=} pInf returned if date is +Infinity + * @param {string=} nInf returned if date is -Infinity + * @param {string=} otherwise returned if date is something else + * @returns {string} rendered date + */ +const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => { + const isDatableType = ['number', 'string'].includes(typeof date); + switch (date) { + case Infinity: + return pInf; + case -Infinity: + return nInf; + default: + if (!date + || Number.isNaN(date.valueOf()) + || (!(date instanceof Date) && !isDatableType)) { + return otherwise; + } + } + if (isDatableType) { + date = new Date(date); + } + const parts = dateFormat._dtf.formatToParts(date); + return parts.map((p) => p.value).join(''); +}; +lazy(dateFormat, '_dtf', () => { + const dateTimeFormatOptions = { + dateStyle: 'medium', + timeStyle: 'long', + }; + return new Intl.DateTimeFormat(undefined, dateTimeFormatOptions); +}); + + +/** + * Wrap a Date in a ', + ].join(''); +}; + + /** * Render a duration. - * @param {Number} seconds - * @returns {String} + * @param {number} seconds duration + * @returns {string} readable duration */ const secondsToPeriod = (seconds) => { let value = seconds; @@ -40,7 +121,7 @@ const secondsToPeriod = (seconds) => { result.push(`${r} ${label}${r != 1 ? 's' : ''}`); } value = factor ? Math.floor(value / factor) : value; - } + }; nextResult(60, 'second'); nextResult(60, 'minute'); @@ -53,14 +134,26 @@ const secondsToPeriod = (seconds) => { }; +/** + * Return array of strings prefixed with tabs. + * @param {number} indent depth + * @param {string[]} list items to indent by depth + * @returns {string[]} indented items + */ +const indented = (indent, list) => { + const spacer = '\t'.repeat(indent); + return list.map((l) => `${spacer}${l}`); +}; + + /** * Render the preamble for an HTML page. - * @param {Number} pagePathLevel number of paths below root this page is - * @param {Object} ctx - * @param {Object} options - * @param {String[]=} options.headElements - * @param {String=} options.pageTitle - * @returns {String} + * @param {number} pagePathLevel number of paths below root this page is + * @param {object} ctx context + * @param {object} options options + * @param {string[]=} options.headElements element strings to add to html head element + * @param {string=} options.pageTitle title of page + * @returns {string} populated head element */ function htmlHead(pagePathLevel, ctx, options) { const rootPathPfx = '../'.repeat(pagePathLevel); @@ -70,6 +163,7 @@ function htmlHead(pagePathLevel, ctx, options) { } = options; return `\t \t\t +\t\t \t\t \t\t ${headElements.map((e) => '\t\t' + e).join('\n')} @@ -80,19 +174,20 @@ ${headElements.map((e) => '\t\t' + e).join('\n')} /** * Render the main content of an HTML page. - * @param {Number} pagePathLevel - * @param {Object} ctx - * @param {Object} options - * @param {Object} options.bodyAttributes - * @param {String[]} main - * @returns {String} + * @param {number} pagePathLevel number of subdirs under root this page is + * @param {object} ctx context + * @param {object} options options + * @param {object} options.bodyAttributes attributes to set on body element + * @param {string[]} main main content strings + * @returns {string} body element */ function htmlBody(pagePathLevel, ctx, options, main = []) { const { bodyAttributes = {}, } = options; + const firefoxFix = '\n'; // This fixes a layout rendering flash on load in Firefox; do not know why this works, but it does. return ` -\t +\t${firefoxFix} ${htmlHeader(pagePathLevel, ctx, options)} ${htmlMessages(ctx, options)} \t\t
@@ -105,23 +200,24 @@ ${htmlFooter(ctx, options)} /** * Render a navigation link for the header section. - * @param {Object} nav - * @param {String} nav.href - * @param {String} nav.class - * @param {String} nav.text - * @returns {String} + * @param {object} nav navigation object + * @param {string} nav.href href of link + * @param {string} nav.class class for anchor element + * @param {string} nav.text text of link + * @returns {string} a element */ function renderNavLink(nav) { - return `${nav.text}`; + const aClass = nav.class ? ` class="${nav.class}"` : ''; + return `${nav.text}`; } /** * Render the navigation section of the page header. - * @param {Object} ctx - * @param {Object} options - * @param {Object[]=} options.navLinks - * @returns {String} + * @param {object} ctx context + * @param {object} options options + * @param {object[]=} options.navLinks array of navlink objects + * @returns {string} nav element */ function htmlNav(ctx, options) { const indent = 3; @@ -137,20 +233,22 @@ ${spacer}` : ''; /** * Render the banner and navigation header. - * @param {Number} pagePathLevel - * @param {Object} ctx - * @param {Object} options - * @param {String[]=} options.logoUrl - * @param {String[]=} options.pageTitle - * @returns {String} + * @param {number} pagePathLevel number of subdirs under root this page is + * @param {object} ctx context + * @param {object} options options + * @param {string[]=} options.logoUrl url to logo image + * @param {string[]=} options.logoAlt alt for logo image + * @param {string[]=} options.pageTitle page title + * @returns {string} header element */ function htmlHeader(pagePathLevel, ctx, options) { - const rootPathPfx = '../'.repeat(Math.max(pagePathLevel - 1, 0)); + const rootPathPfx = '../'.repeat(pagePathLevel); const { logoUrl = '', + logoAlt = 'logo', pageTitle = '', } = options; - const logoImg = logoUrl ? `` : ''; + const logoImg = logoUrl ? `logo` : ''; return `\t\t
\t\t\t

${logoImg}${pageTitle}

${htmlNav(ctx, options)} @@ -160,10 +258,10 @@ ${htmlNav(ctx, options)} /** * Render the bottom boilerplate. - * @param {Object} ctx - * @param {Object} options - * @param {String[]} options.footerEntries - * @returns {String} + * @param {object} ctx context + * @param {object} options options + * @param {string[]} options.footerEntries footer entries + * @returns {string} footer element */ function htmlFooter(ctx, options) { const indent = 2; @@ -180,21 +278,24 @@ ${spacer}` : ''; /** * Convert an object into element attributes. - * @param {Object} attributes - * @returns {String} + * @param {object} attributes attributes + * @returns {string} formatted attributes */ function elementAttributes(attributes) { - const attr = Object.entries(attributes).map(([name, value]) => `${name}="${value}"`).join(' '); + const attr = Object.entries(attributes).map(([name, value]) => { + const v = value ? `="${value}"` : ''; + return `${name}${v}`; + }).join(' '); return attr ? ' ' + attr : ''; } /** * Wrap an item in a list item element. - * @param {String} item - * @param {Number} indent - * @param {Object} attributes - * @returns {String} + * @param {string} item text of item + * @param {number} indent indent level + * @param {object} attributes attributes for li element + * @returns {string} li element */ function LI(item, indent = 0, attributes = {}) { const spacer = '\t'.repeat(indent); @@ -203,60 +304,87 @@ function LI(item, indent = 0, attributes = {}) { /** - * Wrap a list of items in an unordered list. - * @param {String[]} items - * @param {Number} indent - * @param {Object} attributes - * @param {(item) => Object} itemAttributeGenerator - * @returns {String} + * @typedef {(item: any, index: number, array: any[]) => object} ItemAttributeGenerator */ -function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) { + +/** + * Wrap an array of items in a list container element. + * @param {string} element element type, e.g. OL, UL + * @param {number} indent indent level + * @param {object} attributes attributes for list element + * @param {string[]} items list entry elements + * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given list entry element + * @returns {string} list type element + */ +function listContainer(element, indent, attributes, items, itemAttributeGenerator) { const spacer = '\t'.repeat(indent); - return `${spacer} -${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')} -${spacer}`; + return `${spacer}<${element}${elementAttributes(attributes)}> +${items.map((item, index, array) => LI(item, indent + 1, itemAttributeGenerator(item, index, array))).join('\n')} +${spacer}`; } +// eslint-disable-next-line jsdoc/require-returns-check /** - * Wrap a list of items in an ordered list. - * @param {String[]} items - * @param {Number} indent - * @param {Object} attributes - * @param {(item) => Object} itemAttributeGenerator - * @returns {String} + * @param {any} item item + * @param {number} index item index + * @param {Array} array items + * @returns {object=} attribute map */ +function _defaultItemAttributeGenerator(item, index, array) { // eslint-disable-line no-unused-vars + return; +} -function OL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) { - const spacer = '\t'.repeat(indent); - return `${spacer} -${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')} -${spacer}`; + +/** + * Wrap a list of items in an unordered list. + * @param {string[]} items list item elements + * @param {number} indent indent level + * @param {object} attributes ul element attributes + * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given list entry element + * @returns {string} ul element + */ +function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = _defaultItemAttributeGenerator) { + return listContainer('ul', indent, attributes, items, itemAttributeGenerator); +} + + +/** + * Wrap a list of items in an ordered list. + * @param {string[]} items list item elements + * @param {number} indent indent level + * @param {object} attributes ol element attributes + * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given liste entry element + * @returns {string} ol element + */ +function OL(items, indent = 0, attributes = {}, itemAttributeGenerator = _defaultItemAttributeGenerator) { + return listContainer('ol', indent, attributes, items, itemAttributeGenerator); } /** * Show any error or notice messages from context. - * @param {Object} ctx - * @param {String[]=} ctx.errors - * @param {String[]=} ctx.notifications - * @param {Object} options - * @param {String=} options.errorHeading - * @param {String[]=} options.errorContent - * @param {String=} options.notificationHeading - * @param {String[]=} options.notificationContent + * @param {object} ctx context + * @param {string[]=} ctx.errors errors to render + * @param {string[]=} ctx.notifications notifications to render + * @param {object} options options + * @param {string=} options.errorHeading additional heading when rendering errors + * @param {string[]=} options.errorContent additional text when rendering errors + * @param {string=} options.notificationHeading additional heading when rendering notifications + * @param {string[]=} options.notificationContent additional text when rendering notifications + * @returns {string} h2 element and section element */ function htmlMessages(ctx, options) { - const errorHeading = options && options.errorHeading ? ` + const errorHeading = options?.errorHeading ? ` \t

${options.errorHeading}

` : ''; - const errorContent = options && options.errorContent && options.errorContent.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : ''; - const notificationHeading = options && options.notificationHeading ? `\n\t

${options.notificationHeading}

` : ''; - const notificationContent = options && options.notificationContent && options.notificationContent.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : ''; - const errors = ctx && ctx.errors && ctx.errors.length ? ` + const errorContent = options?.errorContent?.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : ''; + const notificationHeading = options?.notificationHeading ? `\n\t

${options.notificationHeading}

` : ''; + const notificationContent = options?.notificationContent?.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : ''; + const errors = ctx?.errors?.length ? `
${errorHeading}${errorContent} ${UL(ctx.errors, 1)}
` : ''; - const notifications = ctx && ctx.notifications && ctx.notifications.length ? ` + const notifications = ctx?.notifications?.length ? `
${notificationHeading}${notificationContent} ${UL(ctx.notifications, 1)}
` : ''; @@ -265,42 +393,27 @@ ${UL(ctx.notifications, 1)} /** - * Render all parts of an HTML page. Adds user logout nav link automatically. - * @param {Number} pagePathLevel - relative path-distance to base - * @param {Object} ctx - * @param {Object=} ctx.session - * @param {String=} ctx.session.authenticatedIdentifier - * @param {String=} ctx.session.authenticatedProfile - * @param {String[]=} ctx.errors - * @param {String[]=} ctx.notifications - * @param {Object} options - * @param {String=} options.pageTitle - * @param {String=} options.logoUrl - * @param {Object[]=} options.bodyAttributes - * @param {String[]=} options.headElements - * @param {Object[]=} options.navLinks - * @param {String[]=} options.footerEntries - * @param {String=} options.errorHeading - * @param {String=} options.errorContent - * @param {String=} options.notificationHeading - * @param {String=} options.notificationContent - * @param {String[]} main - contents - * @returns {String} + * Render all parts of an HTML page. + * @param {number} pagePathLevel relative path-distance to base + * @param {object} ctx context + * @param {string[]=} ctx.errors errors + * @param {string[]=} ctx.notifications notifications + * @param {object} options options + * @param {string=} options.pageTitle page title + * @param {string=} options.logoUrl logo url + * @param {string=} options.logoAlt logo alt + * @param {object[]=} options.bodyAttributes body attributes + * @param {string[]=} options.headElements head element elements + * @param {object[]=} options.navLinks nav objects + * @param {string[]=} options.footerEntries footers + * @param {string=} options.errorHeading errors heading + * @param {string[]=} options.errorContent errors text + * @param {string=} options.notificationHeading notifications heading + * @param {string[]=} options.notificationContent notifications text + * @param {string[]} main array of contents strings + * @returns {string} html page */ function htmlPage(pagePathLevel, ctx, options, main = []) { - const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier); - if (user) { - if (!options.navLinks) { - options.navLinks = []; - } - const logoutRedirect = ctx && ctx.url ? `?r=${encodeURIComponent(ctx.url)}` : ''; - const logoutPath = (pagePathLevel > 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/'; - options.navLinks.push({ - text: `Logout (${user})`, - href: `${logoutPath}logout${logoutRedirect}`, - }); - } - return [ '', '', @@ -312,7 +425,10 @@ function htmlPage(pagePathLevel, ctx, options, main = []) { module.exports = { + initContext, dateOrNot, + dateFormat, + timeElement, secondsToPeriod, htmlHead, htmlBody, @@ -320,9 +436,12 @@ module.exports = { htmlHeader, htmlFooter, htmlMessages, + indented, renderNavLink, LI, + listContainer, UL, OL, htmlPage, + elementAttributes, };