Replace SimpleMDE with custom Markdown editor

This commit is contained in:
Frederic Guillot
2016-11-13 22:51:59 -05:00
parent 527a1677a0
commit ebb6b2827d
22 changed files with 1057 additions and 498 deletions

View File

@@ -181,6 +181,44 @@ class FormHelper extends Base
return $html; return $html;
} }
/**
* Display a markdown editor
*
* @access public
* @param string $name Field name
* @param array $values Form values
* @param array $errors Form errors
* @param array $attributes
* @return string
*/
public function textEditor($name, $values = array(), array $errors = array(), array $attributes = array())
{
if (! isset($attributes['css'])) {
$attributes['css'] = '';
}
$attrHtml = '';
$attributes['css'] .= $this->errorClass($errors, $name);
foreach ($attributes as $attribute => $value) {
$attrHtml .= sprintf(' %s="%s"', $attribute, $value);
}
$html = sprintf(
'<texteditor name="%s" text="%s" label-preview="%s" label-write="%s" placeholder="%s" %s></texteditor>',
$name,
isset($values[$name]) ? $this->helper->text->e($values[$name]) : '',
t('Preview'),
t('Write'),
t('Write your text in Markdown'),
$attrHtml
);
$html .= $this->errorList($errors, $name);
return $html;
}
/** /**
* Display file field * Display file field
* *

View File

@@ -50,20 +50,7 @@ class TaskHelper extends Base
public function selectDescription(array $values, array $errors) public function selectDescription(array $values, array $errors)
{ {
$html = $this->helper->form->label(t('Description'), 'description'); $html = $this->helper->form->label(t('Description'), 'description');
$html .= '<div class="markdown-editor-container">'; $html .= $this->helper->form->textEditor('description', $values, $errors, array('tabindex' => 2));
$html .= $this->helper->form->textarea(
'description',
$values,
$errors,
array(
'placeholder="'.t('Leave a description').'"',
'tabindex="2"',
'data-mention-search-url="'.$this->helper->url->href('UserAjaxController', 'mention', array('project_id' => $values['project_id'])).'"'
),
'markdown-editor'
);
$html .= '</div>';
return $html; return $html;
} }

View File

@@ -13,7 +13,7 @@
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -16,7 +16,7 @@
<?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?> <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1) ?>
<?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -18,7 +18,7 @@
<?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?> <?= $this->form->checkbox('hide_in_dashboard', t('Hide tasks in this column in the dashboard'), 1, $values['hide_in_dashboard'] == 1) ?>
<?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -6,20 +6,7 @@
<?= $this->form->hidden('task_id', $values) ?> <?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?> <?= $this->form->hidden('user_id', $values) ?>
<div class="markdown-editor-small"> <?= $this->form->textEditor('comment', $values, $errors, array('autofocus' => true, 'required' => true)) ?>
<?= $this->form->textarea(
'comment',
$values,
$errors,
array(
'autofocus',
'required',
'placeholder="'.t('Leave a comment').'"',
'data-mention-search-url="'.$this->url->href('UserAjaxController', 'mention', array('project_id' => $task['project_id'])).'"',
),
'markdown-editor'
) ?>
</div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -3,21 +3,12 @@
</div> </div>
<form class="popover-form" method="post" action="<?= $this->url->href('CommentController', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off"> <form class="popover-form" method="post" action="<?= $this->url->href('CommentController', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off">
<?= $this->form->csrf() ?> <?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('task_id', $values) ?> <?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?> <?= $this->form->hidden('user_id', $values) ?>
<div class="markdown-editor-small"> <?= $this->form->textEditor('comment', $values, $errors, array('autofocus' => true, 'required' => true)) ?>
<?= $this->form->textarea(
'comment',
$values,
$errors,
array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'),
'markdown-editor'
) ?>
</div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -3,20 +3,7 @@
<?= $this->form->hidden('task_id', $values) ?> <?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?> <?= $this->form->hidden('user_id', $values) ?>
<div class="markdown-editor-small"> <?= $this->form->textEditor('comment', $values, $errors, array('required' => true)) ?>
<?= $this->form->textarea(
'comment',
$values,
$errors,
array(
'data-markdown-editor-disable-toolbar="true"',
'required',
'placeholder="'.t('Leave a comment').'"',
'data-mention-search-url="'.$this->url->href('UserAjaxController', 'mention', array('project_id' => $task['project_id'])).'"',
),
'markdown-editor'
) ?>
</div>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -11,7 +11,7 @@
<?= $this->form->csrf() ?> <?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?> <?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('name', $values) ?> <?= $this->form->hidden('name', $values) ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors, array('autofocus' => true)) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -10,7 +10,7 @@
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@@ -13,7 +13,7 @@
<?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
<?= $this->form->label(t('Description'), 'description') ?> <?= $this->form->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?> <?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

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

@@ -0,0 +1,158 @@
Vue.component('texteditor', {
props: ['text', 'name', 'labelPreview', 'labelWrite', 'placeholder', 'css', 'tabindex', 'required', 'autofocus'],
template:
'<div class="text-editor">' +
'<div class="text-editor-toolbar">' +
'<button v-if="!preview" v-on:click.prevent="togglePreview"><i class="fa fa-fw fa-eye"></i>{{ labelPreview }}</button>' +
'<button v-if="preview" v-on:click.prevent="toggleEditor"><i class="fa fa-fw fa-pencil-square-o"></i>{{ labelWrite }}</button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertBoldTag"><i class="fa fa-bold fa-fw"></i></button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertItalicTag"><i class="fa fa-italic fa-fw"></i></button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertStrikethroughTag"><i class="fa fa-strikethrough fa-fw"></i></button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertQuoteTag"><i class="fa fa-quote-right fa-fw"></i></button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertBulletListTag"><i class="fa fa-list-ul fa-fw"></i></button>' +
'<button :disabled="isPreview" v-on:click.prevent="insertCodeTag"><i class="fa fa-code fa-fw"></i></button>' +
'</div>' +
'<div v-show="!preview" class="text-editor-write-area">' +
'<textarea ' +
'v-model="text" ' +
'name="{{ name }}" ' +
'id="{{ getId }}" ' +
'class="{{ css }}" ' +
'tabindex="{{ tabindex }}" ' +
':autofocus="hasAutofocus" ' +
'placeholder="{{ placeholder }}" ' +
'></textarea>' +
'</div>' +
'<div v-show="preview" class="text-editor-preview-area markdown">{{{ renderedText }}}</div>' +
'</div>'
,
data: function() {
return {
id: null,
preview: false,
renderedText: '',
textarea: null,
selectionStart: 0,
selectionEnd: 0
};
},
ready: function() {
this.textarea = document.getElementById(this.id);
},
computed: {
hasAutofocus: function() {
return this.autofocus === '1';
},
isPreview: function() {
return this.preview;
},
getId: function() {
if (! this.id) {
var i = 0;
var uniqueId;
while (true) {
i++;
uniqueId = 'text-editor-textarea-' + i;
if (! document.getElementById(uniqueId)) {
break;
}
}
this.id = uniqueId;
}
return this.id;
}
},
methods: {
toggleEditor: function() {
this.preview = false;
},
togglePreview: function() {
this.preview = true;
this.renderedText = marked(this.text, {sanitize: true});
},
insertBoldTag: function() {
this.insertEnclosedTag('**');
},
insertItalicTag: function() {
this.insertEnclosedTag('_');
},
insertStrikethroughTag: function() {
this.insertEnclosedTag('~~');
},
insertQuoteTag: function() {
this.insertPrependTag('> ');
},
insertBulletListTag: function() {
this.insertPrependTag('* ');
},
insertCodeTag: function() {
this.insertBlockTag('```');
},
replaceTextRange: function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
},
getSelectedText: function() {
return this.text.substring(this.textarea.selectionStart, this.textarea.selectionEnd);
},
insertEnclosedTag: function(tag) {
var selectedText = this.getSelectedText();
this.insertText(tag + selectedText + tag);
this.setCursorBeforeClosingTag(tag);
},
insertPrependTag: function(tag) {
var selectedText = this.getSelectedText();
if (selectedText.indexOf('\n') === -1) {
this.insertText('\n' + tag + selectedText);
} else {
var lines = selectedText.split('\n');
for (var i = 0; i < lines.length; i++) {
if (lines[i].indexOf(tag) === -1) {
lines[i] = tag + lines[i];
}
}
this.insertText(lines.join('\n'));
}
},
insertBlockTag: function(tag) {
var selectedText = this.getSelectedText();
this.insertText('\n' + tag + '\n' + selectedText + '\n' + tag);
this.setCursorBeforeClosingTag(tag, 2);
},
insertText: function(replacedText) {
var result = false;
this.selectionStart = this.textarea.selectionStart;
this.selectionEnd = this.textarea.selectionEnd;
this.textarea.focus();
if (document.queryCommandSupported('insertText')) {
result = document.execCommand('insertText', false, replacedText);
}
if (! result) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
this.textarea.value = this.replaceTextRange(this.text, this.textarea.selectionStart, this.textarea.selectionEnd, replacedText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
},
setCursorBeforeClosingTag: function(tag, offset) {
var position = this.selectionEnd + tag.length + offset;
this.textarea.setSelectionRange(position, position);
}
}
});

View File

@@ -1,59 +0,0 @@
Kanboard.Markdown = function(app) {
this.app = app;
this.editor = null;
};
Kanboard.Markdown.prototype.onPopoverOpened = function() {
this.listen();
};
Kanboard.Markdown.prototype.onPopoverClosed = function() {
this.listen();
};
Kanboard.Markdown.prototype.listen = function() {
var editors = $(".markdown-editor");
if (this.editor) {
this.destroy();
}
if (editors.length > 0) {
this.show(editors[0]);
}
};
Kanboard.Markdown.prototype.destroy = function() {
var cm = this.editor.codemirror;
var wrapper = cm.getWrapperElement();
for (var item in ["toolbar", "statusbar", "sideBySide"]) {
if (this.editor.gui[item]) {
wrapper.parentNode.removeChild(this.editor.gui[item]);
}
}
cm.toTextArea();
this.editor = null;
};
Kanboard.Markdown.prototype.show = function(textarea) {
var toolbar = ["bold", "italic", "strikethrough", "heading", "|", "unordered-list", "ordered-list", "link", "|", "code", "table"];
this.editor = new SimpleMDE({
element: textarea,
status: false,
toolbarTips: false,
autoDownloadFontAwesome: false,
spellChecker: false,
autosave: {
enabled: false
},
forceSync: true,
blockStyles: {
italic: "_"
},
toolbar: textarea.hasAttribute("data-markdown-editor-disable-toolbar") ? false : toolbar,
placeholder: textarea.getAttribute("placeholder")
});
};

1156
assets/js/vendor.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -49,11 +49,15 @@ textarea:focus
box-shadow: 0 0 8px rgba(82, 168, 236, 0.6) box-shadow: 0 0 8px rgba(82, 168, 236, 0.6)
textarea textarea
padding: 3px
border: 1px solid #ccc border: 1px solid #ccc
width: 400px width: 400px
max-width: 99% max-width: 99%
height: 200px height: 200px
font-family: sans-serif font-family: sans-serif
font-size: size('normal')
@include placeholder
color: color('lighter')
select select
font-size: 1.0em font-size: 1.0em

View File

@@ -1,15 +1,19 @@
.markdown-editor-container @import variables
max-width: 400px
div .text-editor
&.CodeMirror, &.CodeMirror-scroll button
max-height: 250px font-size: size('normal')
min-height: 200px border: none
color: color('light')
.markdown-editor-small div background: transparent
&.CodeMirror, &.CodeMirror-scroll &:hover
min-height: 100px color: link-color('primary')
max-height: 180px cursor: pointer
.text-editor-preview-area
.form-column div.CodeMirror border: 1px solid color('lighter')
margin-bottom: 10px width: 400px
height: 200px
overflow: auto
.text-editor-toolbar
button:first-child
padding-left: 0

View File

@@ -31,8 +31,6 @@
@mixin placeholder @mixin placeholder
&::-webkit-input-placeholder &::-webkit-input-placeholder
@content @content
&:-moz-placeholder
@content
&::-moz-placeholder &::-moz-placeholder
@content @content
&:-ms-input-placeholder &:-ms-input-placeholder

View File

@@ -16,10 +16,10 @@
"jqueryui-touch-punch": "*", "jqueryui-touch-punch": "*",
"jqueryui-timepicker-addon": "^1.6.3", "jqueryui-timepicker-addon": "^1.6.3",
"mousetrap": "^1.5.3", "mousetrap": "^1.5.3",
"simplemde": "^1.10.1",
"font-awesome": "fontawesome#^4.7.0", "font-awesome": "fontawesome#^4.7.0",
"d3": "~3.5.0", "d3": "~3.5.0",
"isMobile": "0.4.0", "isMobile": "0.4.0",
"select2": "4.0.2" "select2": "4.0.2",
"marked": "^0.3.6"
} }
} }

View File

@@ -22,7 +22,6 @@ var vendor = {
'bower_components/chosen/chosen.css', 'bower_components/chosen/chosen.css',
'bower_components/select2/dist/css/select2.min.css', 'bower_components/select2/dist/css/select2.min.css',
'bower_components/fullcalendar/dist/fullcalendar.min.css', 'bower_components/fullcalendar/dist/fullcalendar.min.css',
'bower_components/simplemde/dist/simplemde.min.css',
'bower_components/font-awesome/css/font-awesome.min.css', 'bower_components/font-awesome/css/font-awesome.min.css',
'bower_components/c3/c3.min.css' 'bower_components/c3/c3.min.css'
], ],
@@ -48,10 +47,10 @@ var vendor = {
'bower_components/fullcalendar/dist/lang-all.js', 'bower_components/fullcalendar/dist/lang-all.js',
'bower_components/mousetrap/mousetrap.min.js', 'bower_components/mousetrap/mousetrap.min.js',
'bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.min.js', 'bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.min.js',
'bower_components/simplemde/dist/simplemde.min.js',
'bower_components/d3/d3.min.js', 'bower_components/d3/d3.min.js',
'bower_components/c3/c3.min.js', 'bower_components/c3/c3.min.js',
'bower_components/isMobile/isMobile.min.js' 'bower_components/isMobile/isMobile.min.js',
'bower_components/marked/marked.min.js'
] ]
}; };