rx/presenters

View on GitHub
views/mdc/assets/js/components/events.js

Summary

Maintainability
C
1 day
Test Coverage
import {VLoads} from './events/loads';
import {VPosts} from './events/posts';
import {VReplaces} from './events/replaces';
import {VDialog} from './events/dialog';
import {VErrors} from './events/errors';
import {VToggleVisibility} from './events/toggle_visibility';
import {VToggleDisabled} from './events/toggle_disabled';
import {VAutoComplete} from './events/autocomplete';
import {VPromptIfDirty} from './events/prompt_if_dirty';
import {VSnackbarEvent} from './events/snackbar';
import {VClears} from './events/clears';
import {VCloseDialog} from './events/close_dialog';
import {VPostMessage} from './events/post_message';
import {VRemoves} from './events/removes';
import {VStepperEvent} from './events/stepper';
import {VNavigates} from './events/navigates';
import {VPluginEventAction} from './events/plugin';
import getRoot from './root_document';
import {hasDragDropData, extractDragDropData} from './drag_n_drop';
import {getEventTarget} from './get_event_target';

const EVENTS_SELECTOR = '[data-events]';

export class VEvents {
    // [[type, url, target, params]]
    constructor(actions, event, root, vComponent) {
        this.event = event;
        this.root = root;
        this.actions = actions.map((action) => {
            return this.constructor.action_class(action, event, root);
        });
        this.vComponent = vComponent;
    }

    call() {
        const event = this.event;
        const target = getEventTarget(event);
        let eventParams = {};

        if (hasDragDropData(event)) {
            eventParams = Object.assign(eventParams, extractDragDropData(event));
        }
        else if (event.detail && event.detail.constructor === Object) {
            eventParams = Object.assign(eventParams, event.detail);
        }

        // Adapted from http://www.datchley.name/promise-patterns-anti-patterns/#executingpromisesinseries
        const fnlist = this.actions.map((action) => {
            return function(results) {
                return Promise.resolve(action.call(results, eventParams));
            };
        });

        // Execute a list of Promise return functions in series
        function pseries(list) {
            const p = Promise.resolve([]);
            return list.reduce(function(pacc, fn) {
                return pacc = pacc.then(fn);
            }, p);
        }

        const ev = new CustomEvent('V:eventsStarted', {
            bubbles: true,
            cancelable: false,
            detail: this,
        });
        target.dispatchEvent(ev);

        if (this.vComponent) {
            this.vComponent.actionsStarted(this);
        }


        new VErrors(this.root).clearErrors();

        pseries(fnlist).then((results) => {
            const result = results.pop();
            const contentType = result.contentType;
            const responseURL = result.responseURL;

            if (contentType && contentType.indexOf('text/html') !== -1 &&
                typeof responseURL !== 'undefined') {
                window.location = responseURL;
            }

            const ev = new CustomEvent('V:eventsSucceeded', {
                bubbles: true,
                cancelable: false,
                detail: this,
            });
            target.dispatchEvent(ev);

            if (this.vComponent) {
                this.vComponent.actionsSucceeded(this);
            }
        }).catch((results) => {
            console.log('If you got here it may not be what you think:',
                results);

            let result = results;

            if (typeof results.pop === 'function') {
                result = results.pop();
            }

            if (!result.squelch) {
                new VErrors(this.root, target).displayErrors(result);
            }

            const ev = new CustomEvent('V:eventsHalted', {
                bubbles: true,
                cancelable: false,
                detail: this,
            });
            target.dispatchEvent(ev);

            if (this.vComponent) {
                this.vComponent.actionsHalted(this);
            }
        }).finally(() => {
            const ev = new CustomEvent('V:eventsFinished', {
                bubbles: true,
                cancelable: false,
                detail: this,
            });
            target.dispatchEvent(ev);

            if (this.vComponent) {
                this.vComponent.actionsFinished(this);
            }
        });
    }

