Component: Content Loader

Implementation Notes

  • All functional elements are linked via a data attribute that contains a selector for the container, e.g. data-content-loader-target="#contentLoaderTarget".
  • There is at least one container that matches the selector (e.g. <div id="contentLoaderTarget">): The HTML code returned by the API is delivered to it.
  • There is at least one trigger with a matching selector in the data attribute and the data attribute data-content-loader-trigger (e.g. <button data-content-loader-trigger data-content-loader-target="#contentLoaderTarget">Load content</button>).
  • For triggers, the additional data attribute data-content-loader-action can be used to determine whether the loaded content should be appended ("append") or replace the existing content ("replace").
  • If the container has the data attribute data-content-loader-animated="true", the content change is animated (only works when replacing the content).

Loading specific content

  • The triggers have an additional data attribute data-content-loader-url that defines the URL of the API call.
  • If no action is defined for a trigger, the content is replaced by default.

Loading specific content via form

  • The trigger is a form with a data attribute data-content-loader-url that defines the URL of the API call. The form data is appended to this URL by the script.
  • The form is submitted via a submit button (<button type="submit"/>), an event handler (e.g. <input onchange="this.form.submit()"/>) or the data attribute data-content-loader-submit (<input data-content-loader-submit/>)
  • If the form does not define an action, the content is replaced by default.

Loading content successively

  • There is at least one link with the selector in the data attribute and another data attribute that defines the URL of the API call, e.g. <a data-content-loader-target="#contentLoaderTarget" data-content-loader-url="/items?page=2">.
  • If no action is defined for a trigger, the content is appended by default.

Process for successive loading:

  1. Elements with a data attribute data-content-loader-target are searched for (“triggers”).
  2. The URLs of the matching links are noted in a list.
  3. With each click on one of the triggers, an API call is started with a URL from the list.
  4. The returned HTML code is appended to (or replaces) the container.
  5. When all the links have been called, the ‘disabled’ attribute is set on the triggers.
<!-- Default -->
<div class="contentLoader"></div>

<!-- Demo Only Single -->
<section class="doc-section">

    <h2 class="doc-section-title">Buttons</h2>

    <div class="buttonGroup">
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#contentLoaderTarget-single" data-content-loader-url="/includes/contentLoader/item-1.html" class="button">Load content (0)</button>
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#contentLoaderTarget-single" data-content-loader-url="/includes/contentLoader/item-2.html" class="button">Load content (1)</button>
    </div>

    <div id="contentLoaderTarget-single" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

</section>

<!-- Demo Only Form Tags -->
<section class="doc-section">

    <h2 class="doc-section-title">Form with Tags</h2>

    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#contentLoaderTarget-form-tags" data-content-loader-url="/includes/contentLoader/form-respond.html">

        <div class="tagGroup">
            <div class="tagGroup-item">
                <label class="tag-container">
                    <input type="checkbox" name="filter[]" value="Church-key Viny" data-content-loader-submit />
                    <span class="tag" data-selectable="true">
                        Church-key Viny
                    </span>
                </label>
            </div>
            <div class="tagGroup-item">
                <label class="tag-container">
                    <input type="checkbox" name="filter[]" value="Flexitarian Retro" data-content-loader-submit />
                    <span class="tag" data-selectable="true">
                        Flexitarian Retro
                    </span>
                </label>
            </div>
            <div class="tagGroup-item">
                <label class="tag-container">
                    <input type="checkbox" name="filter[]" value="Hashtag Echo Park" data-content-loader-submit />
                    <span class="tag" data-selectable="true">
                        Hashtag Echo Park
                    </span>
                </label>
            </div>
        </div>

    </form>

    <div id="contentLoaderTarget-form-tags" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

</section>

