adobe / aem-core-wcm-components

Standardized components to build websites with AEM.
https://docs.adobe.com/content/help/en/experience-manager-core-components/using/introduction.html
Apache License 2.0
747 stars 753 forks source link

Consolidate copy/paste JavaScript #2814

Open HitmanInWis opened 4 months ago

HitmanInWis commented 4 months ago

There's a ton of duplicated javascript that can be consolidated - see below for all functions that could be created in a centralized util to replace multiple (3-5 instances of each).

Logic for checking if data layer is enabled and getting a reference to it

        var dataLayerEnabled;
        var dataLayer;

        var isDataLayerEnabled = function() {
            if (dataLayerEnabled == null) {
                dataLayerEnabled = document.body.hasAttribute('data-cmp-data-layer-enabled');
            }
            return dataLayerEnabled;
        }

        var getDataLayer = function() {
            if (isDataLayerEnabled() && dataLayer == null) {
                var dataLayerName = document.body.getAttribute('data-cmp-data-layer-name') || 'adobeDataLayer';
                dataLayer = window[dataLayerName] = window[dataLayerName] || [];
            }
            return dataLayer;
        }

Logic for constructing component JS objects

        var constructComponents = function (selector, is, clazz) {
            var elements = document.querySelectorAll(selector);
            for (var i = 0; i < elements.length; i++) {
                new clazz({ element: elements[i], options: readData(elements[i], is) });
            }

            var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
            var body = document.querySelector('body');
            var observer = new MutationObserver(function (mutations) {
                mutations.forEach(function (mutation) {
                    // needed for IE
                    var nodesArray = [].slice.call(mutation.addedNodes);
                    if (nodesArray.length > 0) {
                        nodesArray.forEach(function (addedNode) {
                            if (addedNode.querySelectorAll) {
                                var elementsArray = [].slice.call(addedNode.querySelectorAll(selector));
                                elementsArray.forEach(function (element) {
                                    new clazz({ element: element, options: readData(element, is) });
                                });
                            }
                        });
                    }
                });
            });

            observer.observe(body, {
                subtree: true,
                childList: true,
                characterData: true,
            });
        };

        var readData = function (element, is) {
            var data = element.dataset;
            var options = [];
            var capitalized = is;
            capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1);
            var reserved = ['is', 'hook' + capitalized];

            for (var key in data) {
                if (Object.prototype.hasOwnProperty.call(data, key)) {
                    var value = data[key];

                    if (key.indexOf(NS) === 0) {
                        key = key.slice(NS.length);
                        key = key.charAt(0).toLowerCase() + key.substring(1);

                        if (reserved.indexOf(key) === -1) {
                            options[key] = value;
                        }
                    }
                }
            }
            return options;
        };

        var setupProperties = function (options, properties) {
            var transformedProperties = {};

            for (var key in properties) {
                if (Object.prototype.hasOwnProperty.call(properties, key)) {
                    var property = properties[key];
                    var value = null;

                    if (options && options[key] != null) {
                        if (property && typeof property.transform === 'function') {
                            value = property.transform(options[key]);
                        } else {
                            value = options[key];
                        }
                    }

                    if (value === null) {
                        // value still null, take the property default
                        value = properties[key]['default'];
                    }

                    transformedProperties[key] = value;
                }
            }
            return transformedProperties;
        };

Logic for grabbing a components hookable children elements

        var getCacheElements = function(wrapper, is, is_dash = is) {
            var elements = {};
            elements.self = wrapper;
            var hooks = elements.self.querySelectorAll('[data-' + NS + '-hook-' + is_dash + ']');

            for (var i = 0; i < hooks.length; i++) {
                var hook = hooks[i];
                if (hook.closest('.' + NS + '-' + is_dash) === elements.self) {
                    // only process own elements
                    var capitalized = is;
                    capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1);
                    var key = hook.dataset[NS + 'Hook' + capitalized];
                    if (elements[key]) {
                        if (!Array.isArray(elements[key])) {
                            var tmp = elements[key];
                            elements[key] = [tmp];
                        }
                        elements[key].push(hook);
                    } else {
                        elements[key] = hook;
                    }
                }
            }
            return elements;
        };

Logic for getting a component's datalayer ID

        var getDataLayerId = function(item) {
            if (item) {
                if (item.dataset.cmpDataLayer) {
                    return Object.keys(JSON.parse(item.dataset.cmpDataLayer))[0];
                } else {
                    return item.id;
                }
            }
            return null;
        };

Logic for panel containers (Accordion, Carousel, Tabs) listening for panel selection events from author

        var subscribeToAuthorPanelSelect = function(panelContainer, type, navigate) {
            if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) {
                /*
                 * Editor message handling:
                 * - subscribe to "cmp.panelcontainer" message requests sent by the editor frame
                 * - check that the message data panel container type is correct and that the id (path) matches this specific component
                 * - if so, route the "navigate" operation to enact a navigation of the component based on index data
                 */
                window.CQ = window.CQ || {};
                window.CQ.CoreComponents = window.CQ.CoreComponents || {};
                window.CQ.CoreComponents.MESSAGE_CHANNEL =
                    window.CQ.CoreComponents.MESSAGE_CHANNEL ||
                    new window.Granite.author.MessageChannel('cqauthor', window);
                window.CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage(
                    'cmp.panelcontainer',
                    function (message) {
                        if (
                            message.data &&
                            message.data.type === type &&
                            message.data.id === panelContainer.dataset['cmpPanelcontainerId']
                        ) {
                            if (message.data.operation === 'navigate') {
                                navigate(message.data.index);
                            }
                        }
                    },
                );
            }
        };