mirror of
https://github.com/renbaoshuo/S2OJ.git
synced 2024-11-08 12:58:42 +00:00
feat: Markdown Editor for UOJForm (#46)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
ed06c4492b
@ -64,10 +64,9 @@ if ($cur_tab == 'profile') {
|
|||||||
'option_div_class' => 'form-check d-inline-block ms-2',
|
'option_div_class' => 'form-check d-inline-block ms-2',
|
||||||
'default_value' => UOJGroup::info('is_hidden'),
|
'default_value' => UOJGroup::info('is_hidden'),
|
||||||
]);
|
]);
|
||||||
$update_profile_form->addTextArea('announcement', [
|
$update_profile_form->addMarkdownEditor('announcement', [
|
||||||
'div_class' => 'mt-3',
|
'div_class' => 'mt-3',
|
||||||
'label' => '公告',
|
'label' => '公告',
|
||||||
'input_class' => 'form-control font-monospace',
|
|
||||||
'default_value' => UOJGroup::info('announcement'),
|
'default_value' => UOJGroup::info('announcement'),
|
||||||
'help' => '公告支持 Markdown 语法。',
|
'help' => '公告支持 Markdown 语法。',
|
||||||
'validator_php' => function ($announcement, &$vdata) {
|
'validator_php' => function ($announcement, &$vdata) {
|
||||||
@ -287,11 +286,11 @@ if ($cur_tab == 'profile') {
|
|||||||
<div class="card mt-3 mt-md-0">
|
<div class="card mt-3 mt-md-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="result-alert" class="alert" role="alert" style="display: none"></div>
|
<div id="result-alert" class="alert" role="alert" style="display: none"></div>
|
||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md-8">
|
||||||
<?= $update_profile_form->printHTML() ?>
|
<?php $update_profile_form->printHTML() ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-md-4">
|
||||||
<h5>注意事项</h5>
|
<h5>注意事项</h5>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>隐藏的小组无法被普通用户查看,即使该用户属于本小组。</li>
|
<li>隐藏的小组无法被普通用户查看,即使该用户属于本小组。</li>
|
||||||
|
@ -25,6 +25,7 @@ if (!isset($tabs_info[$cur_tab])) {
|
|||||||
if ($cur_tab == 'profile') {
|
if ($cur_tab == 'profile') {
|
||||||
$update_profile_form = new UOJForm('update_profile');
|
$update_profile_form = new UOJForm('update_profile');
|
||||||
$update_profile_form->addInput('name', [
|
$update_profile_form->addInput('name', [
|
||||||
|
'div_class' => 'mb-3',
|
||||||
'label' => '标题',
|
'label' => '标题',
|
||||||
'default_value' => HTML::unescape(UOJList::info('title')),
|
'default_value' => HTML::unescape(UOJList::info('title')),
|
||||||
'validator_php' => function ($title, &$vdata) {
|
'validator_php' => function ($title, &$vdata) {
|
||||||
@ -47,7 +48,7 @@ if ($cur_tab == 'profile') {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
$update_profile_form->addCheckboxes('is_hidden', [
|
$update_profile_form->addCheckboxes('is_hidden', [
|
||||||
'div_class' => 'mt-3',
|
'div_class' => 'mb-3',
|
||||||
'label' => '可见性',
|
'label' => '可见性',
|
||||||
'label_class' => 'me-3',
|
'label_class' => 'me-3',
|
||||||
'select_class' => 'd-inline-block',
|
'select_class' => 'd-inline-block',
|
||||||
@ -59,6 +60,7 @@ if ($cur_tab == 'profile') {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$update_profile_form->addInput('tags', [
|
$update_profile_form->addInput('tags', [
|
||||||
|
'div_class' => 'mb-3',
|
||||||
'label' => '标签',
|
'label' => '标签',
|
||||||
'default_value' => implode(', ', UOJList::cur()->queryTags()),
|
'default_value' => implode(', ', UOJList::cur()->queryTags()),
|
||||||
'validator_php' => function ($tags_str, &$vdata) {
|
'validator_php' => function ($tags_str, &$vdata) {
|
||||||
@ -93,7 +95,7 @@ if ($cur_tab == 'profile') {
|
|||||||
},
|
},
|
||||||
'help' => '多个标签请使用逗号隔开。'
|
'help' => '多个标签请使用逗号隔开。'
|
||||||
]);
|
]);
|
||||||
$update_profile_form->addTextArea('content_md', [
|
$update_profile_form->addMarkdownEditor('content_md', [
|
||||||
'label' => '描述',
|
'label' => '描述',
|
||||||
'default_value' => UOJList::cur()->queryContent()['content_md'],
|
'default_value' => UOJList::cur()->queryContent()['content_md'],
|
||||||
'validator_php' => function ($content_md, &$vdata) {
|
'validator_php' => function ($content_md, &$vdata) {
|
||||||
@ -237,11 +239,11 @@ if ($cur_tab == 'profile') {
|
|||||||
<div class="card mt-3 mt-md-0">
|
<div class="card mt-3 mt-md-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="result-alert" class="alert" role="alert" style="display: none"></div>
|
<div id="result-alert" class="alert" role="alert" style="display: none"></div>
|
||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md-8">
|
||||||
<?= $update_profile_form->printHTML() ?>
|
<?= $update_profile_form->printHTML() ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="col mt-3 mt-md-0">
|
<div class="col-md-4">
|
||||||
<h5>注意事项</h5>
|
<h5>注意事项</h5>
|
||||||
<ul class="mb-0">
|
<ul class="mb-0">
|
||||||
<li>隐藏的题单无法被普通用户查看。</li>
|
<li>隐藏的题单无法被普通用户查看。</li>
|
||||||
|
24
web/app/controllers/subdomain/api/markdown.php
Normal file
24
web/app/controllers/subdomain/api/markdown.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
Auth::check() || redirectToLogin();
|
||||||
|
|
||||||
|
$parsedown_type = UOJRequest::post('parsedown_type', 'is_string', 'default');
|
||||||
|
$purifier_type = UOJRequest::post('purifier_type', 'is_string', 'default');
|
||||||
|
|
||||||
|
$markdown = UOJRequest::post('markdown', 'is_string', '');
|
||||||
|
|
||||||
|
$parsedown = HTML::parsedown();
|
||||||
|
|
||||||
|
if ($purifier_type == 'inline') {
|
||||||
|
$purifier = HTML::purifier_inline();
|
||||||
|
} else {
|
||||||
|
$purifier = HTML::purifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parsedown_type == 'inline') {
|
||||||
|
$html = $purifier->purify($parsedown->line(UOJRequest::post('markdown', 'is_string')));
|
||||||
|
} else {
|
||||||
|
$html = $purifier->purify($parsedown->text(UOJRequest::post('markdown', 'is_string')));
|
||||||
|
}
|
||||||
|
|
||||||
|
die($html);
|
@ -6,9 +6,14 @@ call_user_func(function () { // to prevent variable scope leak
|
|||||||
'domain' => UOJConfig::$data['web']['main']['host'],
|
'domain' => UOJConfig::$data['web']['main']['host'],
|
||||||
],
|
],
|
||||||
function () {
|
function () {
|
||||||
|
// Remote Judge
|
||||||
Route::post("/api/remote_judge/custom_account_validator", '/subdomain/api/remote_judge/custom_account_validator.php');
|
Route::post("/api/remote_judge/custom_account_validator", '/subdomain/api/remote_judge/custom_account_validator.php');
|
||||||
|
|
||||||
|
// Submission
|
||||||
Route::any('/api/submission/submission_status_details', '/subdomain/api/submission/submission_status_details.php');
|
Route::any('/api/submission/submission_status_details', '/subdomain/api/submission/submission_status_details.php');
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
Route::post('/api/markdown', '/subdomain/api/markdown.php');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -46,9 +46,9 @@ function getCommentContentToDisplay($comment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$comment_form = new UOJForm('comment');
|
$comment_form = new UOJForm('comment');
|
||||||
$comment_form->addTextArea('comment', [
|
$comment_form->addMarkdownEditor('comment', [
|
||||||
'label' => '内容',
|
'label' => '内容',
|
||||||
'help' => '评论支持 Markdown 语法。可以用 <code>@mike</code> 来提到 <code>mike</code> 这个用户,<code>mike</code> 会被高亮显示。如果你真的想打 <code>@</code> 这个字符,请用 <code>@@</code>。',
|
'help' => '评论支持 Markdown 语法。可以用 <code>@mike</code> 来提到 <code>mike</code> 这个用户,<code>mike</code> 会被高亮显示。',
|
||||||
'validator_php' => function ($comment) {
|
'validator_php' => function ($comment) {
|
||||||
if (!Auth::check()) {
|
if (!Auth::check()) {
|
||||||
return '请先登录';
|
return '请先登录';
|
||||||
@ -62,6 +62,7 @@ $comment_form->addTextArea('comment', [
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
$comment_form->config['ctrl_enter_submit'] = true;
|
||||||
$comment_form->handle = function () {
|
$comment_form->handle = function () {
|
||||||
global $blog, $comment_form;
|
global $blog, $comment_form;
|
||||||
$comment = $_POST['comment'];
|
$comment = $_POST['comment'];
|
||||||
@ -122,7 +123,7 @@ $reply_form->addHidden(
|
|||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
$reply_form->addTextArea('reply_comment', [
|
$reply_form->addMarkdownEditor('reply_comment', [
|
||||||
'label' => '内容',
|
'label' => '内容',
|
||||||
'validator_php' => function ($comment) {
|
'validator_php' => function ($comment) {
|
||||||
if (!Auth::check()) {
|
if (!Auth::check()) {
|
||||||
|
@ -507,6 +507,50 @@ class UOJForm {
|
|||||||
$this->config['has_file'] = true;
|
$this->config['has_file'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addMarkdownEditor($name, $config = []) {
|
||||||
|
$config += [
|
||||||
|
'div_class' => '',
|
||||||
|
'default_value' => '',
|
||||||
|
'label' => '',
|
||||||
|
'label_class' => 'form-label',
|
||||||
|
'placeholder' => '',
|
||||||
|
'help' => '',
|
||||||
|
'help_class' => 'form-text',
|
||||||
|
'validator_php' => function ($str, &$vdata) {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
'validator_js' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = '';
|
||||||
|
$html .= HTML::tag_begin('div', ['class' => $config['div_class'], 'id' => "div-$name"]);
|
||||||
|
|
||||||
|
$default_value = json_encode($config['default_value']);
|
||||||
|
|
||||||
|
if ($config['label']) {
|
||||||
|
$html .= HTML::tag('label', [
|
||||||
|
'class' => $config['label_class'],
|
||||||
|
'for' => "input-$name",
|
||||||
|
'id' => "label-$name"
|
||||||
|
], $config['label']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= <<<EOD
|
||||||
|
<div id="{$name}-markdown-input-container"></div>
|
||||||
|
<script>
|
||||||
|
$('#{$name}-markdown-input-container').markdown_input_editor("{$name}", "default", {$default_value});
|
||||||
|
</script>
|
||||||
|
EOD;
|
||||||
|
|
||||||
|
if ($config['help']) {
|
||||||
|
$html .= HTML::tag('div', ['class' => $config['help_class']], $config['help']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= HTML::tag_end('div');
|
||||||
|
|
||||||
|
$this->add($name, $html, $config['validator_php'], $config['validator_js']);
|
||||||
|
}
|
||||||
|
|
||||||
public function printHTML() {
|
public function printHTML() {
|
||||||
echo HTML::tag_begin('form', [
|
echo HTML::tag_begin('form', [
|
||||||
'action' => UOJContext::requestURI(),
|
'action' => UOJContext::requestURI(),
|
||||||
|
156
web/js/uoj.js
156
web/js/uoj.js
@ -42,8 +42,16 @@ uojLocaleData = {
|
|||||||
},
|
},
|
||||||
"editor::upload from local": {
|
"editor::upload from local": {
|
||||||
"en": "Local file",
|
"en": "Local file",
|
||||||
"zh-cn": "本地文件"
|
"zh-cn": "本地文件",
|
||||||
}
|
},
|
||||||
|
"editor::edit": {
|
||||||
|
"en": "Edit",
|
||||||
|
"zh-cn": "编辑",
|
||||||
|
},
|
||||||
|
"editor::preview": {
|
||||||
|
"en": "Preview",
|
||||||
|
"zh-cn": "预览",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function uojLocale(name) {
|
function uojLocale(name) {
|
||||||
@ -873,7 +881,7 @@ $.fn.text_file_form_group = function(name, text) {
|
|||||||
monaco_editor_instance = monaco.editor.create(div_editor[0], {
|
monaco_editor_instance = monaco.editor.create(div_editor[0], {
|
||||||
language: 'text',
|
language: 'text',
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
fontSize: "14px",
|
fontSize: "16px",
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#' + spinner_id).css('display', 'none !important');
|
$('#' + spinner_id).css('display', 'none !important');
|
||||||
@ -1645,6 +1653,148 @@ function custom_test_onsubmit(response_text, div_result, url) {
|
|||||||
setTimeout(update, 500);
|
setTimeout(update, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markdown input editor
|
||||||
|
$.fn.markdown_input_editor = function(name, type, text) {
|
||||||
|
return this.each(function() {
|
||||||
|
var input_editor_name = name;
|
||||||
|
var input_editor_id = 'input-' + name + '_editor';
|
||||||
|
var spinner_id = 'spinner-' + name + '_editor';
|
||||||
|
var div_editor_id = 'div-' + name + '_editor';
|
||||||
|
var div_preview_id = 'div-' + name + '_preview';
|
||||||
|
|
||||||
|
var btn_editor = $('<button class="nav-link" type="button" />').attr('data-bs-target', '#' + div_editor_id + '_edit').attr('data-bs-toggle', 'tab').text(uojLocale('editor::edit')).addClass('active');
|
||||||
|
var btn_preview = $('<button class="nav-link" type="button" />').attr('data-bs-target', '#' + div_editor_id + '_preview').attr('data-bs-toggle', 'tab').text(uojLocale('editor::preview'));
|
||||||
|
|
||||||
|
var div_editor = $('<div id="' + div_editor_id + '" style="height: 350px" />')
|
||||||
|
.append(
|
||||||
|
$('<div id="' + spinner_id + '" class="d-flex justify-content-center align-items-center" style="width: 100%; height: 350px;" />')
|
||||||
|
.append('<div class="spinner-border text-muted" style="width: 3rem; height: 3rem;" />')
|
||||||
|
);
|
||||||
|
var div_preview = $('<div id="' + div_preview_id + '" style="min-height: 350px" />');
|
||||||
|
|
||||||
|
var monaco_editor_instance = null;
|
||||||
|
var monaco_editor_init = function() {
|
||||||
|
require_monaco({ markdown: true }, function() {
|
||||||
|
if (monaco_editor_instance != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(div_editor).empty();
|
||||||
|
|
||||||
|
monaco_editor_instance = monaco.editor.create(div_editor[0], {
|
||||||
|
language: 'markdown-math',
|
||||||
|
automaticLayout: true,
|
||||||
|
fontSize: "16px",
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
wordWrap: 'on',
|
||||||
|
unicodeHighlight: {
|
||||||
|
ambiguousCharacters: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#' + spinner_id).css('display', 'none !important');
|
||||||
|
$(div_editor).addClass('overflow-hidden rounded-bottom').show();
|
||||||
|
|
||||||
|
monaco_editor_instance.getModel().setValue(text);
|
||||||
|
monaco_editor_instance.onDidChangeModelContent(function () {
|
||||||
|
$('#' + input_editor_id).val(monaco_editor_instance.getModel().getValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
require(['MonacoMarkdown'], function(MonacoMarkdown) {
|
||||||
|
var extension = new MonacoMarkdown.MonacoMarkdownExtension();
|
||||||
|
extension.activate(monaco_editor_instance);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this)
|
||||||
|
.append(
|
||||||
|
$('<div class="card" />')
|
||||||
|
.append(
|
||||||
|
$('<div class="card-header" />').append(
|
||||||
|
$('<ul class="nav nav-tabs card-header-tabs">')
|
||||||
|
.append($('<li class="nav-item" />').append(btn_editor))
|
||||||
|
.append($('<li class="nav-item" />').append(btn_preview))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
$('<div class="card-body tab-content p-0" />')
|
||||||
|
.append(
|
||||||
|
$('<div class="tab-pane active" />')
|
||||||
|
.attr('id', div_editor_id + '_edit')
|
||||||
|
.append(div_editor)
|
||||||
|
)
|
||||||
|
.append(
|
||||||
|
$('<div class="tab-pane" />')
|
||||||
|
.attr('id', div_editor_id + '_preview')
|
||||||
|
.append(div_preview)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append($('<input type="hidden" name="' + input_editor_name + '" id="' + input_editor_id + '"/>').val(text));
|
||||||
|
|
||||||
|
bootstrap.Tab.jQueryInterface.call(btn_editor);
|
||||||
|
bootstrap.Tab.jQueryInterface.call(btn_preview).on('shown.bs.tab', function () {
|
||||||
|
$(div_preview)
|
||||||
|
.empty()
|
||||||
|
.append(
|
||||||
|
$('<div class="d-flex justify-content-center align-items-center" style="width: 100%; height: 350px;" />')
|
||||||
|
.append('<div class="spinner-border text-muted" style="width: 3rem; height: 3rem;" />')
|
||||||
|
);
|
||||||
|
|
||||||
|
var render = function () {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/markdown',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'text',
|
||||||
|
data: {
|
||||||
|
markdown: $('#' + input_editor_id).val(),
|
||||||
|
purifier_type: type,
|
||||||
|
parsedown_type: type,
|
||||||
|
},
|
||||||
|
success: function (html) {
|
||||||
|
$(div_preview).empty().append($('<div class="markdown-body p-3" />').html(html)).uoj_highlight();
|
||||||
|
if (window.MathJax) window.MathJax.typeset();
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
var btn_retry = $('<button type="button" class="btn btn-link p-0 alert-link" />').text('retry').click(function() {
|
||||||
|
$(div_preview)
|
||||||
|
.empty()
|
||||||
|
.append(
|
||||||
|
$('<div class="d-flex justify-content-center align-items-center" style="width: 100%; height: 350px;" />')
|
||||||
|
.append('<div class="spinner-border text-muted" style="width: 3rem; height: 3rem;" />')
|
||||||
|
);
|
||||||
|
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(div_preview).empty().append(
|
||||||
|
$('<div class="m-3 alert alert-danger" />')
|
||||||
|
.append('<span>Render failed, </span>')
|
||||||
|
.append('<span>please </span>')
|
||||||
|
.append(btn_retry)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
var check_monaco_editor_init = function() {
|
||||||
|
if (div_editor.is(':visible')) {
|
||||||
|
monaco_editor_init();
|
||||||
|
} else {
|
||||||
|
setTimeout(check_monaco_editor_init, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check_monaco_editor_init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// hide comment
|
// hide comment
|
||||||
function toggleModalHideComment(id, content) {
|
function toggleModalHideComment(id, content) {
|
||||||
$('#input-comment_hide_id').val(id);
|
$('#input-comment_hide_id').val(id);
|
||||||
|
Loading…
Reference in New Issue
Block a user