mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-25 05:14:08 +00:00
136 lines
3.1 KiB
JavaScript
136 lines
3.1 KiB
JavaScript
import {
|
|
catalog,
|
|
getDescriptor,
|
|
CONTENTCOMPONENT_ATTR,
|
|
CONTENTCOMPONENT_MAX
|
|
} from './catalog.js';
|
|
|
|
// Runs over the lifetime of the page. Two identical markers in one content therefore get
|
|
// two distinct keys, which Vue needs to keep their Teleports apart.
|
|
let keyCounter = 0;
|
|
|
|
// One log line per problem, not one per render.
|
|
const reported = new Set();
|
|
|
|
function report(message)
|
|
{
|
|
if (reported.has(message))
|
|
return;
|
|
reported.add(message);
|
|
console.error('Contentcomponent: ' + message);
|
|
}
|
|
|
|
/**
|
|
* Turns one attribute string into the declared type. Returns undefined on a bad value.
|
|
*/
|
|
function coerce(raw, spec)
|
|
{
|
|
switch (spec.type)
|
|
{
|
|
case 'number':
|
|
{
|
|
const value = Number(raw);
|
|
return (raw === '' || Number.isNaN(value)) ? undefined : value;
|
|
}
|
|
case 'boolean':
|
|
return raw === 'true' || raw === '1';
|
|
default:
|
|
return raw;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the props of one marker. Only an attribute named in the schema is read. Every
|
|
* other attribute is dropped. This allow-list is the boundary between the content and
|
|
* the component.
|
|
*/
|
|
function readProps(el, descriptor)
|
|
{
|
|
const props = {};
|
|
|
|
for (const [name, spec] of Object.entries(descriptor.props || {}))
|
|
{
|
|
const raw = el.getAttribute('data-' + name);
|
|
|
|
if (raw === null)
|
|
{
|
|
if (spec.required)
|
|
{
|
|
report('"' + descriptor.name + '" misses the required property "' + name + '".');
|
|
return null;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const value = coerce(raw, spec);
|
|
if (value === undefined)
|
|
{
|
|
report('"' + descriptor.name + '" got the value "' + raw + '" for "' + name
|
|
+ '", which is not a ' + spec.type + '.');
|
|
return null;
|
|
}
|
|
|
|
props[name] = value;
|
|
}
|
|
|
|
return props;
|
|
}
|
|
|
|
/**
|
|
* Finds every marker below root and returns the ones that can be rendered.
|
|
* An unknown name or a bad property is logged and skipped, never shown to a reader.
|
|
*
|
|
* @param {Element} root
|
|
* @return {Array<{el: Element, key: string, name: string, props: Object}>}
|
|
*/
|
|
export function scanContentcomponents(root)
|
|
{
|
|
if (!root)
|
|
return [];
|
|
|
|
const elements = Array.from(root.querySelectorAll('[' + CONTENTCOMPONENT_ATTR + ']'));
|
|
|
|
if (elements.length > CONTENTCOMPONENT_MAX)
|
|
{
|
|
report('a content holds ' + elements.length + ' components. Only the first '
|
|
+ CONTENTCOMPONENT_MAX + ' are rendered.');
|
|
}
|
|
|
|
const markers = [];
|
|
|
|
for (const el of elements.slice(0, CONTENTCOMPONENT_MAX))
|
|
{
|
|
const name = el.getAttribute(CONTENTCOMPONENT_ATTR);
|
|
const descriptor = getDescriptor(name);
|
|
|
|
if (!descriptor)
|
|
{
|
|
report('"' + name + '" is not in the catalog. Known: '
|
|
+ catalog.map(entry => entry.name).join(', ') + '.');
|
|
continue;
|
|
}
|
|
|
|
const props = readProps(el, descriptor);
|
|
if (props === null)
|
|
continue;
|
|
|
|
// A fresh marker gets a key and loses its inner HTML. Teleport adds its children
|
|
// and keeps the existing ones, so a fallback text would stay under the component.
|
|
// A marker that already carries a key holds a mounted component. Do not clear it.
|
|
if (!el.dataset.fhcComponentKey)
|
|
{
|
|
el.dataset.fhcComponentKey = String(++keyCounter);
|
|
el.innerHTML = '';
|
|
}
|
|
|
|
markers.push({
|
|
el: el,
|
|
key: el.dataset.fhcComponentKey,
|
|
name: name,
|
|
props: props
|
|
});
|
|
}
|
|
|
|
return markers;
|
|
}
|