mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 17:46:28 +08:00
Merge branch 'master' into monaco
This commit is contained in:
93 files changed
+1414
-736
No files matched your search
@@ -24,9 +24,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
|
||||
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
||||
options: CustomSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
|
||||
@@ -27,9 +27,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
|
||||
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||
options: FileSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
|
||||
@@ -11,10 +11,7 @@ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||
<AutoComplete<DefaultProps>
|
||||
ref={ref as any}
|
||||
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name
|
||||
? i.name.toLowerCase().match(query.toLowerCase())
|
||||
: i.toString().toLowerCase().match(query.toLowerCase())
|
||||
))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
||||
itemText={(item) => `${item.name || item}`}
|
||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||
renderItem={(item) => `${item.name || item}`}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
defaultItems?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
classes?: string;
|
||||
listStyle?: any;
|
||||
allowEmptyQuery?: boolean;
|
||||
freeSolo?: boolean;
|
||||
@@ -18,17 +19,17 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
text?: () => string;
|
||||
}
|
||||
|
||||
export default class AutoComplete extends DOMAttachedObject {
|
||||
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||
ref = null;
|
||||
container = document.createElement('div');
|
||||
options: AutoCompleteOptions;
|
||||
options: AutoCompleteOptions & Options;
|
||||
component = ReactDOM.createRoot(this.container);
|
||||
changeListener = [
|
||||
(val) => this.$dom.val(val),
|
||||
];
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
constructor($dom, options = {} as Options) {
|
||||
super($dom);
|
||||
this.options = {
|
||||
items: async () => [],
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
|
||||
.drop.nav__dropdown
|
||||
max-height: calc(100% - 60px)
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
|
||||
|
||||
.drop.nav__dropdown.drop-open
|
||||
overflow-x: auto
|
||||
|
||||
.dropdown
|
||||
.menu
|
||||
min-width: rem(150px)
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
.vditor
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
.vditor-preview__action
|
||||
display: none
|
||||
|
||||
.vditor ol
|
||||
list-style: decimal
|
||||
|
||||
.decoration-hide
|
||||
color: grey !important
|
||||
background-color: grey !important
|
||||
+103
-109
@@ -1,36 +1,11 @@
|
||||
import $ from 'jquery';
|
||||
import type { editor } from 'monaco-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { i18n, request } from 'vj/utils';
|
||||
|
||||
export const config = {
|
||||
toolbar: [
|
||||
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
|
||||
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
|
||||
'quote', 'line', 'code', 'inline-code', 'table', '|',
|
||||
'upload', 'edit-mode', 'fullscreen', 'export',
|
||||
],
|
||||
mode: UserContext.preferredEditorType || 'ir',
|
||||
toolbarConfig: {
|
||||
pin: true,
|
||||
},
|
||||
cdn: `${UiContext.cdn_prefix}vditor`,
|
||||
counter: {
|
||||
enable: true,
|
||||
max: 65536,
|
||||
},
|
||||
preview: {
|
||||
math: {
|
||||
inlineDigit: true,
|
||||
},
|
||||
theme: {
|
||||
path: `${UiContext.cdn_prefix}vditor/dist/css/content-theme`,
|
||||
current: 'light',
|
||||
},
|
||||
},
|
||||
};
|
||||
import { i18n } from 'vj/utils';
|
||||
import uploadFiles from '../upload';
|
||||
|
||||
interface MonacoOptions {
|
||||
language?: string;
|
||||
@@ -43,23 +18,23 @@ interface MonacoOptions {
|
||||
hide?: string[];
|
||||
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
||||
}
|
||||
interface VditorOptions {
|
||||
theme?: 'classic' | 'dark'
|
||||
}
|
||||
type Options = MonacoOptions & VditorOptions;
|
||||
type Options = MonacoOptions;
|
||||
|
||||
export default class Editor extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjEditorInstance';
|
||||
model: editor.IModel;
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
vditor: import('vditor').default;
|
||||
markdownEditor: any;
|
||||
valueCache?: string;
|
||||
setMarkdownEditorValue?: (v: string) => void;
|
||||
reactRoot?: ReactDOM.Root;
|
||||
isValid: boolean;
|
||||
|
||||
constructor($dom, public options: Options = {}) {
|
||||
super($dom);
|
||||
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
||||
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
||||
else this.initVditor();
|
||||
else this.initMarkdownEditor();
|
||||
}
|
||||
|
||||
async initMonaco() {
|
||||
@@ -169,87 +144,102 @@ export default class Editor extends DOMAttachedObject {
|
||||
window.editor = this.editor;
|
||||
}
|
||||
|
||||
async initVditor() {
|
||||
async initMarkdownEditor() {
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
const isProblemEdit = pagename === 'problem_edit';
|
||||
const { default: Vditor } = await import('vditor');
|
||||
const that = this;
|
||||
const { $dom } = this;
|
||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||
const origin = $dom.get(0);
|
||||
const ele = document.createElement('div');
|
||||
const value = $dom.val();
|
||||
const { onChange } = this.options;
|
||||
await new Promise((resolve) => {
|
||||
this.vditor = new Vditor(ele, {
|
||||
...config,
|
||||
upload: {
|
||||
multiple: false,
|
||||
handler: (files) => {
|
||||
let wrapper = ['', ''];
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
wrapper = [''];
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
wrapper = ['[file](', ')'];
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
const data = new FormData();
|
||||
data.append('filename', filename);
|
||||
data.append('file', files[0]);
|
||||
data.append('operation', 'upload_file');
|
||||
if (isProblemEdit) data.append('type', 'additional_file');
|
||||
let progress = 0;
|
||||
request.postFile(isProblemEdit ? './files' : '/file', data, {
|
||||
xhr: () => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (!e.lengthComputable) return;
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
if (percentComplete === progress) return;
|
||||
progress = percentComplete;
|
||||
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
|
||||
this.vditor.vditor.tip.hide();
|
||||
})
|
||||
.catch((e: { message: string; }) => {
|
||||
console.error(e);
|
||||
return `${i18n('Upload Failed')}: ${e.message}`;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
},
|
||||
...this.options,
|
||||
after: () => {
|
||||
const pos = $(ele).find('button[data-mode="sv"]').get();
|
||||
const button = $('<button data-mode="monaco">Monaco Editor</button>');
|
||||
button.on('click', (e) => {
|
||||
Notification.info('You can select this in PreferenceSettings');
|
||||
e.preventDefault();
|
||||
});
|
||||
button.insertAfter(pos);
|
||||
resolve(null);
|
||||
},
|
||||
input(v) {
|
||||
const { MdEditor } = await import('./mdeditor');
|
||||
|
||||
const renderCallback = (ref) => {
|
||||
this.markdownEditor = ref;
|
||||
};
|
||||
|
||||
function EditorComponent() {
|
||||
const [val, setVal] = React.useState(value);
|
||||
that.setMarkdownEditorValue = setVal;
|
||||
return <MdEditor
|
||||
className='textbox'
|
||||
autoFocus={hasFocus}
|
||||
codeTheme='github'
|
||||
codeStyleReverse={false}
|
||||
ref={renderCallback}
|
||||
modelValue={val}
|
||||
noMermaid
|
||||
noPrettier
|
||||
autoDetectCode
|
||||
toolbarsExclude={[
|
||||
// 'bold',
|
||||
// 'underline',
|
||||
// 'italic',
|
||||
// '-',
|
||||
// 'strikeThrough',
|
||||
// 'title',
|
||||
// 'sub',
|
||||
// 'sup',
|
||||
// 'quote',
|
||||
// 'unorderedList',
|
||||
// 'orderedList',
|
||||
// 'task',
|
||||
// '-',
|
||||
// 'codeRow',
|
||||
// 'code',
|
||||
// 'link',
|
||||
// 'image',
|
||||
// 'table',
|
||||
'mermaid',
|
||||
// 'katex',
|
||||
// '-',
|
||||
// 'revoke',
|
||||
// 'next',
|
||||
'save',
|
||||
// '=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
// 'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
// 'catalog',
|
||||
'github',
|
||||
]}
|
||||
onChange={(v) => {
|
||||
that.valueCache = v;
|
||||
setVal(v);
|
||||
$dom.val(v);
|
||||
$dom.text(v);
|
||||
if (onChange) onChange(v);
|
||||
},
|
||||
value,
|
||||
cache: { id: Math.random().toString() },
|
||||
});
|
||||
});
|
||||
$(ele).addClass('textbox');
|
||||
onChange?.(v);
|
||||
}}
|
||||
onUploadImg={async (files, callback) => {
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
|
||||
type: isProblemEdit ? 'additional_file' : undefined,
|
||||
filenameCallback: () => filename,
|
||||
}).then(() => {
|
||||
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
|
||||
}).catch(() => {
|
||||
callback([]);
|
||||
});
|
||||
return null;
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
this.reactRoot = ReactDOM.createRoot(ele);
|
||||
this.reactRoot.render(<EditorComponent />);
|
||||
$dom.hide();
|
||||
origin.parentElement.appendChild(ele);
|
||||
this.isValid = true;
|
||||
@@ -258,7 +248,7 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
destroy() {
|
||||
this.detach();
|
||||
if (this.vditor?.destroy) this.vditor.destroy();
|
||||
if (this.reactRoot) this.reactRoot.unmount();
|
||||
else if (this.editor?.dispose) this.editor.dispose();
|
||||
}
|
||||
|
||||
@@ -268,13 +258,17 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
value(val?: string) {
|
||||
this.ensureValid();
|
||||
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
|
||||
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
if (typeof val === 'string') {
|
||||
if (this.editor) return this.editor.setValue(val);
|
||||
this.setMarkdownEditorValue?.(val);
|
||||
this.markdownEditor?.resetHistory?.();
|
||||
}
|
||||
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
return this.valueCache;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.ensureValid();
|
||||
this.vditor?.focus();
|
||||
if (!this.editor || !this.model) return;
|
||||
this.editor.focus();
|
||||
const range = this.model.getFullModelRange();
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'md-editor-rt/lib/style.css';
|
||||
|
||||
import Anchor from 'markdown-it-anchor';
|
||||
import Footnote from 'markdown-it-footnote';
|
||||
import Mark from 'markdown-it-mark';
|
||||
import MergeCells from 'markdown-it-merge-cells';
|
||||
import TOC from 'markdown-it-table-of-contents';
|
||||
import { config, MdEditor } from 'md-editor-rt';
|
||||
import Imsize from '../../backendlib/markdown-it-imsize';
|
||||
import { Media } from '../../backendlib/markdown-it-media';
|
||||
import { xssProtector } from '../../backendlib/markdown-it-xss';
|
||||
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
|
||||
config({
|
||||
markdownItConfig(mdit) {
|
||||
mdit.options.html = true;
|
||||
mdit.options.linkify = true;
|
||||
mdit.linkify.tlds('.py', false);
|
||||
mdit.linkify.tlds('.zip', false);
|
||||
mdit.linkify.tlds('.mov', false);
|
||||
mdit.use(Media);
|
||||
mdit.use(Footnote);
|
||||
mdit.use(Mark);
|
||||
mdit.use(Imsize);
|
||||
mdit.use(Anchor);
|
||||
mdit.use(TOC);
|
||||
mdit.use(MergeCells);
|
||||
mdit.use(xssProtector);
|
||||
if (isProblemPage) {
|
||||
mdit.core.ruler.before('normalize', 'xss', (state) => {
|
||||
state.src = state.src.replace(/file:\/\//g, pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/');
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export { MdEditor };
|
||||
@@ -1,5 +1,4 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import { InfoDialog } from 'vj/components/dialog';
|
||||
import VjNotification from 'vj/components/notification/index';
|
||||
import {
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
} from 'vj/constant/message';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { i18n, tpl } from 'vj/utils';
|
||||
import Sock from '../socket';
|
||||
|
||||
let previous: VjNotification;
|
||||
const onmessage = (msg) => {
|
||||
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
|
||||
|
||||
const initWorkerMode = (endpoint) => {
|
||||
console.log('Messages: using SharedWorker');
|
||||
const worker = new SharedWorker('/messages-shared-worker.js', { name: 'HydroMessagesWorker' });
|
||||
const worker = new SharedWorker(new URL('./worker.ts', import.meta.url), { name: 'HydroMessagesWorker' });
|
||||
worker.port.start();
|
||||
window.addEventListener('beforeunload', () => {
|
||||
worker.port.postMessage({ type: 'unload' });
|
||||
@@ -128,12 +128,8 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||
isMaster = true;
|
||||
localStorage.setItem('page.master', selfId);
|
||||
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||
const sock = new ReconnectingWebsocket(endpoint);
|
||||
sock.onopen = () => console.log('Connected');
|
||||
sock.onerror = console.error;
|
||||
sock.onclose = (...args) => console.log('Closed', ...args);
|
||||
const sock = new Sock(endpoint);
|
||||
sock.onmessage = async (message) => {
|
||||
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
|
||||
const payload = JSON.parse(message.data);
|
||||
masterChannel.postMessage({ type: 'message', payload });
|
||||
};
|
||||
|
||||
@@ -31,12 +31,17 @@ function broadcastMsg(message: any) {
|
||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
if (cookie !== lcookie) conn?.close();
|
||||
else if (conn && conn.readyState === conn.OPEN) return;
|
||||
const url = new URL(path, location.origin);
|
||||
if (cookie) url.searchParams.set('sid', cookie.split('sid=')[1].split(';')[0]);
|
||||
lcookie = cookie;
|
||||
console.log('Init connection for', path);
|
||||
conn?.close();
|
||||
conn = new ReconnectingWebsocket(path);
|
||||
conn = new ReconnectingWebsocket(url.toString());
|
||||
ports.push(port);
|
||||
conn.onopen = () => conn.send(cookie);
|
||||
conn.onopen = () => {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
};
|
||||
conn.onerror = () => broadcastMsg({ type: 'error' });
|
||||
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
||||
conn.onmessage = (message) => {
|
||||
@@ -46,14 +51,9 @@ function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(message.data);
|
||||
if (payload.event === 'auth') {
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
}
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
broadcastMsg({ type: 'message', payload });
|
||||
let acked = false;
|
||||
|
||||
@@ -87,8 +87,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||
disposable = [];
|
||||
containerElement: HTMLElement;
|
||||
private __preventUpdate = false;
|
||||
private __preventFormat = false;
|
||||
__preventUpdate = false;
|
||||
__preventFormat = false;
|
||||
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
model: editor.ITextModel;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`;
|
||||
if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`;
|
||||
|
||||
// TODO switch to indexeddb
|
||||
export default function reducer(state = {
|
||||
lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang,
|
||||
code: localStorage.getItem(cacheKey) || UiContext.codeTemplate,
|
||||
|
||||
@@ -54,6 +54,10 @@ export default class Sock {
|
||||
onclose: (code: number, reason: string) => void;
|
||||
onopen: (sock: ReconnectingWebSocket) => void;
|
||||
|
||||
on(event: 'message' | 'close' | 'open', callback: (...args: any[]) => void) {
|
||||
this[`on${event}`] = callback;
|
||||
}
|
||||
|
||||
send(data) {
|
||||
this.sock.send(data);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
const TAB_TRANSITION_DURATION = 300;
|
||||
@@ -144,5 +143,3 @@ export default class Tab extends DOMAttachedObject {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(Tab, DOMAttachedObject);
|
||||
@@ -73,6 +73,26 @@
|
||||
background: $secondary-color
|
||||
content: ''
|
||||
|
||||
.section__tab-container:not(.immersive)
|
||||
.section__tab-header
|
||||
padding-top: 0 !important
|
||||
|
||||
.section__tab-header-item
|
||||
border-top: 3px solid transparent;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
margin-right: 0
|
||||
|
||||
&:hover
|
||||
border-color: #5f9fd6;
|
||||
background: #f8f8f8;
|
||||
|
||||
&.tab--active
|
||||
border-color: #ed5f82;
|
||||
|
||||
&.selected
|
||||
border-color: #ed5f82;
|
||||
|
||||
.section__tab-content
|
||||
position: relative
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ interface UploadOptions {
|
||||
pjax?: boolean;
|
||||
sidebar?: boolean;
|
||||
singleFileUploadCallback?: (file: File) => any;
|
||||
filenameCallback?: (file: File) => string;
|
||||
}
|
||||
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
||||
const dialog = new Dialog({
|
||||
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
|
||||
if (Number.isNaN(+i)) continue;
|
||||
const file = files[i];
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('filename', options.filenameCallback?.(file) || file.name);
|
||||
data.append('file', file);
|
||||
if (options.type) data.append('type', options.type);
|
||||
data.append('operation', 'upload_file');
|
||||
|
||||
Reference in new issue
Block a user