<!-- Demo Only Form Combobox -->
<section class="doc-section">

    <h2 class="doc-section-title">Form with Combobox</h2>

    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#contentLoaderTarget-form-combobox" data-content-loader-url="/includes/contentLoader/form-respond.html">

        <div style="margin-top: var(--sp-component);">
            <div class="combobox" data-autosend data-multiselect>

                <div class="combobox-field">
                    <span class="combobox-input-wrapper">
                        <input class="combobox-input" id="uid" role="combobox" aria-controls="menu-body" aria-autocomplete="list" placeholder="Please select">
                    </span>
                </div>

                <div class="combobox-dropdown" aria-hidden="true" tabindex="-1">

                    <ul class="combobox-list" data-noresults="Keine Treffer">
                        <li class="combobox-item" data-id="my-given-ID" data-name="my-given-name[]" data-value="1" data-label="Vinyl pug cardigan">
                            <label>Vinyl pug cardigan</label>
                        </li>
                        <li class="combobox-item" data-name="my-given-name[]" data-value="2" data-label="Flexitarian Retro">
                            <label>Flexitarian Retro</label>
                        </li>
                        <li class="combobox-item" data-name="my-given-name[]" data-value="3" data-label="Plaid 8-bit" aria-disabled="true">
                            <label>Plaid 8-bit</label>
                        </li>
                        <li class="combobox-item" data-name="my-given-name[]" data-value="4" data-label="Echo Echo Park">
                            <label>Echo Echo Park</label>
                        </li>
                        <li class="combobox-item" data-name="my-given-name[]" data-value="5" data-label="Echo Vinyl">
                            <label>Echo Vinyl</label>
                        </li>
                        <li class="combobox-item" data-name="my-given-name[]" data-value="6" data-label="Echo">
                            <label>Echo</label>
                        </li>
                    </ul>

                </div>

            </div>

        </div>

    </form>

    <div id="contentLoaderTarget-form-combobox" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

</section>

<!-- Demo Only Multiple -->
<section class="doc-section">

    <h2 class="doc-section-title">Successive Loading</h2>

    <div class="doc-notes">

        <p><strong>List with the URLs of the content to be loaded</strong> (hidden in the application):</p>

        <ul>
            <li><a data-content-loader-target="#contentLoaderTarget-multiple" data-content-loader-url="/includes/contentLoader/item-1.html">/includes/contentLoader/item-1.html</a></li>
            <li><a data-content-loader-target="#contentLoaderTarget-multiple" data-content-loader-url="/includes/contentLoader/item-2.html">/includes/contentLoader/item-2.html</a></li>
        </ul>

    </div>

    <div id="contentLoaderTarget-multiple" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

    <p><button class="button" data-content-loader-trigger data-content-loader-action="append" data-content-loader-target="#contentLoaderTarget-multiple">Load more</button></p>

</section>

<!-- Default -->
<div class="contentLoader{{#modifier}} {{.}}{{/modifier}}"></div>

