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); } } } }