mdui.editor
Version:
Material Design 样式的富文本编辑器
274 lines (229 loc) • 7.55 kB
text/typescript
import $ from 'mdui.jq/es/$';
import ajax from 'mdui.jq/es/functions/ajax';
import { JQ } from 'mdui.jq/es/JQ';
import 'mdui.jq/es/methods/find';
import 'mdui.jq/es/methods/html';
import 'mdui.jq/es/methods/insertAfter';
import 'mdui.jq/es/methods/is';
import 'mdui.jq/es/methods/next';
import 'mdui.jq/es/methods/on';
import 'mdui.jq/es/methods/remove';
import 'mdui.jq/es/methods/val';
import 'mdui.jq/es/methods/trigger';
import 'mdui/es/components/dialog/alert';
import { Dialog } from 'mdui/es/components/dialog/class';
import 'mdui/es/components/dialog/dialog';
import mdui from 'mdui/es/mdui';
import 'mdui/es/jq_extends/static/guid';
import MenuAbstract from '../abstracts/menuAbstract';
import Editor from '../index';
import { memoryFormat } from '../utils/format';
class Image extends MenuAbstract {
static icon = 'image';
static title = '插入图片';
/**
* <input type="file"/> 元素
*/
private $input: JQ = $();
/**
* <input type="file"/> 元素的 ID
*/
private inputID: string = $.guid();
/**
* 允许上传的图片后缀
*/
private suffixs: string[] = [];
/**
* 允许上传的图片格式
*/
private accepts: string[] = [];
public constructor(editor: Editor, $button: JQ) {
super(editor, $button);
this.setAccepts();
this.setInput();
this.bindInputChange();
this.bindKeyboardEvent();
}
private bindKeyboardEvent(): void {
this.$container.on('keydown', (event) => {
const keyCode = (event as KeyboardEvent).keyCode;
this.selection.saveRange();
const $curElem = this.selection.getContainerElem();
if (keyCode === 8 || keyCode === 46) {
if ($curElem.is('figcaption')) {
// 在 figcaption 元素中按删除键时,若该元素内容为空,则不删除
const html = $curElem.html().toLowerCase().trim();
if (!html || html === '<br>' || html === '<br/>') {
$curElem.html('');
event.preventDefault();
}
}
}
if ($curElem.is('figure')) {
if (keyCode === 8 || keyCode === 46) {
// 删除图片时,删除 figure 元素,并聚焦到下一行
this.selection.createRangeByElem($curElem.next());
$curElem.remove();
this.selection.restore();
}
event.preventDefault();
}
if (keyCode === 13) {
// 在 figcaption 中按回车键时,跳出图片元素,聚焦到下一个 root 元素
if ($curElem.is('figcaption')) {
const $nextElem = this.selection.getRootElem().next();
if (!$nextElem.length) {
// 没有下一个元素,新增一行
this.command.do('insertAfterRoot', '<p><br></p>');
} else {
// 有下一个元素,聚焦到下一行
this.selection.createRangeByElem($nextElem);
this.selection.restore();
}
event.preventDefault();
}
}
});
this.$container.on('keyup', (event) => {
const keyCode = (event as KeyboardEvent).keyCode;
const $curElem = this.selection.getContainerElem();
if (keyCode === 8 || keyCode === 46) {
// 在 figcaption 中删除时,若该元素不含文字,则清空该元素
if ($curElem.is('figcaption')) {
const html = $curElem.html().toLowerCase().trim();
if (!html || html === '<br>' || html === '<br/>') {
$curElem.html('');
}
}
}
});
}
/**
* 设置允许上传的图片类型
*/
private setAccepts(): void {
this.suffixs = this.editor.options.imageUploadSuffix!;
const map: { [name: string]: string } = {
png: 'image/png',
jpg: 'image/jpeg',
gif: 'image/gif',
};
this.accepts = this.suffixs.map((suffix) => map[suffix]);
}
/**
* 在按钮后面插入 <input type="file"/> 元素
*/
private setInput(): void {
this.$input = $(
`<input type="file" id="${this.inputID}" name="${
this.editor.options.imageUploadName
}" accept="${this.accepts.join(', ')}"/>`,
).insertAfter(this.$button);
}
/**
* 选择文件后触发的事件
*/
private bindInputChange(): void {
this.$input.on('change', (event) => {
// @ts-ignore
const files = event.target.files as FileList;
if (!files.length) {
return;
}
this.upload(files[0]);
this.$input.val('');
});
}
/**
* 执行上传
* @param file
*/
private upload(file: File): void {
if (this.accepts.indexOf(file.type) < 0) {
mdui.alert(`仅允许上传 ${this.suffixs.join(', ')} 格式的图片`);
return;
}
if (
this.editor.options.imageUploadMaxSize! &&
file.size > this.editor.options.imageUploadMaxSize!
) {
mdui.alert(
`图片体积不能超过 ${memoryFormat(
this.editor.options.imageUploadMaxSize!,
)}`,
);
return;
}
const formData = new FormData();
formData.append(this.editor.options.imageUploadName!, file);
let loadingDialog: Dialog;
let uploadTime: number;
let uploadTimeInterval: any;
ajax({
url: this.editor.options.imageUploadUrl!,
method: 'POST',
data: formData,
processData: false,
dataType: 'json',
contentType: false,
global: false,
beforeSend: (xhr) => {
uploadTime = 0;
uploadTimeInterval = setInterval(() => (uploadTime += 100), 100);
loadingDialog = mdui.dialog({
title: '上传中…',
content: '<p class="mdui_editor-upload-progress">0%</p>',
history: false,
modal: true,
cssClass: 'mdui_editor-upload-progress-dialog',
});
const $progress = loadingDialog.$element.find(
'.mdui_editor-upload-progress',
);
xhr.upload.onprogress = (event): void => {
// @ts-ignore
$progress.html(`${((event.loaded / event.total) * 100).toFixed(0)}%`);
};
},
complete: () => {
clearInterval(uploadTimeInterval);
if (uploadTime < 500) {
setTimeout(() => {
loadingDialog.close();
}, 500 - uploadTime);
} else {
loadingDialog.close();
}
},
})
.then((response) => {
if (this.editor.options.imageUploadResponseTransform) {
response = this.editor.options.imageUploadResponseTransform(response);
}
if (response.code) {
mdui.alert(response.message);
return;
}
const $rootElem = this.selection.getRootElem();
const rootHTML = $rootElem.html().toLowerCase().trim();
const imgHTML = `<figure><img src="${response.data.url}"/><figcaption placeholder="图片描述(选填)"></figcaption></figure>`;
if (
$rootElem[0].nodeName === 'P' &&
(rootHTML === '<br>' || rootHTML === '<br/>')
) {
// 当前为空的 p 元素,替换该元素
this.command.do('replaceRoot', imgHTML);
} else {
// 当前不是空的 p 元素,在当前元素后面插入图片
this.command.do('insertAfterRoot', imgHTML);
}
// 在图片下面重新插入一行,并聚焦
this.command.do('insertAfterRoot', '<p><br></p>');
})
.catch(() => mdui.alert('图片上传失败'));
}
public onclick(): void {
$(`#${this.inputID}`).trigger('click');
}
}
export default Image;