UNPKG

tui-code-snippet

Version:
557 lines (492 loc) 18.5 kB
import forEachArray from '../collection/forEachArray'; import forEachOwnProperties from '../collection/forEachOwnProperties'; import extend from '../object/extend'; import isArray from '../type/isArray'; import isEmpty from '../type/isEmpty'; import isFunction from '../type/isFunction'; import isNull from '../type/isNull'; import isObject from '../type/isObject'; import isUndefined from '../type/isUndefined'; function encodePairs(key, value) { return `${encodeURIComponent(key)}=${encodeURIComponent( isNull(value) || isUndefined(value) ? '' : value )}`; } function serializeParams(key, value, serializedList) { if (isArray(value)) { forEachArray(value, (arrVal, index) => { serializeParams(`${key}[${isObject(arrVal) ? index : ''}]`, arrVal, serializedList); }); } else if (isObject(value)) { forEachOwnProperties(value, (objValue, objKey) => { serializeParams(`${key}[${objKey}]`, objValue, serializedList); }); } else { serializedList.push(encodePairs(key, value)); } } /** * Serializer to serialize parameters * @callback ajax/serializer * @param {*} params - parameter to serialize * @returns {string} - serialized strings */ /** * Serializer * * 1. Array format * * The default array format to serialize is 'bracket'. * However in case of nested array, only the deepest format follows the 'bracket', the rest follow 'indice' format. * * - basic * { a: [1, 2, 3] } => a[]=1&a[]=2&a[]=3 * - nested * { a: [1, 2, [3]] } => a[]=1&a[]=2&a[2][]=3 * * 2. Object format * * The default object format to serialize is 'bracket' notation and doesn't allow the 'dot' notation. * * - basic * { a: { b: 1, c: 2 } } => a[b]=1&a[c]=2 * * @param {*} params - parameters to serialize * @returns {string} * @private */ function serialize(params) { if (!params || isEmpty(params)) { return ''; } const serializedList = []; forEachOwnProperties(params, (value, key) => { serializeParams(key, value, serializedList); }); return serializedList.join('&'); } const getDefaultOptions = () => ({ baseURL: '', headers: { common: {}, get: {}, post: {}, put: {}, delete: {}, patch: {}, options: {}, head: {} }, serializer: serialize }); const HTTP_PROTOCOL_REGEXP = /^(http|https):\/\//i; /** * Combine an absolute URL string (baseURL) and a relative URL string (url). * @param {string} baseURL - An absolute URL string * @param {string} url - An relative URL string * @returns {string} * @private */ function combineURL(baseURL, url) { if (HTTP_PROTOCOL_REGEXP.test(url)) { return url; } if (baseURL.slice(-1) === '/' && url.slice(0, 1) === '/') { url = url.slice(1); } return baseURL + url; } /** * Get merged options by its priorities. * defaults.common < defaults[method] < custom options * @param {Object} defaultOptions - The default options * @param {Object} customOptions - The custom options * @returns {Object} * @private */ function getComputedOptions(defaultOptions, customOptions) { const { baseURL, headers: defaultHeaders, serializer: defaultSerializer, beforeRequest: defaultBeforeRequest, success: defaultSuccess, error: defaultError, complete: defaultComplete } = defaultOptions; const { url, contentType, method, params, headers, serializer, beforeRequest, success, error, complete, withCredentials, mimeType } = customOptions; const options = { url: combineURL(baseURL, url), method, params, headers: extend(defaultHeaders.common, defaultHeaders[method.toLowerCase()], headers), serializer: serializer || defaultSerializer || serialize, beforeRequest: [defaultBeforeRequest, beforeRequest], success: [defaultSuccess, success], error: [defaultError, error], complete: [defaultComplete, complete], withCredentials, mimeType }; options.contentType = contentType || options.headers['Content-Type']; delete options.headers['Content-Type']; return options; } function validateStatus(status) { return status >= 200 && status < 300; } function hasRequestBody(method) { return /^(?:POST|PUT|PATCH)$/.test(method.toUpperCase()); } function executeCallback(callback, param) { if (isArray(callback)) { forEachArray(callback, fn => executeCallback(fn, param)); } else if (isFunction(callback)) { callback(param); } } function parseHeaders(text) { const headers = {}; forEachArray(text.split('\r\n'), header => { const [key, value] = header.split(': '); if (key !== '' && !isUndefined(value)) { headers[key] = value; } }); return headers; } function parseJSONData(data) { let result = ''; try { result = JSON.parse(data); } catch (_) { result = data; } return result; } const REQUEST_DONE = 4; function handleReadyStateChange(xhr, options) { const { readyState } = xhr; // eslint-disable-next-line eqeqeq if (readyState != REQUEST_DONE) { return; } const { status, statusText, responseText } = xhr; const { success, resolve, error, reject, complete } = options; if (validateStatus(status)) { const contentType = xhr.getResponseHeader('Content-Type'); let data = responseText; if (contentType && contentType.indexOf('application/json') > -1) { data = parseJSONData(data); } /** * successCallback is executed when the response is received successfully * @callback ajax/successCallback * @param {Object} response - success response wrapper * @param {number} response.status - response status code * @param {string} response.statusText - response status text * @param {*} response.data - response data. If its Content-Type is 'application/json', the parsed object will be passed. * @param {Object.<string,string>} response.headers - response headers */ executeCallback([success, resolve], { status, statusText, data, headers: parseHeaders(xhr.getAllResponseHeaders()) }); } else { /** * errorCallback executed when the request failed * @callback ajax/errorCallback * @param {Object} response - error response wrapper * @param {number} response.status - response status code * @param {string} response.statusText - response status text */ executeCallback([error, reject], { status, statusText }); } /** * completeCallback executed when the request is completed after success or error callbacks are executed * @callback ajax/completeCallback * @param {Object} response - error response wrapper * @param {number} response.status - response status code * @param {string} response.statusText - response status text */ executeCallback(complete, { status, statusText }); } const QS_DELIM_REGEXP = /\?/; function open(xhr, options) { const { url, method, serializer, params } = options; let requestUrl = url; if (!hasRequestBody(method) && params) { // serialize query string const qs = (QS_DELIM_REGEXP.test(url) ? '&' : '?') + serializer(params); requestUrl = `${url}${qs}`; } xhr.open(method, requestUrl); } function applyConfig(xhr, options) { const { method, contentType, mimeType, headers, withCredentials = false } = options; // set withCredentials (IE10+) if (withCredentials) { xhr.withCredentials = withCredentials; } // override MIME type (IE11+) if (mimeType) { xhr.overrideMimeType(mimeType); } forEachOwnProperties(headers, (value, header) => { if (!isObject(value)) { xhr.setRequestHeader(header, value); } }); if (hasRequestBody(method)) { xhr.setRequestHeader('Content-Type', `${contentType}; charset=UTF-8`); } // set 'x-requested-with' header to prevent CSRF in old browser xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest'); } const ENCODED_SPACE_REGEXP = /%20/g; function send(xhr, options) { const { method, serializer, beforeRequest, params = {}, contentType = 'application/x-www-form-urlencoded' } = options; let body = null; if (hasRequestBody(method)) { // The space character '%20' is replaced to '+', because application/x-www-form-urlencoded follows rfc-1866 body = contentType.indexOf('application/x-www-form-urlencoded') > -1 ? serializer(params).replace(ENCODED_SPACE_REGEXP, '+') : JSON.stringify(params); } xhr.onreadystatechange = () => handleReadyStateChange(xhr, options); /** * beforeRequestCallback is executed before the Ajax request is sent * @callback ajax/beforeRequestCallback * @param {XMLHttpRequest} xhr - XMLHttpRequest object */ executeCallback(beforeRequest, xhr); xhr.send(body); } /** * @module ajax * @description * A module for the Ajax request. * If the browser supports Promise, return the Promise object. If not, return null. * @param {Object} options - Options for the Ajax request * @param {string} options.url - URL string * @param {('GET'|'POST'|'PUT'|'DELETE'|'PATCH'|'OPTIONS'|'HEAD')} options.method - Method of the Ajax request * @param {Object.<string,string>} [options.headers] - Headers for the Ajax request * @param {string} [options.contentType] - Content-Type for the Ajax request. It is applied to POST, PUT, and PATCH requests only. Its encoding automatically sets to UTF-8. * @param {*} [options.params] - Parameters to send by the Ajax request * @param {serializer} [options.serializer] - {@link ajax_serializer Serializer} that determine how to serialize the parameters. Default serializer is {@link https://github.com/nhn/tui.code-snippet/tree/v2.3.0/ajax/index.mjs#L38 serialize()}. * @param {beforeRequestCallback} [options.beforeRequest] - {@link ajax_beforeRequestCallback beforeRequest callback} executed before the Ajax request is sent. * @param {successCallback} [options.success] - {@link ajax_successCallback success callback} executed when the response is received successfully. * @param {errorCallback} [options.error] - {@link ajax_errorCallback error callback} executed when the request failed. * @param {completeCallback} [options.complete] - {@link ajax_completeCallback complete callback} executed when the request is completed after success or error callbacks are executed. * @param {boolean} [options.withCredentials] - Determine whether cross-site Access-Control requests should be made using credentials or not. This option can be used on IE10+ * @param {string} [options.mimeType] - Override the MIME type returned by the server. This options can be used on IE11+ * @returns {?Promise} - If the browser supports Promise, return the Promise object. If not, return null. * @example * // ES6 * import ajax from 'tui-code-snippet/ajax'; * * // import transfiled file (IE8+) * import ajax from 'tui-code-snippet/ajax/index.js'; * * // CommonJS * const ajax = require('tui-code-snippet/ajax/index.js'); * * // If the browser supports Promise, return the Promise object * ajax({ * url: 'https://nhn.github.io/tui-code-snippet/', * method: 'POST', * contentType: 'application/json', * params: { * version: 'v2.3.0', * author: 'NHN. FE Development Lab <dl_javascript@nhn.com>' * }, * success: res => console.log(`success: ${res.status} ${res.statusText}`), * error: res => console.log(`error: ${res.status} ${res.statusText}`) * }).then(res => console.log(`resolve: ${res.status} ${res.statusText}`)) * .catch(res => console.log(`reject: ${res.status} ${res.statusText}`)); * * // If the request succeeds (200, OK) * // success: 200 OK * // resolve: 200 OK * * // If the request failed (503, Service Unavailable) * // error: 503 Service Unavailable * // reject: 503 Service Unavailable * * // If the browser does not support Promise, return null * ajax({ * url: 'https://ui.toast.com/fe-guide/', * method: 'GET', * contentType: 'application/json', * params: { * lang: 'en' * title: 'PERFORMANCE', * }, * success: res => console.log(`success: ${res.status} ${res.statusText}`), * error: res => console.log(`error: ${res.status} ${res.statusText}`) * }); * * // If the request succeeds (200, OK) * // success: 200 OK * * // If the request failed (503, Service Unavailable) * // error: 503 Service Unavailable */ function ajax(options) { const xhr = new XMLHttpRequest(); const request = opts => forEachArray([open, applyConfig, send], fn => fn(xhr, opts)); options = getComputedOptions(ajax.defaults, options); if (typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { request(extend(options, { resolve, reject })); }); } request(options); return null; } /** * Default configuration for the Ajax request. * @property {string} baseURL - baseURL appended with url of ajax options. If url is absolute, baseURL is ignored. * ex) baseURL = 'https://nhn.github.io', url = '/tui.code-snippet' => request is sent to 'https://nhn.github.io/tui.code-snippet' * ex) baseURL = 'https://nhn.github.io', url = 'https://ui.toast.com' => request is sent to 'https://ui.toast.com' * @property {Object} headers - request headers. It extends the header object in the following order: headers.common -> headers\[method\] -> headers in ajax options. * @property {Object.<string,string>} headers.common - Common headers regardless of the type of request * @property {Object.<string,string>} headers.get - Headers for the GET method * @property {Object.<string,string>} headers.post - Headers for the POST method * @property {Object.<string,string>} headers.put - Headers for the PUT method * @property {Object.<string,string>} headers.delete - Headers for the DELETE method * @property {Object.<string,string>} headers.patch - Headers for the PATCH method * @property {Object.<string,string>} headers.options - Headers for the OPTIONS method * @property {Object.<string,string>} headers.head - Headers for the HEAD method * @property {serializer} serializer - {@link ajax_serializer Serializer} that determine how to serialize the parameters. If serializer is specified in both default options and ajax options, use serializer in ajax options. * @property {beforeRequestCallback} beforeRequest - {@link ajax_beforeRequestCallback beforeRequest callback} executed before the Ajax request is sent. Callbacks in both default options and ajax options are executed, but default callbacks are called first. * @property {successCallback} success - {@link ajax_successCallback success callback} executed when the response is received successfully. Callbacks in both default options and ajax options are executed, but default callbacks are called first. * @property {errorCallback} error - {@link ajax_errorCallback error callback} executed when the request failed. Callbacks in both default options and ajax options are executed, but default callbacks are called first. * @property {completeCallback} complete - {@link ajax_completeCallback complete callback} executed when the request is completed after success or error callbacks are executed. Callbacks in both default options and ajax options are executed, but default callbacks are called first. * @example * ajax.defaults.baseURL = 'https://nhn.github.io/tui.code-snippet'; * ajax.defaults.headers.common = { * 'Content-Type': 'application/json' * }; * ajax.defaults.beforeRequest = () => showProgressBar(); * ajax.defaults.complete = () => hideProgressBar(); */ ajax.defaults = getDefaultOptions(); /** * Reset the default options * @private */ ajax._reset = function() { ajax.defaults = getDefaultOptions(); }; /** * Ajax request * @private */ ajax._request = function(url, method, options = {}) { return ajax(extend(options, { url, method })); }; /** * Send the Ajax request by GET * @memberof module:ajax * @function get * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.get('https://nhn.github.io/tui.code-snippet/', { * params: { * version: 'v2.3.0' * } * }); */ /** * Send the Ajax request by POST * @memberof module:ajax * @function post * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.post('https://nhn.github.io/tui.code-snippet/', { * contentType: 'application/json', * params: { * version: 'v2.3.0' * } * }); */ /** * Send the Ajax request by PUT * @memberof module:ajax * @function put * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.put('https://nhn.github.io/tui.code-snippet/v2.3.0', { * success: ({status, statusText}) => alert(`success: ${status} ${statusText}`), * error: ({status, statusText}) => alert(`error: ${status} ${statusText}`) * }); */ /** * Send the Ajax request by DELETE * @memberof module:ajax * @function delete * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.delete('https://nhn.github.io/tui.code-snippet/v2.3.0'); */ /** * Send the Ajax request by PATCH * @memberof module:ajax * @function patch * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.patch('https://nhn.github.io/tui.code-snippet/v2.3.0', { * beforeRequest: () => showProgressBar(), * complete: () => hideProgressBar() * }); */ /** * Send the Ajax request by OPTIONS * @memberof module:ajax * @function options * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.head('https://nhn.github.io/tui.code-snippet/v2.3.0'); */ /** * Send the Ajax request by HEAD * @memberof module:ajax * @function head * @param {string} url - URL string * @param {object} options - Options for the Ajax request. Refer to {@link ajax options of ajax()}. * @example * ajax.options('https://nhn.github.io/tui.code-snippet/v2.3.0'); */ forEachArray(['get', 'post', 'put', 'delete', 'patch', 'options', 'head'], type => { ajax[type] = (url, options) => ajax._request(url, type.toUpperCase(), options); }); export default ajax;