UNPKG

@ckeditor/ckeditor5-cloud-services

Version:

CKEditor 5's Cloud Services integration layer.

196 lines (195 loc) 6.32 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ import { EmitterMixin, CKEditorError } from 'ckeditor5/src/utils.js'; const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/; /** * FileUploader class used to upload single file. */ export default class FileUploader extends /* #__PURE__ */ EmitterMixin() { /** * A file that is being uploaded. */ file; xhr; /** * CKEditor Cloud Services access token. */ _token; /** * CKEditor Cloud Services API address. */ _apiAddress; /** * Creates `FileUploader` instance. * * @param fileOrData A blob object or a data string encoded with Base64. * @param token Token used for authentication. * @param apiAddress API address. */ constructor(fileOrData, token, apiAddress) { super(); if (!fileOrData) { /** * File must be provided as the first argument. * * @error fileuploader-missing-file */ throw new CKEditorError('fileuploader-missing-file', null); } if (!token) { /** * Token must be provided as the second argument. * * @error fileuploader-missing-token */ throw new CKEditorError('fileuploader-missing-token', null); } if (!apiAddress) { /** * Api address must be provided as the third argument. * * @error fileuploader-missing-api-address */ throw new CKEditorError('fileuploader-missing-api-address', null); } this.file = _isBase64(fileOrData) ? _base64ToBlob(fileOrData) : fileOrData; this._token = token; this._apiAddress = apiAddress; } /** * Registers callback on `progress` event. */ onProgress(callback) { this.on('progress', (event, data) => callback(data)); return this; } /** * Registers callback on `error` event. Event is called once when error occurs. */ onError(callback) { this.once('error', (event, data) => callback(data)); return this; } /** * Aborts upload process. */ abort() { this.xhr.abort(); } /** * Sends XHR request to API. */ send() { this._prepareRequest(); this._attachXHRListeners(); return this._sendRequest(); } /** * Prepares XHR request. */ _prepareRequest() { const xhr = new XMLHttpRequest(); xhr.open('POST', this._apiAddress); xhr.setRequestHeader('Authorization', this._token.value); xhr.responseType = 'json'; this.xhr = xhr; } /** * Attaches listeners to the XHR. */ _attachXHRListeners() { const xhr = this.xhr; const onError = (message) => { return () => this.fire('error', message); }; xhr.addEventListener('error', onError('Network Error')); xhr.addEventListener('abort', onError('Abort')); /* istanbul ignore else -- @preserve */ if (xhr.upload) { xhr.upload.addEventListener('progress', event => { if (event.lengthComputable) { this.fire('progress', { total: event.total, uploaded: event.loaded }); } }); } xhr.addEventListener('load', () => { const statusCode = xhr.status; const xhrResponse = xhr.response; if (statusCode < 200 || statusCode > 299) { return this.fire('error', xhrResponse.message || xhrResponse.error); } }); } /** * Sends XHR request. */ _sendRequest() { const formData = new FormData(); const xhr = this.xhr; formData.append('file', this.file); return new Promise((resolve, reject) => { xhr.addEventListener('load', () => { const statusCode = xhr.status; const xhrResponse = xhr.response; if (statusCode < 200 || statusCode > 299) { if (xhrResponse.message) { /** * Uploading file failed. * * @error fileuploader-uploading-data-failed */ return reject(new CKEditorError('fileuploader-uploading-data-failed', this, { message: xhrResponse.message })); } return reject(xhrResponse.error); } return resolve(xhrResponse); }); xhr.addEventListener('error', () => reject(new Error('Network Error'))); xhr.addEventListener('abort', () => reject(new Error('Abort'))); xhr.send(formData); }); } } /** * Transforms Base64 string data into file. * * @param base64 String data. */ function _base64ToBlob(base64, sliceSize = 512) { try { const contentType = base64.match(BASE64_HEADER_REG_EXP)[1]; const base64Data = atob(base64.replace(BASE64_HEADER_REG_EXP, '')); const byteArrays = []; for (let offset = 0; offset < base64Data.length; offset += sliceSize) { const slice = base64Data.slice(offset, offset + sliceSize); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } byteArrays.push(new Uint8Array(byteNumbers)); } return new Blob(byteArrays, { type: contentType }); } catch (error) { /** * Problem with decoding Base64 image data. * * @error fileuploader-decoding-image-data-error */ throw new CKEditorError('fileuploader-decoding-image-data-error', null); } } /** * Checks that string is Base64. */ function _isBase64(string) { if (typeof string !== 'string') { return false; } return !!string.match(BASE64_HEADER_REG_EXP)?.length; }