UNPKG

kedao

Version:

Rich Text Editor Based On Draft.js

461 lines (460 loc) 22 kB
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;