ACF Flexible Content image previews, 2.0

We’ve been using this gist for years now for creating image previews for ACF Flexible Content components. Very recently it just stopped working, however. Our best guess is that it is a race condition of some sort, most likely caused by the DOM being generated in JS later.

The fix is overall pretty simple, just use a MutationObserver to watch for new specific DOM items and bind the listener. While we were in there, we also took the time to yank our jQuery since we’re in the admin and guaranteed to be in a modern-enough browser. We also did some background loading with fallback to a transparent PNG, just in case we missed a specific thumbnail, which could potentially happen for deeply nested components.

The CSS and PHP are identical to the gist still (although you could remove jQuery as a dependency now), so only the JS needs to be replaced with this:

/*jslint maxparams: 4, maxdepth: 4, maxstatements: 20, maxcomplexity: 8 */
(function (w) {

    let
        hoverIndex = 0
    ;

    const

        document = w.document,

        ATTRIBUTE_FOR_EVENT_ALREADY_BOUND = 'data-vendi-popup-bound',

        getBlankImageSrc = () => {
            return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
        },

        setImage = (img, imagePath, thisIndex) => {
            const g = new Image();

            g
                .addEventListener(
                    'load',
                    () => {
                        if (thisIndex === hoverIndex) {
                            img.src = g.src;
                        }
                    }
                )
            ;

            g
                .addEventListener(
                    'error',
                    () => {
                        if (thisIndex === hoverIndex) {
                            img.src = getBlankImageSrc();
                        }
                    }
                )
            ;

            g.src = imagePath;
        },

        createPreview = (outerNode) => {
            const outerDiv = document.createElement('div');
            outerDiv.classList.add('preview');

            const innerDiv = document.createElement('div');
            innerDiv.classList.add('inner-preview');

            const img = document.createElement('img');

            innerDiv.appendChild(img);
            outerDiv.appendChild(innerDiv);

            outerNode.appendChild(outerDiv);
            return outerDiv;
        },

        getPreview = (outerNode) => {
            return outerNode.querySelector('.preview');
        },

        getOrCreatePreview = (outerNode) => {
            return getPreview(outerNode) || createPreview(outerNode);
        },

        doStuffWithThing = (outerNode, link) => {
            const filename = link.getAttribute('data-layout');
            const preview = getOrCreatePreview(outerNode);
            const img = preview.querySelector('img');
            const thisIndex = ++hoverIndex;
            const imagePath = w.theme_var.upload + filename + '.png';
            setImage(img, imagePath, thisIndex);
        },

        lookForNewThings = () => {
            const nodes = document.querySelectorAll('.acf-fc-popup');

            Array
                .from(nodes)
                .forEach(
                    (node) => {
                        Array
                            .from(node.querySelectorAll('li a'))
                            .forEach(
                                (link) => {

                                    if (link.hasAttribute(ATTRIBUTE_FOR_EVENT_ALREADY_BOUND)) {
                                        return;
                                    }

                                    link.setAttribute(ATTRIBUTE_FOR_EVENT_ALREADY_BOUND, 'true');

                                    link
                                        .addEventListener(
                                            'mouseover',
                                            () => {
                                                doStuffWithThing(node, link);
                                            }
                                        )
                                    ;
                                }
                            )
                        ;
                    }
                )
            ;
        },

        onload = () => {

            if (!w.theme_var || !w.theme_var.upload) {
                return;
            }

            const target = document.body;
            const config = {attributes: true, childList: true, subtree: true, attributeFilter: ['class']};

            const callback = function (mutationsList, observer) {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        // Look for at least one ACF popup item added
                        const foundNewThings = Array
                            .from(mutation.addedNodes)
                            .some(
                                (node) => {
                                    return node.classList && node.classList.contains('acf-fc-popup');
                                }
                            )

                        // If we found at least one, call a global bind.
                        // Technically we could bind to each specific item found but it is cleaner
                        // to just re-spider the entire DOM. Perf shouldn't be affected by this.
                        if (foundNewThings) {
                            lookForNewThings();
                            return;
                        }
                    }
                }
            };

            const observer = new MutationObserver(callback);
            observer.observe(target, config);

            // No matter what, call the page searcher at least once
            lookForNewThings();
        },

        init = () => {
            if (document.readyState && ('complete' === document.readyState || 'loaded' === document.readyState)) {
                onload();
            } else {
                document.addEventListener('DOMContentLoaded', onload);
            }
        }

    ;

    init();

})(window);

Hopefully Gutenburg and/or my formatting plugin didn’t mess that up.

Could this be better and more performant? Possibly. Does my code style annoy you? Sorry, don’t care.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.