0

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:

enter image description here

Block in the FSE template editor:

enter image description here

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",
...
16
  • Is there any errors in the console? Without some idea of what the error is this a shot in the dark but the last section of that article mentions using the script from within the iframe. So in your case instead of 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.
    – stokesman
    Commented Aug 19, 2023 at 19:38
  • No errors in the console with my code. But, If I change it to element.ownerDocument.defaultView.imagesLoaded and element.ownerDocument.defaultView.Masonry like you suggested I get an Uncaught TypeError: s.imagesLoaded is not a function error in the console.
    – leemon
    Commented Aug 23, 2023 at 9:01
  • Sorry if I wasn't clear enough about the null check. On a line before the imagesLoaded call I'd put this if ( ! defaultView.imagesLoaded || ! defaultView.Masonry ) return;. That assumes you've assigned const { 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."
    – stokesman
    Commented Aug 23, 2023 at 14:33
  • I added that check to my code but then the code after it it's never executed. It seems both conditions evaluate to undefined and never change to true.
    – leemon
    Commented Aug 23, 2023 at 15:32
  • 1
    Yes, and starting with 6.3 even the post editor is iframed unless some registered blocks have 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 with apiVersion: 3?
    – stokesman
    Commented Aug 23, 2023 at 19:29

1 Answer 1

1

Your code isn't at fault. The Masonry library doesn't support this cross frame scenario. I can think of three ways you might get past this:

  1. Inject the masonry script into the iframe so it could be used from within the iframe
  2. Patch the masonry script and ship that version with your plugin
  3. Find an alternative library that doesn't have the issue

The third option might be fruitless. I'm not sure there are many options out there and they might have the same sort of issues.

The second option I've tried. I patched a couple places in the library and it seems to work okay now. I'm not sure it's 100% covered i.e. you might encounter an issue with some configuration I haven't tried. Here’s a gist with the patched version if you want to try it. In WordPress, you'll have to deregister the masonry script they bundled and register that one of course.

The first option might be a good one but you'd have to be sure to only try injecting the script into the iframe when there actually is one and you'd have to make sure your block waits to be sure the script has been made available in the iframe. There may be other potential gotchas as well.

On a side note, I'm considering submitting PRs to the libraries (the parts I patched are in separate dependencies (fizzy-ui-utils and outlayer) but not sure if/when they'd make it into a release.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.