kedao
Version:
Rich Text Editor Based On Draft.js
461 lines (460 loc) • 22 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import React, { forwardRef, useEffect, useState, useRef, useImperativeHandle, useMemo } from 'react';
import { UniqueIndex, compressImage } from '../../utils';
import { classNameParser } from '../../utils/style';
import styles from "./styles.module.css";
import useLanguage from '../../hooks/use-language';
import PlusIcon from 'tabler-icons-react/dist/icons/plus';
import { tablerIconProps } from '../../constants';
import CheckIcon from 'tabler-icons-react/dist/icons/check';
import FileDescriptionIcon from 'tabler-icons-react/dist/icons/file-description';
import MovieIcon from 'tabler-icons-react/dist/icons/movie';
import MusicIcon from 'tabler-icons-react/dist/icons/music';
import TrashIcon from 'tabler-icons-react/dist/icons/trash';
import XIcon from 'tabler-icons-react/dist/icons/x';
import CodeIcon from 'tabler-icons-react/dist/icons/code';
const cls = classNameParser(styles);
const defaultValidator = () => true;
const defaultAccepts = {
image: 'image/png,image/jpeg,image/gif,image/webp,image/apng,image/svg',
video: 'video/mp4',
audio: 'audio/mp3'
};
const defaultExternals = {
image: true,
video: true,
audio: true,
embed: true
};
const Finder = forwardRef(({ uploadFn = defaultValidator, validateFn, accepts = defaultAccepts, externals = defaultExternals, onCancel, onInsert }, ref) => {
useImperativeHandle(ref, () => ({
uploadImage
}));
const [items, setItems] = useState([]);
const language = useLanguage();
useEffect(() => {
uploadItems();
}, [items]);
const getMediaItem = id => {
return items.find(item => item.id === id);
};
const getSelectedItems = () => {
return items.filter(item => item.selected);
};
const addItems = newItems => {
setItems(oldItems => [...oldItems, ...newItems]);
};
const addMediaItem = item => {
addItems([item]);
};
const selectMediaItem = id => {
const item = getMediaItem(id);
if (item && (item.uploading || item.error)) {
return;
}
setMediaItemState(id, {
selected: true
});
};
const selectAllItems = () => {
setItems(items => items
.filter(item => !item.error && !item.uploading)
.map(item => (Object.assign(Object.assign({}, item), { selected: true }))));
};
const deselectMediaItem = id => {
setMediaItemState(id, {
selected: false
});
};
const deselectAllItems = () => {
setItems(items => items.map(item => (Object.assign(Object.assign({}, item), { selected: false }))));
};
const removeMediaItem = id => {
setItems(items => items.filter(item => item.id !== id));
};
const removeSelectedItems = () => {
setItems(items => items.filter(item => !item.selected));
};
const setMediaItemState = (id, state) => {
setItems(items => items.map(item => (item.id === id ? Object.assign(Object.assign({}, item), state) : item)));
};
const uploadItems = (ignoreError = false) => {
items.forEach(item => {
if (item.uploading || item.url) {
return;
}
if (!ignoreError && item.error) {
return;
}
let uploader;
if (item.type === 'IMAGE') {
createThumbnail(item);
uploader = uploadFn || createInlineImage;
}
else if (!uploadFn) {
setMediaItemState(item.id, { error: 1 });
return;
}
setMediaItemState(item.id, {
uploading: true,
uploadProgress: 0,
error: 0
});
uploader === null || uploader === void 0 ? void 0 : uploader({
id: item.id,
file: item.file,
success: res => {
handleUploadSuccess(item.id, res);
},
progress: progress => {
setMediaItemState(item.id, {
uploading: true,
uploadProgress: progress
});
},
error: () => {
setMediaItemState(item.id, {
uploading: false,
error: 2
});
}
});
});
};
const createThumbnail = ({ id, file }) => {
compressImage(URL.createObjectURL(file), 226, 226)
.then((result) => {
setMediaItemState(id, { thumbnail: result.url });
})
.catch(console.error);
};
const createInlineImage = param => {
compressImage(URL.createObjectURL(param.file), 1280, 800)
.then((result) => {
param.success({ url: result.url });
})
.catch(error => {
param.error(error);
});
};
const handleUploadSuccess = (id, data) => {
var _a;
setMediaItemState(id, Object.assign(Object.assign({}, data), { file: null, uploadProgress: 1, uploading: false, selected: false }));
const item = getMediaItem(data.id || id);
(_a = item.onReady) === null || _a === void 0 ? void 0 : _a.call(item, item);
};
const uploadImage = (file, callback) => {
const fileId = new Date().getTime() + '_' + UniqueIndex();
addMediaItem({
type: 'IMAGE',
id: fileId,
file: file,
name: fileId,
size: file.size,
uploadProgress: 0,
uploading: false,
selected: false,
error: 0,
onReady: callback
});
};
const addResolvedFiles = (param, index, accepts) => {
const data = {
id: new Date().getTime() + '_' + UniqueIndex(),
file: param.files[index],
name: param.files[index].name,
size: param.files[index].size,
uploadProgress: 0,
uploading: false,
selected: false,
error: 0,
onReady: item => {
var _a;
(_a = param.onItemReady) === null || _a === void 0 ? void 0 : _a.call(param, item);
}
};
if (param.files[index].type.indexOf('image/') === 0 && accepts.image) {
data.type = 'IMAGE';
addMediaItem(data);
}
else if (param.files[index].type.indexOf('video/') === 0 &&
accepts.video) {
data.type = 'VIDEO';
addMediaItem(data);
}
else if (param.files[index].type.indexOf('audio/') === 0 &&
accepts.audio) {
data.type = 'AUDIO';
addMediaItem(data);
}
setTimeout(() => {
resolveFiles(param, index + 1, accepts).catch(console.error);
}, 60);
};
const resolveFiles = (param, index, accepts) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
if (index < param.files.length) {
let validateResult = true;
if (validateFn) {
validateResult = yield validateFn(param.files[index]);
}
if (validateResult) {
addResolvedFiles(param, index, accepts);
}
}
else {
(_a = param.onAllReady) === null || _a === void 0 ? void 0 : _a.call(param);
}
});
const dragCounter = useRef(0);
const [draging, setDraging] = useState(false);
const confirmable = useMemo(() => {
return items.find(({ selected }) => selected);
}, [items]);
const [external, setExternal] = useState({ url: '', type: 'IMAGE' });
const [fileAccept, setFileAccept] = useState('');
const [showExternalForm, setShowExternalForm] = useState(false);
const [allowExternal, setAllowExternal] = useState(false);
useEffect(() => {
const newAccepts = Object.assign(Object.assign({}, defaultAccepts), accepts);
const fileAccept = !newAccepts
? [
defaultAccepts.image,
defaultAccepts.video,
defaultAccepts.audio
].join(',')
: [newAccepts.image, newAccepts.video, newAccepts.audio]
.filter(item => item)
.join(',');
const external = {
url: '',
type: externals.image
? 'IMAGE'
: externals.audio
? 'AUDIO'
: externals.video
? 'VIDEO'
: externals.embed
? 'EMBED'
: ''
};
setFileAccept(fileAccept);
setExternal(external);
setAllowExternal(externals &&
(externals.image ||
externals.audio ||
externals.video ||
externals.embed));
}, [accepts, externals]);
const buildItemList = () => {
return (React.createElement("ul", { className: cls('kedao-list') },
React.createElement("li", { className: cls('kedao-add-item') },
React.createElement(PlusIcon, Object.assign({}, tablerIconProps)),
React.createElement("input", { accept: fileAccept, onChange: reslovePickedFiles, multiple: true, type: 'file' })),
items.map((item, index) => {
let previewerComponents = null;
const progressMarker = item.uploading
? (React.createElement("div", { className: cls('kedao-item-uploading') },
React.createElement("div", { className: cls('kedao-item-uploading-bar'), style: { width: item.uploadProgress / 1 + '%' } })))
: ('');
switch (item.type) {
case 'IMAGE':
previewerComponents = (React.createElement("div", { className: cls('finder-image') },
progressMarker,
React.createElement("img", { src: item.thumbnail || item.url })));
break;
case 'VIDEO':
previewerComponents = (React.createElement("div", { className: cls('kedao-icon kedao-video'), title: item.url },
progressMarker,
React.createElement(MovieIcon, Object.assign({}, tablerIconProps)),
React.createElement("span", null, item.name || item.url)));
break;
case 'AUDIO':
previewerComponents = (React.createElement("div", { className: cls('kedao-icon kedao-audio'), title: item.url },
progressMarker,
React.createElement(MusicIcon, Object.assign({}, tablerIconProps)),
React.createElement("span", null, item.name || item.url)));
break;
case 'EMBED':
previewerComponents = (React.createElement("div", { className: cls('kedao-icon kedao-embed'), title: item.url },
progressMarker,
React.createElement(CodeIcon, Object.assign({}, tablerIconProps)),
React.createElement("span", null, item.name || language.finder.embed)));
break;
default:
previewerComponents = (React.createElement("a", { className: cls('kedao-icon kedao-file'), title: item.url, href: item.url },
progressMarker,
React.createElement(FileDescriptionIcon, Object.assign({}, tablerIconProps)),
React.createElement("span", null, item.name || item.url)));
break;
}
const className = ['kedao-item'];
item.selected && className.push('active');
item.uploading && className.push('uploading');
item.error && className.push('error');
return (React.createElement("li", { key: index, title: item.name, "data-id": item.id, className: cls(className.join(' ')), onClick: toggleSelectItem },
previewerComponents,
item.selected && (React.createElement("div", { className: cls('kedao-icon-selected') },
React.createElement(CheckIcon, Object.assign({}, tablerIconProps, { size: 50, color: 'white' })))),
React.createElement(XIcon, { "data-id": item.id, onClick: removeItem, className: cls('kedao-item-remove') }),
React.createElement("span", { className: cls('kedao-item-title') }, item.name)));
})));
};
const toggleSelectItem = event => {
const itemId = event.currentTarget.dataset.id;
const item = getMediaItem(itemId);
if (!item) {
return;
}
if (item.selected) {
deselectMediaItem(itemId);
}
else {
selectMediaItem(itemId);
}
};
const removeItem = event => {
const itemId = event.currentTarget.dataset.id;
const item = getMediaItem(itemId);
if (!item) {
return;
}
removeMediaItem(itemId);
event.stopPropagation();
};
const handleDragLeave = event => {
event.preventDefault();
dragCounter.current = dragCounter.current - 1;
dragCounter.current === 0 && setDraging(false);
};
const handleDragDrop = (event) => __awaiter(void 0, void 0, void 0, function* () {
event.preventDefault();
dragCounter.current = 0;
setDraging(false);
yield reslovePickedFiles(event);
});
const handleDragEnter = event => {
event.preventDefault();
dragCounter.current = dragCounter.current + 1;
setDraging(true);
};
const reslovePickedFiles = (event) => __awaiter(void 0, void 0, void 0, function* () {
event.persist();
const { files } = event.type === 'drop' ? event.dataTransfer : event.target;
const newAccepts = Object.assign(Object.assign({}, defaultAccepts), accepts);
yield resolveFiles({
files: files,
onItemReady: ({ id }) => selectMediaItem(id),
onAllReady: () => {
event.target.value = null;
}
}, 0, newAccepts);
});
const inputExternal = event => {
setExternal(external => (Object.assign(Object.assign({}, external), { url: event.target.value })));
};
const switchExternalType = event => {
setExternal(external => (Object.assign(Object.assign({}, external), { type: event.target.dataset.type })));
};
const confirmAddExternal = event => {
if (event.target.nodeName.toLowerCase() === 'button' ||
event.keyCode === 13) {
let { url, type } = external;
const urlArr = url.split('|');
const name = urlArr.length > 1 ? urlArr[0] : language.finder.unnamedItem;
url = urlArr.length > 1 ? urlArr[1] : urlArr[0];
const thumbnail = type === 'IMAGE' ? url : null;
addItems([
{
thumbnail,
url,
name,
type,
id: new Date().getTime() + '_' + UniqueIndex(),
uploading: false,
uploadProgress: 1,
selected: true
}
]);
setShowExternalForm(false);
setExternal({ url: '', type: 'IMAGE' });
}
};
const toggleExternalForm = () => {
setShowExternalForm(v => !v);
};
const cancelInsert = () => {
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
};
const confirmInsert = () => {
const selectedItems = getSelectedItems();
deselectAllItems();
onInsert === null || onInsert === void 0 ? void 0 : onInsert(selectedItems);
};
return (React.createElement("div", { className: cls('kedao-finder') },
React.createElement("div", { onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDrop: handleDragDrop, className: cls('kedao-uploader') },
React.createElement("div", { className: cls('kedao-drag-uploader ' +
(draging || !items.length ? 'active ' : ' ') +
(draging ? 'draging' : '')) },
React.createElement("span", { className: cls('kedao-drag-tip') },
React.createElement("input", { accept: fileAccept, onChange: reslovePickedFiles, multiple: true, type: 'file' }),
draging ? language.finder.dropTip : language.finder.dragTip)),
items.length
? (React.createElement("div", { className: cls('kedao-list-wrap') },
React.createElement("div", { className: cls('kedao-list-tools') },
React.createElement("span", { onClick: selectAllItems, className: cls('kedao-select-all') },
React.createElement(CheckIcon, Object.assign({}, tablerIconProps)),
language.finder.selectAll),
React.createElement("span", Object.assign({ onClick: deselectAllItems, className: cls('kedao-deselect-all') }, { disabled: !confirmable }),
React.createElement(XIcon, Object.assign({}, tablerIconProps)),
language.finder.deselect),
React.createElement("span", Object.assign({ onClick: removeSelectedItems, className: cls('kedao-remove-selected') }, { disabled: !confirmable }),
React.createElement(TrashIcon, Object.assign({}, tablerIconProps)),
language.finder.removeSelected)),
buildItemList()))
: null,
showExternalForm && allowExternal
? (React.createElement("div", { className: cls('kedao-add-external') },
React.createElement("div", { className: cls('kedao-external-form') },
React.createElement("div", { className: cls('kedao-external-input') },
React.createElement("div", null,
React.createElement("input", { onKeyDown: confirmAddExternal, value: external.url, onChange: inputExternal, placeholder: language.finder.externalInputPlaceHolder })),
React.createElement("button", { type: 'button', onClick: confirmAddExternal, disabled: !external.url.trim().length }, language.finder.confirm)),
React.createElement("div", { "data-type": external.type, className: cls('kedao-switch-external-type') },
externals.image
? (React.createElement("button", { type: 'button', onClick: switchExternalType, "data-type": 'IMAGE' }, language.finder.image))
: null,
externals.audio
? (React.createElement("button", { type: 'button', onClick: switchExternalType, "data-type": 'AUDIO' }, language.finder.audio))
: null,
externals.video
? (React.createElement("button", { type: 'button', onClick: switchExternalType, "data-type": 'VIDEO' }, language.finder.video))
: null,
externals.embed
? (React.createElement("button", { type: 'button', onClick: switchExternalType, "data-type": 'EMBED' }, language.finder.embed))
: null),
React.createElement("span", { className: cls('kedao-external-tip') }, language.finder.externalInputTip))))
: null),
React.createElement("footer", { className: cls('kedao-manager-footer') },
React.createElement("div", { className: cls('pull-left') }, allowExternal
? (React.createElement("span", { onClick: toggleExternalForm, className: cls('kedao-toggle-external-form') }, showExternalForm
? (React.createElement("span", { className: cls('kedao-bottom-text') },
React.createElement(PlusIcon, Object.assign({}, tablerIconProps)),
language.finder.addLocalFile))
: (React.createElement("span", { className: cls('kedao-bottom-text') },
React.createElement(PlusIcon, Object.assign({}, tablerIconProps)),
language.finder.addExternalSource))))
: null),
React.createElement("div", { className: cls('pull-right') },
React.createElement("button", { onClick: confirmInsert, className: cls('button button-insert'), disabled: !confirmable }, language.finder.insert),
React.createElement("button", { onClick: cancelInsert, className: cls('button button-cancel') }, language.finder.cancel)))));
});
export default Finder;