core&ui: support pasting image in editor

This commit is contained in:
undefined committed 2022-03-01 21:40:09 +08:00
1 parent 6a289f788d
commit 8223b45d23
13 files changed
+131 -39

No files matched your search

+1 -3
View File
@@ -140,9 +140,7 @@ env.addGlobal('set', (obj, key, val) => {
else Object.assign(obj, key);
return '';
});
env.addGlobal('findSubModule', (prefix) => {
filter(Object.keys(global.Hydro.ui.template), (n) => n.startsWith(prefix));
});
env.addGlobal('findSubModule', (prefix) => Object.keys(global.Hydro.ui.template).filter((n) => n.startsWith(prefix)));
async function render(name, state) {
// eslint-disable-next-line no-return-await
@@ -2,7 +2,9 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { EditorAction, registerEditorAction } from 'monaco-editor/esm/vs/editor/browser/editorExtensions';
import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickInput';
import list from 'monaco-themes/themes/themelist.json';
import { nanoid } from 'nanoid';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import './monaco.styl';
export default monaco;
@@ -59,6 +61,72 @@ class ChangeThemeAction extends EditorAction {
registerEditorAction(ChangeThemeAction);
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
const isProblemEdit = pagename === 'problem_edit';
function handlePasteEvent(editor: monaco.editor.IStandaloneCodeEditor) {
window.addEventListener('paste', (ev: ClipboardEvent) => {
if (!editor.hasTextFocus()) return;
const selection = editor.getSelection();
const { items } = ev.clipboardData;
let wrapper = ['', ''];
let ext;
const matches = items[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
wrapper = ['![image](', ')'];
[, ext] = matches;
} else if (items[0].type === 'application/x-zip-compressed') {
wrapper = ['[file](', ')'];
ext = 'zip';
}
if (!ext) return;
const filename = `${nanoid()}.${ext}`;
const data = new FormData();
data.append('filename', filename);
data.append('file', items[0].getAsFile());
data.append('operation', 'upload_file');
if (isProblemEdit) data.append('type', 'additional_file');
let range: monaco.Range = null;
editor.executeEdits('', [{
range: new monaco.Range(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn),
text: `![image](${i18n('Preparing Upload...')}) `,
}], (inverseOp) => { range = inverseOp[0].range; return null; });
editor.setPosition(editor.getSelection().getEndPosition());
function updateText(text: string) {
const pos = editor.getSelection();
const rangeBefore = range;
editor.executeEdits('', [{ range, text: `${wrapper[0]}${text}${wrapper[1]} ` }], (inverseOp) => {
range = inverseOp[0].range;
if (pos.endLineNumber !== pos.startLineNumber || pos.endLineNumber !== range.endLineNumber) return null;
const delta = rangeBefore.endColumn - range.endColumn;
editor.setPosition(new monaco.Position(pos.endLineNumber, pos.endColumn - delta));
return null;
});
}
let progress = 0;
request.postFile(isProblemEdit ? './file' : '/file', data, {
xhr() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => updateText(i18n('Uploading...')));
xhr.upload.addEventListener('progress', (e) => {
if (!e.lengthComputable) return;
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === progress) return;
progress = percentComplete;
updateText(`${i18n('Uploading...')} ${percentComplete}%`);
}, false);
return xhr;
},
})
.then(() => updateText(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`))
.catch((e) => {
console.error(e);
updateText(`${i18n('Upload Failed')}: ${e.message}`);
});
});
}
export function registerAction(
editor: monaco.editor.IStandaloneCodeEditor,
model: monaco.editor.IModel,
@@ -92,5 +160,6 @@ export function registerAction(
if (model.getLanguageId() === 'markdown') {
const suggestWidget = (editor.getContribution('editor.contrib.suggestController') as any).widget?.value;
if (suggestWidget?._setDetailsVisible) suggestWidget._setDetailsVisible(true);
handlePasteEvent(editor);
}
}
@@ -92,6 +92,7 @@ export default class Tab extends DOMAttachedObject {
.removeClass('active');
this.$content
.css('x', '0');
$newTab.trigger('vjContentShow');
// 4. Finalize
this.currentIndex = idx;
+7
View File
@@ -52,6 +52,8 @@ About test data: 关于测试数据
about: 关于
About: 关于
AC Message: 通过消息
Accept: 通过
Accepted Problem: 通过的题目
Accepted Ratio: 通过率
Accepted: 已通过
Access Control: 访问控制
@@ -574,6 +576,7 @@ Problem Settings: 题目设置
Problem Solution List: 题解列表
Problem Solution: 题解列表
Problem Tags Visibility: 题目标签可见性
Problem Tags: 题目标签
problem_create: 创建题目
problem_edit: 编辑题目
problem_import: 导入题目
@@ -825,8 +828,12 @@ User ID: 用户 ID
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
user_detail: 用户
user_login: 登录
Uploading...: 正在上传...
Upload Failed: 上传失败
Preparing Upload...: 准备上传...
user_logout: 登出
user_lostpass: 忘记密码
user_register_with_code: 注册
user_register: 注册
User-defined role: 用户定义的角色
User: 用户
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.32.10",
"version": "4.32.11",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
+24 -25
View File
@@ -99,35 +99,34 @@
{% endif %}
</div>
</li>
{% if pdocs.length %}
<li class="section__tab-item">
<h1 class="section__tab-title">{{ _('Accepted Problems') }}</h1>
<div class="section__tab-main">
<div class="typo section__body no-media">
{% set _mark = Math.ceil(pdocs.length/4) %}
{% macro col(pdocs) %}
<div class="medium-3 columns"><ul>
{% for pdoc in pdocs %}
<li>{{ problem.render_problem_title(pdoc, show_tags=false, small=true) }}</li>
{% endfor %}
</ul></div>
{% endmacro %}
<div class="row">
{{ col(pdocs.slice(0, _mark)) }}
{{ col(pdocs.slice(_mark, _mark*2)) }}
{{ col(pdocs.slice(_mark*2, _mark*3)) }}
{{ col(pdocs.slice(_mark*3, pdocs.length - 1)) }}
</div>
</div>
</div>
</li>
{% endif %}
{% for page in findSubModule('user_detail/') %}{% include page %}{% endfor %}
</ul>
</div>
</div>
</div>
{% if pdocs.length %}
<div class="section">
<div class="section__header">
<h2 class="section__title">{{ _('Accepted Problems') }}</h2>
</div>
<div class="typo section__body no-media">
{% set _mark = Math.ceil(pdocs.length/4) %}
{% macro col(pdocs) %}
<div class="medium-3 columns">
<ul>
{% for pdoc in pdocs %}
<li>{{ problem.render_problem_title(pdoc, show_tags=false, small=true) }}</li>
{% endfor %}
</ul>
</div>
{% endmacro %}
<div class="row">
{{ col(pdocs.slice(0, _mark)) }}
{{ col(pdocs.slice(_mark, _mark*2)) }}
{{ col(pdocs.slice(_mark*2, _mark*3)) }}
{{ col(pdocs.slice(_mark*3, pdocs.length - 1)) }}
</div>
</div>
</div>
{% endif %}
</div>
<div class="medium-3 columns">
{% if isSelfProfile %}
+2 -2
View File
@@ -21,8 +21,6 @@ const request = {
reject(err);
} else if (jqXHR.readyState === 0) {
reject(new Error(i18n('Network error')));
} else if (errorThrown instanceof Error) {
reject(errorThrown);
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
const { error } = jqXHR.responseJSON;
if (error.params) {
@@ -32,6 +30,8 @@ const request = {
err.params = error.params;
reject(err);
} else reject(new Error(jqXHR.responseJSON.error.message));
} else if (errorThrown instanceof Error) {
reject(errorThrown);
} else {
reject(new Error(textStatus));
}