<!-- Demo Only Single -->
{{#demo}}
<section class="doc-section">

    {{#title}}<h2 class="doc-section-title">{{{.}}}</h2>{{/title}}

        {{#buttongroup}}
    <div class="buttonGroup">
            {{#urls}}
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}" class="button">Load content ({{@key}})</button>
            {{/urls}}
    </div>
        {{/buttongroup}}

        {{#form}}
    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../id}}" data-content-loader-url="{{action}}">

        {{#taggroup}}
            {{render '@taggroup' (contextData '@contentloader' this) merge=true}}
        {{/taggroup}}

            {{#combobox}}
        <div style="margin-top: var(--sp-component);">
            {{render '@combobox' (contextData '@contentloader' this) merge=true}}
        </div>
            {{/combobox}}

    </form>
        {{/form}}

        {{#list}}
    <div class="doc-notes">

        <p><strong>List with the URLs of the content to be loaded</strong> (hidden in the application):</p>

        <ul>
                {{#urls}}
            <li><a data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}">{{{.}}}</a></li>
                {{/urls}}
        </ul>

    </div>
        {{/list}}

    <div id="{{id}}" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

        {{#if list}}
    <p><button class="button" data-content-loader-trigger data-content-loader-action="append"  data-content-loader-target="#{{id}}">Load more</button></p>
        {{/if}}

</section>
    {{/demo}}

<!-- Demo Only Form Tags -->
{{#demo}}
<section class="doc-section">

    {{#title}}<h2 class="doc-section-title">{{{.}}}</h2>{{/title}}

        {{#buttongroup}}
    <div class="buttonGroup">
            {{#urls}}
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}" class="button">Load content ({{@key}})</button>
            {{/urls}}
    </div>
        {{/buttongroup}}

        {{#form}}
    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../id}}" data-content-loader-url="{{action}}">

        {{#taggroup}}
            {{render '@taggroup' (contextData '@contentloader' this) merge=true}}
        {{/taggroup}}

            {{#combobox}}
        <div style="margin-top: var(--sp-component);">
            {{render '@combobox' (contextData '@contentloader' this) merge=true}}
        </div>
            {{/combobox}}

    </form>
        {{/form}}

        {{#list}}
    <div class="doc-notes">

        <p><strong>List with the URLs of the content to be loaded</strong> (hidden in the application):</p>

        <ul>
                {{#urls}}
            <li><a data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}">{{{.}}}</a></li>
                {{/urls}}
        </ul>

    </div>
        {{/list}}

    <div id="{{id}}" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

        {{#if list}}
    <p><button class="button" data-content-loader-trigger data-content-loader-action="append"  data-content-loader-target="#{{id}}">Load more</button></p>
        {{/if}}

</section>
    {{/demo}}

<!-- Demo Only Form Combobox -->
{{#demo}}
<section class="doc-section">

    {{#title}}<h2 class="doc-section-title">{{{.}}}</h2>{{/title}}

        {{#buttongroup}}
    <div class="buttonGroup">
            {{#urls}}
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}" class="button">Load content ({{@key}})</button>
            {{/urls}}
    </div>
        {{/buttongroup}}

        {{#form}}
    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../id}}" data-content-loader-url="{{action}}">

        {{#taggroup}}
            {{render '@taggroup' (contextData '@contentloader' this) merge=true}}
        {{/taggroup}}

            {{#combobox}}
        <div style="margin-top: var(--sp-component);">
            {{render '@combobox' (contextData '@contentloader' this) merge=true}}
        </div>
            {{/combobox}}

    </form>
        {{/form}}

        {{#list}}
    <div class="doc-notes">

        <p><strong>List with the URLs of the content to be loaded</strong> (hidden in the application):</p>

        <ul>
                {{#urls}}
            <li><a data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}">{{{.}}}</a></li>
                {{/urls}}
        </ul>

    </div>
        {{/list}}

    <div id="{{id}}" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

        {{#if list}}
    <p><button class="button" data-content-loader-trigger data-content-loader-action="append"  data-content-loader-target="#{{id}}">Load more</button></p>
        {{/if}}

</section>
    {{/demo}}

<!-- Demo Only Multiple -->
{{#demo}}
<section class="doc-section">

    {{#title}}<h2 class="doc-section-title">{{{.}}}</h2>{{/title}}

        {{#buttongroup}}
    <div class="buttonGroup">
            {{#urls}}
        <button data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}" class="button">Load content ({{@key}})</button>
            {{/urls}}
    </div>
        {{/buttongroup}}

        {{#form}}
    <form data-content-loader-trigger data-content-loader-action="replace" data-content-loader-target="#{{../id}}" data-content-loader-url="{{action}}">

        {{#taggroup}}
            {{render '@taggroup' (contextData '@contentloader' this) merge=true}}
        {{/taggroup}}

            {{#combobox}}
        <div style="margin-top: var(--sp-component);">
            {{render '@combobox' (contextData '@contentloader' this) merge=true}}
        </div>
            {{/combobox}}

    </form>
        {{/form}}

        {{#list}}
    <div class="doc-notes">

        <p><strong>List with the URLs of the content to be loaded</strong> (hidden in the application):</p>

        <ul>
                {{#urls}}
            <li><a data-content-loader-target="#{{../../id}}" data-content-loader-url="{{.}}">{{{.}}}</a></li>
                {{/urls}}
        </ul>

    </div>
        {{/list}}

    <div id="{{id}}" data-content-loader-animated="true" class="doc-box" style="margin-top: var(--sp-component);">

        <div class="doc-notes">
            [Container for content to be loaded]
        </div>

    </div>

        {{#if list}}
    <p><button class="button" data-content-loader-trigger data-content-loader-action="append"  data-content-loader-target="#{{id}}">Load more</button></p>
        {{/if}}

</section>
    {{/demo}}
/* Default: No context defined. */

/* Demo Only Single */
{
  "demo": {
    "id": "contentLoaderTarget-single",
    "title": "Buttons",
    "buttongroup": {
      "urls": [
        "/includes/contentLoader/item-1.html",
        "/includes/contentLoader/item-2.html"
      ]
    }
  }
}

/* Demo Only Form Tags */
{
  "demo": {
    "id": "contentLoaderTarget-form-tags",
    "title": "Form with Tags",
    "form": {
      "action": "/includes/contentLoader/form-respond.html",
      "taggroup": {
        "tags": [
          {
            "is-selectable": true,
            "name": "filter[]",
            "value": "Church-key Viny",
            "label": "Church-key Viny",
            "dataset": [
              {
                "name": "data-content-loader-submit"
              }
            ]
          },
          {
            "is-selectable": true,
            "name": "filter[]",
            "value": "Flexitarian Retro",
            "label": "Flexitarian Retro",
            "dataset": [
              {
                "name": "data-content-loader-submit"
              }
            ]
          },
          {
            "is-selectable": true,
            "name": "filter[]",
            "value": "Hashtag Echo Park",
            "label": "Hashtag Echo Park",
            "dataset": [
              {
                "name": "data-content-loader-submit"
              }
            ]
          }
        ]
      }
    }
  }
}

/* Demo Only Form Combobox */
{
  "demo": {
    "id": "contentLoaderTarget-form-combobox",
    "title": "Form with Combobox",
    "form": {
      "action": "/includes/contentLoader/form-respond.html",
      "combobox": {
        "id": "uid",
        "placeholder": "Please select",
        "noresults": "Keine Treffer",
        "options": [
          {
            "label": "Vinyl pug cardigan",
            "id": "my-given-ID",
            "name": "my-given-name[]",
            "value": 1
          },
          {
            "label": "Flexitarian Retro",
            "name": "my-given-name[]",
            "value": 2
          },
          {
            "label": "Plaid 8-bit",
            "name": "my-given-name[]",
            "value": 3,
            "is-disabled": true
          },
          {
            "label": "Echo Echo Park",
            "name": "my-given-name[]",
            "value": 4
          },
          {
            "label": "Echo Vinyl",
            "name": "my-given-name[]",
            "value": 5
          },
          {
            "label": "Echo",
            "value": 6,
            "name": "my-given-name[]"
          }
        ],
        "label": null,
        "is-multiselect": true,
        "is-autosend": true
      }
    }
  }
}

/* Demo Only Multiple */
{
  "demo": {
    "id": "contentLoaderTarget-multiple",
    "title": "Successive Loading",
    "list": {
      "urls": [
        "/includes/contentLoader/item-1.html",
        "/includes/contentLoader/item-2.html"
      ]
    }
  }
}

  • Content:
    export default (function (){
    
        const defaults = {
            selectors: {
                trigger: "[data-content-loader-trigger]",
                item: "[data-content-loader-item]",
                submit: '[onchange*="this.form.submit"], [data-content-loader-submit]',
            },
            classes: {
                wrapper: "contentLoader-wrapper",
            },
            actions: [
                "append",
                "replace",
            ],
            transitionDuration: 200,
        };
    
        function init(options) {
    
            let container = document;
    
            if (typeof options === "object" && typeof options.target) {
                container = options.target;
            }
    
            const triggers = container.querySelectorAll(defaults.selectors.trigger);
    
            for (let i = 0; i < triggers.length; i++) {
                new ContentLoader(triggers[i], options);
            }
    
        }
    
        const ContentLoader = function(trigger, options) {
    
            this.settings = Object.assign({}, defaults, options);
    
            this.trigger = trigger;
    
            if (! this.trigger.dataset.contentLoaderTarget) {
                console.warn("contentLoader: No target defined!");
                return false;
            }
    
            this.selector = this.trigger.dataset.contentLoaderTarget;
    
            this.targets = document.querySelectorAll(this.selector);
    
            if (this.targets.length === 0) {
                console.warn(`contentLoader: No targets match selector "${this.selector}"`);
                return false;
            }
    
            // Enable nested content loaders by watching changes in targets
    
            this.targets.forEach((target) => {
    
                const observer = new MutationObserver((mutationsList, observer) => {
    
                    for (const mutation of mutationsList) {
    
                        if (mutation.type === 'childList') {
                            init({
                                target: target,
                            });
                        }
    
                    }
    
                });
    
                observer.observe(target, { childList: true, subtree: true });
    
            });
    
            // Get urls
    
            this.urls = [];
    
            const form = document.querySelector(`form[data-content-loader-target="${this.selector}"][data-content-loader-url]`);
    
            if (form) {
    
                const loadData = () => {
    
                    if (typeof form.dataset.contentLoaderUrl !== "string" || form.dataset.contentLoaderUrl === "") {
                        console.warn(`contentLoader: No URL in form element defined!`);
                        return;
                    }
    
                    const formData = new FormData(form),
                          search = new URLSearchParams(formData),
                          query = search.toString();
    
                    const url = trigger.dataset.contentLoaderUrl + "?" + query;
    
                    this.load({
                        trigger: form,
                        url: url,
                    });
    
                };
    
                // Select triggers and bind loading content
    
                const formTriggers = form.querySelectorAll(this.settings.selectors.submit);
    
                formTriggers.forEach((formTrigger) => {
    
                    // Remove onChange events to prevent form from being send
                    formTrigger.removeAttribute("onchange");
    
                    formTrigger.addEventListener(
                        "change",
                        (e) => {
                            e.preventDefault();
                            loadData();
                        }
                    );
    
                });
    
                // Handle custom event send by combobox
    
                const comboboxes = this.trigger.querySelectorAll(".combobox");
    
                comboboxes.forEach((combobox) => {
    
                    combobox.addEventListener('combobox:send', (event) => {
                        loadData();
                    }, false);
    
                });
    
            } else {
    
                const links = document.querySelectorAll(`*[data-content-loader-target="${this.selector}"][data-content-loader-url]`);
    
                if (links.length === 0) {
                    console.warn(`contentLoader: No links referring "${this.selector}" are found!`);
                    return false;
                }
    
                links.forEach((link) => {
                    this.urls.push(link.dataset.contentLoaderUrl);
                    link.removeAttribute("data-content-loader-url");
                });
    
                this.trigger.addEventListener(
                    "click",
                    (e) => {
                        e.preventDefault();
    
                        this.load({
                            trigger: e.target,
                        });
                    }
                );
    
            }
    
        }
    
        ContentLoader.prototype.load = function(options) {
    
            if (typeof options !== "object" || typeof options.trigger !== "object") {
                return;
            }
    
            if (typeof options.url !== "string") {
                options.url = "";
            }
    
            let customUrl = (options.url !== "");
    
            if (! customUrl) {
    
                // If trigger itself defines URL, use it, otherwise get next URL
    
                customUrl = (typeof options.trigger.dataset.contentLoaderUrl === "string");
    
                if (customUrl) {
    
                    options.url = options.trigger.dataset.contentLoaderUrl;
    
                } else {
    
                    if (this.urls.length === 0) {
                        return;
                    }
    
                    options.url = this.urls.shift();
    
                }
    
            }
    
            if (options.url.match(/^\//)) {
                options.url = location.origin + options.url;
            }
    
            // Get action
    
            if (typeof options.action !== "string") {
    
                options.action = this.settings.actions[0];
    
                if (customUrl) {
                    options.action = "replace";
                }
    
            }
    
    
            if (options.trigger.dataset.contentLoaderAction) {
    
                const customAction = options.trigger.dataset.contentLoaderAction;
    
                if (this.settings.actions.includes(customAction)) {
                    options.action = customAction;
                } else {
                    console.warn(`contentLoader: Action "${customAction}" is not defined.`);
                }
    
            }
    
            // Load data
    
            fetch(options.url)
                .then(response => response.text())
                .then(html => {
                    this.append(html, options.action);
                })
                .catch(error => {
                    console.error(`contentLoader: ${error}`);
                })
                .finally(() => {
                });
    
        }
    
        ContentLoader.prototype.append = function(data, action) {
    
            if (typeof action !== "string") {
                action = this.settings.actions[0];
            }
    
            this.targets.forEach((target) => {
    
                if (action === "replace") {
    
                    const isAnimated = (typeof target.dataset.contentLoaderAnimated === "string" && target.dataset.contentLoaderAnimated === "true");
    
                    if (isAnimated) {
                        this.animate(target, data);
                    } else {
                        target.innerHTML = data;
                    }
    
                } else {
    
                    const container = document.createElement("div");
    
                    container.innerHTML = data;
    
                    const children = Array.from(container.children);
    
                    children.forEach((child) => {
                        target.appendChild(child);
                    });
    
                }
    
            });
    
            if (this.urls.length === 0) {
                this.disable();
            }
    
        }
    
        ContentLoader.prototype.animate = function(target, data) {
    
            if (typeof this.timeout !== "undefined") {
                clearTimeout(this.timeout);
            }
    
            // Add structure for animation
    
            let structureCreated = false,
                item = target.querySelector(`:scope > ${this.settings.selectors.item}`);
    
            if (! item) {
    
                const attributeName = this.settings.selectors.item.replace(/[\[\]]/g, "");
    
                item = document.createElement("div");
                item.setAttribute(attributeName, "");
    
                item.innerHTML = target.innerHTML;
                target.innerHTML = "";
    
                target.appendChild(item);
    
                structureCreated = true;
    
            }
    
            let wrapper = item.querySelector(`:scope > .${this.settings.classes.wrapper}`);
    
            if (! wrapper) {
    
                wrapper = document.createElement("div");
                wrapper.classList.add(this.settings.classes.wrapper);
    
                wrapper.innerHTML = item.innerHTML;
    
                item.innerHTML = "";
                item.appendChild(wrapper);
    
                structureCreated = true;
    
            }
    
            // Predict height
    
            function getContentBoxHeight(element) {
    
                const style = window.getComputedStyle(element),
                      paddingTop = parseFloat(style.paddingTop),
                      paddingBottom = parseFloat(style.paddingBottom),
                      itemBoxHeight = element.clientHeight - (paddingTop + paddingBottom);
    
                return Math.round(itemBoxHeight);
    
            }
    
            const height = target.offsetHeight,
                  padding = height - getContentBoxHeight(target);
    
            target.style.height = `${height}px`;
    
            let delay = 0;
    
            if (structureCreated) {
                delay = 50;
            }
    
            setTimeout(
                () => {
                    item.setAttribute("aria-hidden", true);
                },
                delay
            );
    
            this.timeout = setTimeout(
                () => {
                    wrapper.innerHTML = data;
    
                    this.predictInnerHeight(item).then((innerHeight) => {
    
                        const offsetHeight = innerHeight + padding;
    
                        target.style.height = `${offsetHeight}px`;
    
                        item.setAttribute("aria-hidden", false);
    
                        this.timeout = setTimeout(
                            () => {
                                target.style.height = null;
                            },
                            this.duration
                        );
    
                    });
                },
                this.settings.transitionDuration + delay
            );
    
        }
    
        ContentLoader.prototype.predictInnerHeight = function(element) {
    
            return new Promise((resolve, reject) => {
    
                // Item height
    
                element.style.visibility = "hidden";
                element.style.transitionDuration = "0s";
    
                element.setAttribute("aria-hidden", false);
    
                const height = element.offsetHeight;
    
                setTimeout(() => {
                    element.setAttribute("aria-hidden", true);
    
                    setTimeout(() => {
                        element.style.visibility = null;
                        element.style.transitionDuration = null;
    
                        resolve(height);
    
                    }, 10);
    
                }, 10);
    
            });
    
        }
    
        ContentLoader.prototype.disable = function(data) {
    
            const triggers = document.querySelectorAll(defaults.selectors.trigger + `[data-content-loader-target="${this.selector}"]`);
    
            for (let i = 0; i < triggers.length; i++) {
                triggers[i].disabled = "disabled";
            }
    
        }
    
        return {
            init: init
        }
    
    })();
    
  • URL: /components/raw/contentloader/_contentLoader.script.js
  • Filesystem Path: components/03-fragments/contentLoader/_contentLoader.script.js
  • Size: 11.1 KB
  • Content:
    %contentLoader {
    
        &-trigger {
    
            &[disabled] {
                display: none;
            }
    
        }
    
    }
    
    .contentLoader {
        @extend %contentLoader;
    }
    
    button[data-content-loader-trigger] {
        @extend %contentLoader-trigger;
    }
    
    .contentLoader-wrapper {
        $bleeding: 24px; // To prevent overflowing content (e.g. box shadows) from being cut off
    
        padding: $bleeding;
        margin: (-1 * $bleeding);
    
        overflow: hidden;
    
        opacity: 1;
        transition: inherit;
        transition-property: opacity;
    
        > *:first-child {
            margin-top: 0 !important;
        }
    
    }
    
    [data-content-loader-item] {
        margin-top: 0 !important;
    
        &[aria-hidden="true"] {
            display: none;
    
            .contentLoader-wrapper {
                opacity: 0;
            }
    
        }
    
    }
    
    [data-content-loader-animated="true"] {
        transition-duration: 200ms;
        transition-property: height;
    
        overflow: hidden;
    
        [data-content-loader-item] {
            display: grid;
            grid-template-rows: 1fr;
    
            transition: inherit;
            transition-delay: 0;
            transition-property: grid-template-rows;
            transition-timing-function: easing(cubic);
    
            &[aria-hidden="true"] {
                grid-template-rows: 0fr;
            }
    
        }
    
    }
    
  • URL: /components/raw/contentloader/_contentLoader.styles.scss
  • Filesystem Path: components/03-fragments/contentLoader/_contentLoader.styles.scss
  • Size: 1.2 KB