Switch from raw textareas to easymde for challenge/hint content (#1496)

* Convert textareas for hint content and challenge content into EasyMDE editors. 
* Works on #1493
This commit is contained in:
Kevin Chung
2020-06-18 18:31:20 -04:00
committed by GitHub
parent 74ff3b57a2
commit 0bd3130fc2
35 changed files with 868 additions and 240 deletions

View File

@@ -4,6 +4,7 @@ from CTFd.admin import admin
from CTFd.models import Pages
from CTFd.schemas.pages import PageSchema
from CTFd.utils import markdown
from CTFd.utils.config.pages import build_html
from CTFd.utils.decorators import admins_only
@@ -26,7 +27,7 @@ def pages_preview():
data = request.form.to_dict()
schema = PageSchema()
page = schema.load(data)
return render_template("page.html", content=markdown(page.data.content))
return render_template("page.html", content=build_html(page.data["content"]))
@admin.route("/admin/pages/<int:page_id>")

View File

@@ -1,12 +1,4 @@
CTFd.plugin.run((_CTFd) => {
const $ = _CTFd.lib.$
const md = _CTFd.lib.markdown()
$('a[href="#new-desc-preview"]').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
md.render(editor_value)
);
}
});
})

View File

@@ -1,5 +1,6 @@
@import "~codemirror/lib/codemirror.css";
.CodeMirror {
@import "includes/easymde.scss";
.CodeMirror.cm-s-default {
font-size: 12px;
border: 1px solid lightgray;
}

View File

@@ -0,0 +1,382 @@
.CodeMirror.cm-s-easymde {
box-sizing: border-box;
height: auto;
border: 1px solid lightgray;
padding: 10px;
font: inherit;
z-index: 0;
word-wrap: break-word;
}
.CodeMirror-scroll {
overflow-y: hidden;
overflow-x: auto;
height: 200px;
}
.editor-toolbar {
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
padding: 0 10px;
border-top: 1px solid #bbb;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
}
.editor-toolbar:after,
.editor-toolbar:before {
display: block;
content: " ";
height: 1px;
}
.editor-toolbar:before {
margin-bottom: 8px;
}
.editor-toolbar:after {
margin-top: 8px;
}
.editor-toolbar.fullscreen {
width: 100%;
height: 50px;
padding-top: 10px;
padding-bottom: 10px;
box-sizing: border-box;
background: #fff;
border: 0;
position: fixed;
top: 0;
left: 0;
opacity: 1;
z-index: 9;
}
.editor-toolbar.fullscreen::before {
width: 20px;
height: 50px;
background: -moz-linear-gradient(
left,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
background: -webkit-gradient(
linear,
left top,
right top,
color-stop(0%, rgba(255, 255, 255, 1)),
color-stop(100%, rgba(255, 255, 255, 0))
);
background: -webkit-linear-gradient(
left,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
background: -o-linear-gradient(
left,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
background: -ms-linear-gradient(
left,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
background: linear-gradient(
to right,
rgba(255, 255, 255, 1) 0%,
rgba(255, 255, 255, 0) 100%
);
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}
.editor-toolbar.fullscreen::after {
width: 20px;
height: 50px;
background: -moz-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
background: -webkit-gradient(
linear,
left top,
right top,
color-stop(0%, rgba(255, 255, 255, 0)),
color-stop(100%, rgba(255, 255, 255, 1))
);
background: -webkit-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
background: -o-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
background: -ms-linear-gradient(
left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 100%
);
position: fixed;
top: 0;
right: 0;
margin: 0;
padding: 0;
}
.editor-toolbar button,
.editor-toolbar .easymde-dropdown {
background: transparent;
display: inline-block;
text-align: center;
text-decoration: none !important;
height: 30px;
margin: 0;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
}
.editor-toolbar button {
width: 30px;
}
.editor-toolbar button.active,
.editor-toolbar button:hover {
background: #fcfcfc;
border-color: #95a5a6;
}
.editor-toolbar i.separator {
display: inline-block;
width: 0;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #fff;
color: transparent;
text-indent: -10px;
margin: 0 6px;
}
.editor-toolbar button:after {
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size: 65%;
vertical-align: text-bottom;
position: relative;
top: 2px;
}
.editor-toolbar button.heading-1:after {
content: "1";
}
.editor-toolbar button.heading-2:after {
content: "2";
}
.editor-toolbar button.heading-3:after {
content: "3";
}
.editor-toolbar button.heading-bigger:after {
content: "";
}
.editor-toolbar button.heading-smaller:after {
content: "";
}
.editor-toolbar.disabled-for-preview button:not(.no-disable) {
opacity: 0.6;
pointer-events: none;
}
@media only screen and (max-width: 700px) {
.editor-toolbar i.no-mobile {
display: none;
}
}
.editor-statusbar {
padding: 8px 10px;
font-size: 12px;
color: #959694;
text-align: right;
}
.editor-statusbar span {
display: inline-block;
min-width: 4em;
margin-left: 1em;
}
.editor-statusbar .lines:before {
content: "lines: ";
}
.editor-statusbar .words:before {
content: "words: ";
}
.editor-statusbar .characters:before {
content: "characters: ";
}
.editor-preview-full {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 7;
overflow: auto;
display: none;
box-sizing: border-box;
}
.editor-preview-side {
position: fixed;
bottom: 0;
width: 50%;
top: 50px;
right: 0;
z-index: 9;
overflow: auto;
display: none;
box-sizing: border-box;
border: 1px solid #ddd;
word-wrap: break-word;
}
.editor-preview-active-side {
display: block;
}
.editor-preview-active {
display: block;
}
.editor-preview {
padding: 10px;
background: #fafafa;
}
.editor-preview > p {
margin-top: 0;
}
.editor-preview pre {
background: #eee;
margin-bottom: 10px;
}
.editor-preview table td,
.editor-preview table th {
border: 1px solid #ddd;
padding: 5px;
}
.cm-s-easymde .cm-tag {
color: #63a35c;
}
.cm-s-easymde .cm-attribute {
color: #795da3;
}
.cm-s-easymde .cm-string {
color: #183691;
}
.cm-s-easymde .cm-header-1 {
font-size: 200%;
line-height: 200%;
}
.cm-s-easymde .cm-header-2 {
font-size: 160%;
line-height: 160%;
}
.cm-s-easymde .cm-header-3 {
font-size: 125%;
line-height: 125%;
}
.cm-s-easymde .cm-header-4 {
font-size: 110%;
line-height: 110%;
}
.cm-s-easymde .cm-comment {
background: rgba(0, 0, 0, 0.05);
border-radius: 2px;
}
.cm-s-easymde .cm-link {
color: #7f8c8d;
}
.cm-s-easymde .cm-url {
color: #aab2b3;
}
.cm-s-easymde .cm-quote {
color: #7f8c8d;
font-style: italic;
}
.editor-toolbar .easymde-dropdown {
position: relative;
background: linear-gradient(
to bottom right,
#fff 0%,
#fff 84%,
#333 50%,
#333 100%
);
border-radius: 0;
border: 1px solid #fff;
}
.editor-toolbar .easymde-dropdown:hover {
background: linear-gradient(
to bottom right,
#fff 0%,
#fff 84%,
#333 50%,
#333 100%
);
}
.easymde-dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
padding: 8px;
z-index: 2;
top: 30px;
}
.easymde-dropdown:active .easymde-dropdown-content,
.easymde-dropdown:focus .easymde-dropdown-content {
display: block;
}

View File

@@ -6,14 +6,13 @@ export function showHintModal(event) {
event.preventDefault();
$("#hint-edit-modal form")
.find("input, textarea")
.val("");
.val("")
// Trigger a change on the textarea to get codemirror to clone changes in
.trigger("change");
// Markdown Preview
$("#new-hint-edit").on("shown.bs.tab", function(event) {
if (event.target.hash == "#hint-preview") {
const renderer = CTFd.lib.markdown();
const editor_value = $("#hint-write textarea").val();
$(event.target.hash).html(renderer.render(editor_value));
$("#hint-edit-form textarea").each(function(i, e) {
if (e.hasOwnProperty("codemirror")) {
e.codemirror.refresh();
}
});
@@ -37,21 +36,33 @@ export function showEditHintModal(event) {
})
.then(function(response) {
if (response.success) {
$("#hint-edit-form input[name=content],textarea[name=content]").val(
response.data.content
);
$("#hint-edit-form input[name=content],textarea[name=content]")
.val(response.data.content)
// Trigger a change on the textarea to get codemirror to clone changes in
.trigger("change");
$("#hint-edit-modal")
.on("shown.bs.modal", function() {
$("#hint-edit-form textarea").each(function(i, e) {
if (e.hasOwnProperty("codemirror")) {
e.codemirror.refresh();
}
});
})
.on("hide.bs.modal", function() {
$("#hint-edit-form textarea").each(function(i, e) {
$(e)
.val("")
.trigger("change");
if (e.hasOwnProperty("codemirror")) {
e.codemirror.refresh();
}
});
});
$("#hint-edit-form input[name=cost]").val(response.data.cost);
$("#hint-edit-form input[name=id]").val(response.data.id);
// Markdown Preview
$("#new-hint-edit").on("shown.bs.tab", function(event) {
if (event.target.hash == "#hint-preview") {
const renderer = CTFd.lib.markdown();
const editor_value = $("#hint-write textarea").val();
$(event.target.hash).html(renderer.render(editor_value));
}
});
$("#hint-edit-modal").modal();
}
});

View File

@@ -9,6 +9,7 @@ import { default as helpers } from "core/helpers";
import { addFile, deleteFile } from "../challenges/files";
import { addTag, deleteTag } from "../challenges/tags";
import { addRequirement, deleteRequirement } from "../challenges/requirements";
import { bindMarkdownEditors } from "../styles";
import {
showHintModal,
editHint,
@@ -133,6 +134,7 @@ function loadChalTemplate(challenge) {
$.getScript(CTFd.config.urlRoot + challenge.scripts.view, function() {
let template_data = challenge.create;
$("#create-chal-entry-div").html(template_data);
bindMarkdownEditors();
$.getScript(CTFd.config.urlRoot + challenge.scripts.create, function() {
$("#create-chal-entry-div form").submit(function(event) {

View File

@@ -1,6 +1,42 @@
import "bootstrap/dist/js/bootstrap.bundle";
import { makeSortableTables } from "core/utils";
import $ from "jquery";
import EasyMDE from "easymde";
export function bindMarkdownEditors() {
$("textarea.markdown").each(function(_i, e) {
if (e.hasOwnProperty("mde") === false) {
let mde = new EasyMDE({
autoDownloadFontAwesome: false,
toolbar: [
"bold",
"italic",
"heading",
"|",
"quote",
"unordered-list",
"ordered-list",
"|",
"link",
"image",
"|",
"preview",
"guide"
],
element: this,
initialValue: $(this).val(),
forceSync: true,
minHeight: "200px"
});
this.mde = mde;
this.codemirror = mde.codemirror;
$(this).on("change keyup paste", function() {
mde.codemirror.getDoc().setValue($(this).val());
mde.codemirror.refresh();
});
}
});
}
export default () => {
// TODO: This is kind of a hack to mimic a React-like state construct.
@@ -96,6 +132,7 @@ export default () => {
}
}
bindMarkdownEditors();
makeSortableTables();
$('[data-toggle="tooltip"]').tooltip();
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,7 @@
<link rel="stylesheet" href="{{ url_for('views.themes', theme='core', path='css/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('views.themes', theme='core', path='css/main.css') }}">
<link rel="stylesheet" href="{{ url_for('views.themes', theme='admin', path='css/admin.css') }}">
<link rel="stylesheet" href="{{ url_for('views.themes', theme='admin', path='css/codemirror.css') }}">
<script type="text/javascript">
var init = {
'urlRoot': "{{ request.script_root }}",

View File

@@ -28,30 +28,14 @@
{% block message %}
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
data-toggle="tab" tabindex="-1">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab" tabindex="-1">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
<div class="form-group">
<label>
Message:<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="new-desc-editor" class="form-control markdown" name="description" rows="10"></textarea>
</div>
{% endblock %}

View File

@@ -30,7 +30,7 @@
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
<textarea id="desc-editor" class="form-control chal-desc-editor markdown" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="{{ url_for('views.themes', theme='admin', path='css/codemirror.css') }}">
{% endblock %}
{% block content %}

View File

@@ -18,29 +18,12 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<ul class="nav nav-tabs" role="tablist" id="new-hint-edit">
<li class="nav-item">
<a class="nav-link active" href="#hint-write" aria-controls="home" role="tab"
data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#hint-preview" aria-controls="home" role="tab"
data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="hint-write">
<div class="form-group">
<label class="text-muted">
Hint<br>
<small>Markdown &amp; HTML are supported</small>
</label>
<textarea type="text" class="form-control" name="content" rows="7"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="hint-preview" style="height:234px; overflow-y: scroll;">
</div>
<div class="form-group">
<label class="text-muted">
Hint<br>
<small>Markdown &amp; HTML are supported</small>
</label>
<textarea type="text" class="form-control markdown" name="content" rows="7"></textarea>
</div>
<div class="form-group">

View File

@@ -32,6 +32,7 @@
"bootstrap-multimodal": "~1.0.4",
"codemirror": "~5.42.2",
"css-loader": "~2.1.0",
"easymde": "^2.10.1",
"echarts": "^4.8.0",
"eslint": "~5.12.0",
"event-source-polyfill": "~1.0.7",

View File

@@ -1489,6 +1489,18 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror-spell-checker@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
integrity sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=
dependencies:
typo-js "*"
codemirror@^5.52.2:
version "5.54.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.54.0.tgz#82b6adf662b29eeb7b867fe7839d49e25e4a0b38"
integrity sha512-Pgf3surv4zvw+KaW3doUU7pGjF0BPU8/sj7eglWJjzni46U/DDW8pu3nZY0QgQKUcICDXRkq8jZmq0y6KhxM3Q==
codemirror@~5.42.2:
version "5.42.2"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.42.2.tgz#801ab715a7a7e1c7ed4162b78e9d8138b98de8f0"
@@ -2103,6 +2115,15 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
easymde@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.10.1.tgz#671836ed08033def1a4379bfa2d193994c46471d"
integrity sha512-khKYfB18ZYbkJAGEeQXaV2ZZ1QS7BavNj2xdcPGpO8GXi30qA9bVxzswGW8QNZQRmPtaXiQDG236Yv/BQY7P2A==
dependencies:
codemirror "^5.52.2"
codemirror-spell-checker "1.1.2"
marked "^0.8.2"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -3755,6 +3776,11 @@ markdown-it@~10.0.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
marked@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355"
integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -6188,6 +6214,11 @@ typeface-raleway@~0.0.54:
resolved "https://registry.yarnpkg.com/typeface-raleway/-/typeface-raleway-0.0.54.tgz#f166c517becc81f34d14e72736d28b8923c79c65"
integrity sha512-oilrvv0fV+z4eYvfTAl6t9Oq1mE/ZrLDPM71TrcAtqyVb0QzAJ6hQBS+FQcYdHxK2LGQE+gloQUPtwgY2I7WxA==
typo-js@*:
version "1.1.0"
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.1.0.tgz#a5a9f592bcb453666bf70c9694da58705d025ed8"
integrity sha512-W3kLbx+ML9PBl5Bzso/lTvVxk4BCveSNAtQeht59FEtxCdGThmn6wSHA4Xq3eQYAK24NHdisMM4JmsK0GFy/pg==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"