    static action_class(action, event, root) {
        const action_type = action[0];
        const url = action[1];
        const options = action[2];
        const params = action[3];

        switch (action_type) {
            case 'loads':
                return new VLoads(options, url, params, event, root);
            case 'replaces':
                return new VReplaces(options, url, params, event, root);
            case 'post':
                return new VPosts(options, url, params, 'POST', event, root);
            case 'update':
                return new VPosts(options, url, params, 'PUT', event, root);
            case 'delete':
                return new VPosts(options, url, params, 'DELETE', event, root);
            case 'dialog':
                return new VDialog(options, params, event, root);
            case 'toggle_visibility':
                return new VToggleVisibility(options, params, event, root);
            case 'toggle_disabled':
                return new VToggleDisabled(options, params, event, root);
            case 'prompt_if_dirty':
                return new VPromptIfDirty(options, params, event, root);
            case 'remove':
                return new VRemoves(options, params, event, root);
            case 'snackbar':
                return new VSnackbarEvent(options, params, event, root);
            case 'autocomplete':
                return new VAutoComplete(options, url, params, event, root);
            case 'clear':
                return new VClears(options, params, event, root);
            case 'close_dialog':
                return new VCloseDialog(options, params, event, root);
            case 'post_message':
                return new VPostMessage(options, params, event, root);
            case 'stepper':
                return new VStepperEvent(options, params, event, root);
            case 'navigates':
                return new VNavigates(options, params, event, root);
            default:
                return new VPluginEventAction(action_type, options, params,
                    event, root);
        }
    }
}

// This is used to get a proper binding of the actionData
// https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example
function createEventHandler(actionsData, root, vComponent) {
    return function(event) {
        event.stopPropagation();
        new VEvents(actionsData, event, root, vComponent).call();
    };
}

function getEventElements(root) {
    const elements = Array.from(root.querySelectorAll(EVENTS_SELECTOR));

    // Include `root` if it has events:
    if (typeof root.matches === 'function' && root.matches(EVENTS_SELECTOR)) {
        elements.unshift(root);
    }

    return elements;
}

export function initEvents(e) {
    console.debug('\tEvents');

    for (const eventElem of getEventElements(e)) {
        var eventsData = JSON.parse(eventElem.dataset.events);
        for (var j = 0; j < eventsData.length; j++) {
            var eventData = eventsData[j];
            var eventName = eventData[0];
            var eventOptions = eventData[2];
            eventOptions.passive = true;
            var actionsData = eventData[1];
            const vComponent = eventElem.vComponent;
            var eventHandler = createEventHandler(actionsData, getRoot(e),
                vComponent);
            // allow override of event handler by component
            if (vComponent && vComponent.createEventHandler) {
                eventHandler = vComponent.createEventHandler(
                    actionsData, getRoot(e), vComponent);
            }
            // Delegate to the component if possible
            if (vComponent &&
                vComponent.initEventListener) {
                vComponent.initEventListener(eventName, eventHandler,
                    eventOptions);
            }
            else {
                if (typeof eventElem.eventsHandler === 'undefined') {
                    eventElem.eventsHandler = {};
                }
                if (typeof eventElem.eventsHandler[eventName] === 'undefined') {
                    eventElem.eventsHandler[eventName] = [];
                }
                eventElem.eventsHandler[eventName].push(eventHandler);
                eventElem.addEventListener(eventName, eventHandler,
                    eventOptions);
            }

            if (vComponent) {
                vComponent.afterInit();
            }
        }
    }
    fireAfterLoad(e);
}

export function removeEvents(elem) {
    console.debug('\tuninitEvents');

    for (const eventElem of getEventElements(elem)) {
        let eventsData = JSON.parse(eventElem.dataset.events);
        for (var j = 0; j < eventsData.length; j++) {
            let eventData = eventsData[j];
            let eventName = eventData[0];
            let eventOptions = eventData[2];
            eventOptions.passive = true;
            for (const handler of eventElem.eventsHandler[eventName]) {
                eventElem.removeEventListener(eventName, handler, eventOptions);
            }
        }
    }
}

function fireAfterLoad(e) {
    for (const eventElem of getEventElements(e)) {
        var eventsData = JSON.parse(eventElem.dataset.events);
        for (var j = 0; j < eventsData.length; j++) {
            var eventData = eventsData[j];
            var eventName = eventData[0];
            if (eventName === 'after_init') {
                var event = new Event('after_init', { composed: true });
                // Dispatch the event.
                eventElem.dispatchEvent(event);
            }
        }
    }
}