import {VBase} from './base'; import appConfig from '../../config'; import {expandParams} from './action_parameter'; import {encode} from './encode'; import {getEventTarget} from '../get_event_target'; import {buildFormData} from '../../utils/form-data' // Replaces a given element with the contents of the call to the url. // parameters are appended. export class VPosts extends VBase { constructor(options, url, params, method, event, root) { super(options, root); this.url = url; this.params = params; this.method = method; this.event = event; this.headers = options.headers; } call(results, eventParams) { this.clearErrors(); let method = this.method; const ev = new CustomEvent('V:postStarted', { bubbles: true, cancelable: false, detail: this, composed: true, }); this.dispatchLifecycleEvent(this.event, ev); // Manually build the FormData. // Passing in a
element (if available) would skip over // unchecked toggle elements, which would be unexpected if the user // has specified a value for the toggle's `off_value` attribute. const formData = new FormData(); // NB: `inputValues` will appropriately handle `input_tag`. for (const [name, value] of this.inputValues()) { formData.append(name, value); } if (eventParams){ buildFormData(formData, eventParams); } // Add CSRF authenticity token if present const csrf_meta_token = document.querySelector('meta[name=csrf-token]'); const csrf_meta_param = document.querySelector('meta[name=csrf-param]'); if (csrf_meta_token && csrf_meta_param) { formData.append(csrf_meta_param.content, csrf_meta_token.content); } // Add params from presenter: const expandedParams = expandParams(results, this.params); for (const [name, value] of Object.entries(expandedParams)) { formData.append(name, encode(value)); } if (this.paramCount(formData) < 1) { console.warn( 'Creating request with no data!' + ' Are you sure you\'ve hooked everything up correctly?'); } let errors = this.validate(formData); if (errors.length > 0) { return new Promise(function(_, reject) { results.push({ action: 'posts', method: method, statusCode: 400, contentType: 'v/errors', content: errors, }); reject(results); }); } const httpRequest = new XMLHttpRequest(); const url = this.url; const callHeaders = this.headers; const root = this.root; const vEvent = this; if (!httpRequest) { throw new Error( 'Cannot talk to server! Please upgrade your browser to one that supports XMLHttpRequest.'); } if (formData.has('rich_text_payload')) { callHeaders['X-Rich-Text-Payload'] = true; } const snackbarCallback = function(contentType, response) { const element = root.querySelector('.mdc-snackbar'); if (!(element && element.vComponent)) { return; } const snackbar = element.vComponent; if (contentType && contentType.includes('application/json')) { const messages = JSON.parse(response).message; if (messages && messages.snackbar) { const message = messages.snackbar.join('
'); if (message !== '') { snackbar.display(message); } } } }; return new Promise((resolve, reject) => { httpRequest.onreadystatechange = (event) => { if (httpRequest.readyState === XMLHttpRequest.DONE) { const contentType = httpRequest.getResponseHeader('content-type'); console.debug(httpRequest.status + ':' + contentType); const result = { action: 'posts', method: httpRequest.method, statusCode: httpRequest.status, contentType: contentType, content: httpRequest.responseText, responseURL: httpRequest.responseURL, }; var postFailed = httpRequest.status >= 400; const ev = new CustomEvent(postFailed ? 'V:postFailed' : 'V:postSucceeded', { bubbles: true, cancelable: false, detail: {event: vEvent, result: result}, composed: true, }); this.dispatchLifecycleEvent(this.event, ev); if (httpRequest.status >= 200 && httpRequest.status < 300) { results.push(result); snackbarCallback(contentType, httpRequest.responseText); resolve(results); } // Response is an html error page else if (contentType && contentType.indexOf('text/html') !== -1) { root.open(contentType); root.write(httpRequest.responseText); root.close(); results.push(result); resolve(results); } else { results.push(result); reject(results); } const evFinished = new CustomEvent('V:postFinished', { bubbles: true, cancelable: false, detail: {event: vEvent, result: result}, composed: true, }); this.dispatchLifecycleEvent(this.event, evFinished); } }; // Set up our request httpRequest.open(method, url); const configHeaders = appConfig.get('request.headers.POST', {}); for (const [key, value] of Object.entries(configHeaders)) { httpRequest.setRequestHeader(key, value); } if (callHeaders) { for (const [key, value] of Object.entries(callHeaders)) { httpRequest.setRequestHeader(key, value); } } // Send our FormData object; HTTP headers are set automatically // Rack 2.2 will throw an exception https://github.com/rack/rack/issues/1603 // if we set the header as multi-part form data with no data in the body // So we set the body to null in this case. httpRequest.send(this.paramCount(formData) < 1 ? null : formData); }); } paramCount(formData){ return Array.from(formData).length; } isForm() { var parentElement = this.parentElement(); return parentElement && parentElement.elements; } form() { if (this.isForm()) { return this.parentElement(); } return null; } dispatchLifecycleEvent(domEvent, lifecycleEvent) { let target = getEventTarget(domEvent); if (!target || !target.isConnected) { // If an action has hidden `target` or its parent (via e.g. // `hides :some_element`), it will no longer be connected to the DOM // and its dispatched lifecycle events won't make it up to the root. // Instead, dispatch straight from the root instead of bubbling up // from the DOM event's target. target = this.root; } return target.dispatchEvent(lifecycleEvent); } }