suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
345 lines (303 loc) • 10.6 kB
JavaScript
import ApiManager from './ApiManager';
/**
* @typedef {Object} FileManagerParams
* @property {string} query The query selector used to find file elements in the editor
* @property {string} loadEventName Event name for file load (e.g., `'onImageLoad'`)
* @property {string} actionEventName Event name for file action (e.g., `'onImageAction'`)
*/
/**
* @class
* @description This module manages the file information of the editor.
*/
class FileManager {
#$;
/**
* @constructor
* @param {*} inst The instance object that called the constructor.
* @param {SunEditor.Deps} $ Kernel dependencies
* @param {FileManagerParams} params FileManager options
*/
constructor(inst, $, params) {
this.#$ = $;
// members
inst.__fileManagement = this;
this.kind = inst.constructor.key || inst.constructor.name;
this.inst = inst;
this.query = params.query;
this.loadEventName = params.loadEventName;
this.actionEventName = params.actionEventName;
this.infoList = [];
this.infoIndex = 0;
this.uploadFileLength = 0;
this.__updateTags = [];
// api manager
this.apiManager = new ApiManager(this, $);
// se-ts-ignore - call by editor
void this._resetInfo;
void this._checkInfo;
}
/**
* @description Upload the file to the server.
* @param {string} uploadUrl Upload server url
* @param {?Object<string, string>} uploadHeader Request header
* @param {FileList|File[]|{formData: FormData, size: number}} data FormData in body or Files array
* @param {?(xmlHttp: XMLHttpRequest) => boolean} [callBack] Success call back function
* @param {?(res: *, xmlHttp: XMLHttpRequest) => string} [errorCallBack] Error call back function
* @example
* // Upload with a File array
* const files = [new File(['content'], 'photo.jpg', { type: 'image/jpeg' })];
* fileManager.upload('/api/upload', { Authorization: 'Bearer token' }, files, onSuccess, onError);
*
* // Upload with a pre-built FormData
* const formData = new FormData();
* formData.append('file-0', myFile);
* fileManager.upload('/api/upload', null, { formData, size: 1 }, onSuccess, onError);
*/
upload(uploadUrl, uploadHeader, data, callBack, errorCallBack) {
this.#$.ui.showLoading();
let formData = null;
// create formData
if ('length' in data) {
formData = new FormData();
for (let i = 0, len = data.length; i < len; i++) {
formData.append('file-' + i, data[i]);
}
this.uploadFileLength = data.length;
} else {
formData = data.formData;
this.uploadFileLength = data.size;
}
this.apiManager.call({ method: 'POST', url: uploadUrl, headers: uploadHeader, data: formData, callBack, errorCallBack });
}
/**
* @description Upload the file to the server.
* @param {string} uploadUrl Upload server url
* @param {?Object<string, string>} uploadHeader Request header
* @param {FileList|File[]|{formData: FormData, size: number}} data FormData in body or Files array
* @returns {Promise<XMLHttpRequest>}
* @example
* const files = [new File(['content'], 'photo.jpg')];
* const xmlHttp = await fileManager.asyncUpload('/api/upload', { Authorization: 'Bearer token' }, files);
* const response = JSON.parse(xmlHttp.responseText);
*/
async asyncUpload(uploadUrl, uploadHeader, data) {
this.#$.ui.showLoading();
let formData = null;
// create formData
if ('length' in data) {
formData = new FormData();
for (let i = 0, len = data.length; i < len; i++) {
formData.append('file-' + i, data[i]);
}
this.uploadFileLength = data.length;
} else {
formData = data.formData;
this.uploadFileLength = data.size;
}
return await this.apiManager.asyncCall({ method: 'POST', url: uploadUrl, headers: uploadHeader, data: formData });
}
/**
* @description Set the file information to the element.
* @param {Node} element File information element
* @param {Object} params
* @param {string} params.name File name
* @param {number} params.size File size
* @returns
* @example
* const imgElement = document.createElement('img');
* imgElement.src = 'https://example.com/photo.jpg';
* fileManager.setFileData(imgElement, { name: 'photo.jpg', size: 2048 });
*/
setFileData(element, { name, size }) {
if (!element) return;
/** @type {Element} */ (element).setAttribute('data-se-file-name', name);
/** @type {Element} */ (element).setAttribute('data-se-file-size', size + '');
}
/**
* @description Gets the sum of the sizes of the currently saved files.
* @returns {number} Size
*/
getSize() {
let size = 0;
for (let i = 0, len = this.infoList.length; i < len; i++) {
size += this.infoList[i].size * 1;
}
return size;
}
/**
* @internal
* @description Checke the file's information and modify the tag that does not fit the format.
* @param {boolean} loaded Whether the editor is loaded
*/
_checkInfo(loaded) {
const tags = [].slice.call(this.#$.frameContext.get('wysiwyg').querySelectorAll(this.query));
const infoList = this.infoList;
if (tags.length === infoList.length) {
// reset
let infoUpdate = false;
for (let i = 0, len = infoList.length, info; i < len; i++) {
info = infoList[i];
if (
tags.filter(function (t) {
return info.src === t.src && info.index.toString() === GetAttr(t, 'index');
}).length === 0
) {
infoUpdate = true;
break;
}
}
// pass
if (!infoUpdate) return;
}
// check
const currentTags = [];
const infoIndex = [];
for (let i = 0, len = infoList.length; i < len; i++) {
infoIndex[i] = infoList[i].index;
}
this.__updateTags = tags;
while (tags.length > 0) {
const tag = tags.shift();
if (!GetAttr(tag, 'index') || !infoIndex.includes(Number(GetAttr(tag, 'index')))) {
currentTags.push(this.infoIndex);
tag.removeAttribute('data-se-index');
this.#setInfo(tag, null);
} else {
currentTags.push(Number(GetAttr(tag, 'index')));
}
}
// editor load
if (loaded && this.loadEventName) {
this.#$.eventManager.triggerEvent(this.loadEventName, { infoList });
return;
}
for (let i = 0, dataIndex; i < infoList.length; i++) {
dataIndex = infoList[i].index;
if (currentTags.includes(dataIndex)) continue;
infoList.splice(i, 1);
const params = { element: null, index: dataIndex, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind };
if (this.actionEventName) {
this.#$.eventManager.triggerEvent(this.actionEventName, params);
}
this.#$.eventManager.triggerEvent('onFileManagerAction', { ...params, pluginName: this.kind });
i--;
}
}
/**
* @internal
* @description Reset info object and `infoList = []`, `infoIndex = 0`
*/
_resetInfo() {
const params = { element: null, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind };
for (let i = 0, len = this.infoList.length; i < len; i++) {
if (this.actionEventName) {
this.#$.eventManager.triggerEvent(this.actionEventName, { ...params, index: this.infoList[i].index, pluginName: this.kind });
}
this.#$.eventManager.triggerEvent('onFileManagerAction', { ...params, index: this.infoList[i].index, pluginName: this.kind });
}
this.infoList = [];
this.infoIndex = 0;
}
/**
* @description Delete info object at `infoList`
* @param {number} index index of info object infoList[].index)
*/
#deleteInfo(index) {
if (index >= 0) {
for (let i = 0, len = this.infoList.length; i < len; i++) {
if (index === this.infoList[i].index) {
this.infoList.splice(i, 1);
if (this.actionEventName) {
this.#$.eventManager.triggerEvent(this.actionEventName, { element: null, index, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind });
}
return;
}
}
}
}
/**
* @description Create info object of file and add it to `infoList`
* @param {HTMLMediaElement} element
* @param {{name: string, size: number}|null} file File information
*/
#setInfo(element, file) {
let dataIndex = Number(GetAttr(element, 'index'));
let info = null;
let state = '';
file ||= {
name: GetAttr(element, 'file-name') || (typeof element.src === 'string' ? element.src.split('/').pop() : ''),
size: Number(GetAttr(element, 'file-size')) || 0,
};
// create
if (!dataIndex || !this.#$.store._editorInitFinished) {
state = 'create';
dataIndex = this.infoIndex++;
element.setAttribute('data-se-index', dataIndex + '');
element.setAttribute('data-se-file-name', file.name);
element.setAttribute('data-se-file-size', file.size + '');
info = {
src: element.src,
index: dataIndex,
name: file.name,
size: file.size,
};
this.infoList.push(info);
} else {
// update
state = 'update';
dataIndex *= 1;
for (let i = 0, len = this.infoList.length; i < len; i++) {
if (dataIndex === this.infoList[i].index) {
info = this.infoList[i];
break;
}
}
if (!info) {
dataIndex = this.infoIndex++;
info = {
index: dataIndex,
};
this.infoList.push(info);
}
info.src = element.src;
info.name = GetAttr(element, 'file-name');
info.size = Number(GetAttr(element, 'file-size'));
}
// method bind
info.element = element;
info.delete = function (deleteCallback, el) {
this.inst.componentDestroy?.(el);
deleteCallback(Number(GetAttr(el, 'index')));
}.bind(this, this.#deleteInfo.bind(this), element);
info.select = function (component, el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
const comp = component.get(el);
if (comp) {
component.select(comp.target, comp.pluginName);
} else {
this.inst.componentSelect?.(el);
}
}.bind(this, this.#$.component, element);
const params = { element, index: dataIndex, state, info, remainingFilesCount: --this.uploadFileLength < 0 ? 0 : this.uploadFileLength, pluginName: this.kind };
if (this.actionEventName) {
this.#$.eventManager.triggerEvent(this.actionEventName, params);
}
this.#$.eventManager.triggerEvent('onFileManagerAction', { ...params, pluginName: this.kind });
}
}
/**
* @param {Element} element - Element
* @param {string} name - Attribute name
* @returns {string|null}
*/
function GetAttr(element, name) {
const seAttr = element.getAttribute(`data-se-${name}`);
if (seAttr) return seAttr;
// v2-migration
const v2SeAttr = element.getAttribute(`data-${name}`);
if (!v2SeAttr) return null;
element.removeAttribute(`data-${name}`);
element.setAttribute(`data-se-${name}`, v2SeAttr);
return v2SeAttr;
}
export default FileManager;