I'm building a custom Masonry (non-jQuery version) gallery block and I managed to make it work in the block editor. I'm using WP 6.3. This is roughly the code:
JS:
const containerRef = useRef(null);
const {
attributes: { items, columns, gap },
setAttributes,
} = props;
useEffect(() => {
var msnry;
if (containerRef.current) {
imagesLoaded(containerRef.current, function () {
msnry = new Masonry(containerRef.current, {
itemSelector: '.gallery-item',
columnWidth: '.grid-sizer',
percentPosition: true,
gutter: parseInt(gap),
});
});
}
return () => {
msnry?.destroy();
}
}, [items, columns, gap]);
....
const displayItems = (items) => {
return (
items.map((item, index) => {
return (
<div className="gallery-item" key={index}>
<figure>
<img className={`wp-image-${item.id}`} src={item.url} alt={item.alt} key={item.id} />
</figure>
</div>
)
})
)
};
{items.length > 0 ?
<div {...blockProps}>
<div className="gallery-items" style={{ '--gap': gap }} ref={containerRef}>
<div className="grid-sizer"></div>
{displayItems(items)}
</div>
</div>
}
SCSS:
.wp-block-my-masonry-gallery {
.gallery-items {
--gap: 10px;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
}
@for $i from 2 through 8 {
&.columns-#{$i} {
.gallery-item,
.grid-sizer {
width: calc(percentage(math.div(1, $i)) - var(--gap) + (var(--gap) / $i));
}
}
}
}
block.json
{
"apiVersion": 3,
"name": "my/masonry-gallery",
"title": "My Masonry Gallery",
"attributes": {
"items": {
"type": "array",
"default": []
},
"columns": {
"type": "number",
"minimum": 1,
"maximum": 8,
"default": 3
},
"gap": {
"type": "string",
"default": "10px"
}
},
"editorScript": [
"file:./index.js",
"imagesloaded",
"masonry"
],
"viewScript": [
"file:./view.js",
"imagesloaded",
"masonry"
],
"style": [
"file:./style-index.css",
"file:./view.css"
],
"editorStyle": "file:./index.css"
}
As I said before, the block works in the block editor, but when I try it in the new FSE template editor, the Masonry lib is not initialized correctly as the height of the Masonry container is set to zero and therefore the gallery images are overlapping the next blocks in the screen.
Block in the block editor:
Block in the FSE template editor:
Someone pointed me to the following post that mentions that the template editor is loaded in an iframe to isolate it from the rest of the admin screen and might cause issues with non-React libraries and suggest some changes to make them work in the iframed editor:
https://make.wordpress.org/core/2021/06/29/blocks-in-an-iframed-template-editor/
I refactored the code following the examples in the post but I’m getting the same results as before:
const containerRef = useRefEffect((element) => {
var msnry;
imagesLoaded(element, function () {
msnry = new Masonry(element, {
itemSelector: '.gallery-item',
columnWidth: '.grid-sizer',
percentPosition: true,
gutter: parseInt(gap),
});
});
return () => {
msnry?.destroy();
}
}, [items, columns, gap]);
Following @stokesman suggestions, I refactored the code to the following with no success:
const containerRef = useRefEffect((element) => {
var msnry;
const { ownerDocument } = element;
const { defaultView } = ownerDocument;
if ( ! defaultView.imagesLoaded || ! defaultView.Masonry ) {
return;
}
defaultView.imagesLoaded(element, function () {
msnry = new defaultView.Masonry(element, {
itemSelector: '.gallery-item',
columnWidth: '.grid-sizer',
percentPosition: true,
gutter: parseInt(gap),
});
});
return () => {
msnry?.destroy();
}
}, [items, columns, gap]);
The if ( ! defaultView.imagesLoaded || ! defaultView.Masonry )
check never evaluates to false
and the Masonry part is never executed.
Any ideas?
UPDATE (02/20/2024):
In order to inject the masonry and imagesloaded scripts into the iframe so they could be used from within the iframe, you must add them to the script
prop (not the editorScript
prop) in the block.json
file. Otherwise, they are loaded OUTSIDE the iframe for some reason.
...
"script": [
"imagesloaded",
"masonry"
],
"editorScript": "file:./index.js",
"viewScript": "file:./view.js",
...
new Masonry(…
it’d be something like:new element.ownerDocument.defaultView.Masonry(…
but you'd want some optional chaining or null checks in there as demonstrated in the article.element.ownerDocument.defaultView.imagesLoaded
andelement.ownerDocument.defaultView.Masonry
like you suggested I get anUncaught TypeError: s.imagesLoaded is not a function
error in the console.imagesLoaded
call I'd put thisif ( ! defaultView.imagesLoaded || ! defaultView.Masonry ) return;
. That assumes you've assignedconst { defaultView } = element.ownerDocument;
on a line before that. As stated in the last code example in that article: "Scripts are loaded asynchronously, so check that the script is loaded. After the dependencies have loaded, the block will re-render."undefined
and never change totrue
.apiVersion < 3
(or custom fields or other legacy metaboxes are displayed). I take it the refactored version no longer works even in the post editor? I've been doing a little experimenting and it seems like "We’ve loaded all front-end scripts in the iframe to fix these cases" is no longer true. Anyway, this might not even be the cause of the problem. So when you said it was working in the post editor that was withapiVersion: 3
?