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
No known key found for this signature in database
GPG Key ID: 92D77191BA7FBC99
22 changed files with 1057 additions and 498 deletions

View File

@ -181,6 +181,44 @@ class FormHelper extends Base
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
*

View File

@ -50,20 +50,7 @@ class TaskHelper extends Base
public function selectDescription(array $values, array $errors)
{
$html = $this->helper->form->label(t('Description'), 'description');
$html .= '<div class="markdown-editor-container">';
$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>';
$html .= $this->helper->form->textEditor('description', $values, $errors, array('tabindex' => 2));
return $html;
}

View File

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

View File

@ -3,21 +3,12 @@
</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">
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $values) ?>
<?= $this->form->hidden('task_id', $values) ?>
<?= $this->form->hidden('user_id', $values) ?>
<div class="markdown-editor-small">
<?= $this->form->textarea(
'comment',
$values,
$errors,
array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'),
'markdown-editor'
) ?>
</div>
<?= $this->form->textEditor('comment', $values, $errors, array('autofocus' => true, 'required' => true)) ?>
<div class="form-actions">
<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('user_id', $values) ?>
<div class="markdown-editor-small">
<?= $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>
<?= $this->form->textEditor('comment', $values, $errors, array('required' => true)) ?>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>

View File

@ -11,7 +11,7 @@
<?= $this->form->csrf() ?>
<?= $this->form->hidden('id', $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">
<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->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?>
<?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions">
<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->label(t('Description'), 'description') ?>
<?= $this->form->textarea('description', $values, $errors, array(), 'markdown-editor') ?>
<?= $this->form->textEditor('description', $values, $errors) ?>
<div class="form-actions">
<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")
});
};

1172
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)
textarea
padding: 3px
border: 1px solid #ccc
width: 400px
max-width: 99%
height: 200px
font-family: sans-serif
font-size: size('normal')
@include placeholder
color: color('lighter')
select
font-size: 1.0em

View File

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

View File

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

View File

@ -16,10 +16,10 @@
"jqueryui-touch-punch": "*",
"jqueryui-timepicker-addon": "^1.6.3",
"mousetrap": "^1.5.3",
"simplemde": "^1.10.1",
"font-awesome": "fontawesome#^4.7.0",
"d3": "~3.5.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/select2/dist/css/select2.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/c3/c3.min.css'
],
@ -48,10 +47,10 @@ var vendor = {
'bower_components/fullcalendar/dist/lang-all.js',
'bower_components/mousetrap/mousetrap.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/c3/c3.min.js',
'bower_components/isMobile/isMobile.min.js'
'bower_components/isMobile/isMobile.min.js',
'bower_components/marked/marked.min.js'
]
};