);
};
/* ---------- Craft config ---------- */
Gallery.craft = {
displayName: 'Gallery',
props: {
images: defaultImages,
columns: 3,
gap: '16px',
style: {
backgroundColor: '#ffffff',
marginTop: '', marginRight: '', marginBottom: '', marginLeft: '',
paddingTop: '', paddingRight: '', paddingBottom: '', paddingLeft: '',
border: 'none',
boxShadow: 'none',
opacity: '1',
},
lightbox: false,
animation: '',
animationDelay: '0',
hideOnDesktop: false,
hideOnTablet: false,
hideOnMobile: false,
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
};
/* ---------- HTML export ---------- */
(Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string, nodeId?: string) => {
const sectionStyle = cssPropsToString({
padding: '60px 20px',
...props.style,
});
const images = props.images || defaultImages;
// Number() coercion: `columns` is a raw string-interpolation sink into the
// grid style attribute below (repeat(${columns},1fr)) -- a non-numeric
// (e.g. hand-crafted/AI-generated tree) value would otherwise be able to
// break out; Number() of anything non-numeric collapses safely to NaN.
const columns = Number(props.columns) || 3;
// Sanitized -- gap is a raw string-interpolation sink into the grid style
// attribute below.
const gap = cssValue(props.gap) || '16px';
const lightbox = props.lightbox || false;
// Deterministic AND unique id, scoped on the Craft node id, for this
// gallery's overlay/grid element ids and inline-script function names --
// so two Gallery instances (e.g. both left at default images) don't
// collide and end up sharing/clobbering one lightbox overlay.
const galleryId = scopeId(nodeId, JSON.stringify(images) + columns + gap, 'gallery');
const items = images.map((img) => {
const caption = img.caption
? `
${escapeHtml(img.caption)}
`
: '';
// Lightbox items carry the image URL as a data attribute rather than an
// inline onclick with an interpolated src -- a single delegated click
// listener below reads it, so a src containing a quote can't break out
// of a per-item event-handler string.
const lbAttr = lightbox ? ` data-lb-src="${escapeAttr(safeImageUrl(img.src || ''))}" role="button" tabindex="0"` : '';
const itemStyle = lightbox ? 'cursor:pointer;position:relative;overflow:hidden;border-radius:8px' : 'position:relative;overflow:hidden;border-radius:8px';
return `
${caption}
`;
}).join('\n ');
let lightboxHtml = '';
let gridIdAttr = '';
if (lightbox) {
gridIdAttr = ` id="${galleryId}_grid"`;
// M-2: focus management for the lightbox dialog.
// - OPEN: stash `document.activeElement` (the thumbnail that triggered
// the open) in a module-scoped var, then move focus onto the close
// button -- so a screen-reader/keyboard user lands inside the dialog
// instead of focus staying on (or silently falling back to )
// behind the now-visible overlay.
// - Tab trap: while the overlay is open, every Tab keypress is
// intercepted and refocuses the close button (the dialog's only
// focusable control besides Escape/click-to-close), so focus can
// never wander out into the page content hidden behind the overlay.
// - CLOSE (Escape, backdrop click, or the close button): restore focus
// to the element stashed on open.
lightboxHtml = `