Rewrite markdown editor in vanilla Javascript
This commit is contained in:
@@ -193,27 +193,18 @@ class FormHelper extends Base
|
|||||||
*/
|
*/
|
||||||
public function textEditor($name, $values = array(), array $errors = array(), array $attributes = array())
|
public function textEditor($name, $values = array(), array $errors = array(), array $attributes = array())
|
||||||
{
|
{
|
||||||
if (! isset($attributes['css'])) {
|
$params = array(
|
||||||
$attributes['css'] = '';
|
'name' => $name,
|
||||||
}
|
'text' => isset($values[$name]) ? $this->helper->text->e($values[$name]) : '',
|
||||||
|
'css' => $this->errorClass($errors, $name),
|
||||||
$attrHtml = '';
|
'required' => isset($attributes['required']) && $attributes['required'],
|
||||||
$attributes['css'] .= $this->errorClass($errors, $name);
|
'tabindex' => isset($attributes['tabindex']) ? $attributes['tabindex'] : '-1',
|
||||||
|
'labelPreview' => t('Preview'),
|
||||||
foreach ($attributes as $attribute => $value) {
|
'labelWrite' => t('Write'),
|
||||||
$attrHtml .= sprintf(' %s="%s"', $attribute, $value);
|
'placeholder' => t('Write your text in Markdown'),
|
||||||
}
|
|
||||||
|
|
||||||
$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 = '<div class="js-text-editor" data-params=\''.json_encode($params, JSON_HEX_APOS).'\'></div>';
|
||||||
$html .= $this->errorList($errors, $name);
|
$html .= $this->errorList($errors, $name);
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
|
|||||||
2
assets/css/app.min.css
vendored
2
assets/css/app.min.css
vendored
File diff suppressed because one or more lines are too long
4
assets/js/app.min.js
vendored
4
assets/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,158 +1,140 @@
|
|||||||
Vue.component('texteditor', {
|
KB.component('text-editor', function (containerElement, options) {
|
||||||
props: ['text', 'name', 'labelPreview', 'labelWrite', 'placeholder', 'css', 'tabindex', 'required', 'autofocus'],
|
var textarea, viewModeElement, writeModeElement, previewElement, selectionStart, selectionEnd;
|
||||||
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) {
|
this.render = function() {
|
||||||
i++;
|
writeModeElement = buildWriteMode();
|
||||||
uniqueId = 'text-editor-textarea-' + i;
|
viewModeElement = buildViewMode();
|
||||||
|
|
||||||
if (! document.getElementById(uniqueId)) {
|
containerElement.appendChild(KB.el('div')
|
||||||
break;
|
.attr('class', 'text-editor')
|
||||||
}
|
.add(viewModeElement)
|
||||||
|
.add(writeModeElement)
|
||||||
|
.build());
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildViewMode() {
|
||||||
|
var toolbarElement = KB.el('div')
|
||||||
|
.attr('class', 'text-editor-toolbar')
|
||||||
|
.for('a', [
|
||||||
|
{href: '#', html: '<i class="fa fa-pencil-square-o fa-fw"></i> ' + options.labelWrite, click: function() { toggleViewMode(); }}
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
previewElement = KB.el('div')
|
||||||
|
.attr('class', 'text-editor-preview-area markdown')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return KB.el('div')
|
||||||
|
.attr('class', 'text-editor-view-mode')
|
||||||
|
.add(toolbarElement)
|
||||||
|
.add(previewElement)
|
||||||
|
.hide()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWriteMode() {
|
||||||
|
var toolbarElement = KB.el('div')
|
||||||
|
.attr('class', 'text-editor-toolbar')
|
||||||
|
.for('a', [
|
||||||
|
{href: '#', html: '<i class="fa fa-eye fa-fw"></i> ' + options.labelPreview, click: function() { toggleViewMode(); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-bold fa-fw"></i>', click: function() { insertEnclosedTag('**'); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-italic fa-fw"></i>', click: function() { insertEnclosedTag('_'); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-strikethrough fa-fw"></i>', click: function() { insertEnclosedTag('~~'); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-quote-right fa-fw"></i>', click: function() { insertPrependTag('> '); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-list-ul fa-fw"></i>', click: function() { insertPrependTag('* '); }},
|
||||||
|
{href: '#', html: '<i class="fa fa-code fa-fw"></i>', click: function() { insertBlockTag('```'); }}
|
||||||
|
])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
textarea = KB.el('textarea')
|
||||||
|
.attr('name', options.name)
|
||||||
|
.attr('tabindex', options.tabindex || '-1')
|
||||||
|
.attr('required', options.required || false)
|
||||||
|
.attr('autofocus', options.autofocus || null)
|
||||||
|
.attr('placeholder', options.placeholder || '')
|
||||||
|
.text(options.text)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return KB.el('div')
|
||||||
|
.attr('class', 'text-editor-write-mode')
|
||||||
|
.add(toolbarElement)
|
||||||
|
.add(textarea)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleViewMode() {
|
||||||
|
KB.el(previewElement).html(marked(textarea.value, {sanitize: true}));
|
||||||
|
KB.el(viewModeElement).toggle();
|
||||||
|
KB.el(writeModeElement).toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedText() {
|
||||||
|
return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceTextRange(s, start, end, substitute) {
|
||||||
|
return s.substring(0, start) + substitute + s.substring(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertEnclosedTag(tag) {
|
||||||
|
var selectedText = getSelectedText();
|
||||||
|
|
||||||
|
insertText(tag + selectedText + tag);
|
||||||
|
setCursorBeforeClosingTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertBlockTag(tag) {
|
||||||
|
var selectedText = getSelectedText();
|
||||||
|
|
||||||
|
insertText('\n' + tag + '\n' + selectedText + '\n' + tag);
|
||||||
|
setCursorBeforeClosingTag(tag, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertPrependTag(tag) {
|
||||||
|
var selectedText = getSelectedText();
|
||||||
|
|
||||||
|
if (selectedText.indexOf('\n') === -1) {
|
||||||
|
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.id = uniqueId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.id;
|
insertText(lines.join('\n'));
|
||||||
}
|
|
||||||
},
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertText(replacedText) {
|
||||||
|
var result = false;
|
||||||
|
|
||||||
|
selectionStart = textarea.selectionStart;
|
||||||
|
selectionEnd = textarea.selectionEnd;
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
if (document.queryCommandSupported('insertText')) {
|
||||||
|
result = document.execCommand('insertText', false, replacedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! result) {
|
||||||
|
try {
|
||||||
|
document.execCommand('ms-beginUndoUnit');
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
textarea.value = replaceTextRange(text, textarea.selectionStart, textarea.selectionEnd, replacedText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('ms-endUndoUnit');
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCursorBeforeClosingTag(tag, offset) {
|
||||||
|
offset = offset || 0;
|
||||||
|
var position = selectionEnd + tag.length + offset;
|
||||||
|
textarea.setSelectionRange(position, position);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ var _KB = null;
|
|||||||
jQuery(document).ready(function() {
|
jQuery(document).ready(function() {
|
||||||
_KB = new Kanboard.App();
|
_KB = new Kanboard.App();
|
||||||
_KB.execute();
|
_KB.execute();
|
||||||
|
|
||||||
|
KB.render();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,111 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var Kanboard = {};
|
var Kanboard = {};
|
||||||
|
|
||||||
|
var KB = {
|
||||||
|
components: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
KB.component = function (name, object) {
|
||||||
|
this.components[name] = object;
|
||||||
|
};
|
||||||
|
|
||||||
|
KB.render = function () {
|
||||||
|
for (var name in this.components) {
|
||||||
|
var elementList = document.querySelectorAll('.js-' + name);
|
||||||
|
|
||||||
|
for (var i = 0; i < elementList.length; i++) {
|
||||||
|
var object = this.components[name];
|
||||||
|
var component = new object(elementList[i], JSON.parse(elementList[i].dataset.params));
|
||||||
|
component.render();
|
||||||
|
elementList[i].className = elementList[i].className + '-rendered';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
KB.el = function (tag) {
|
||||||
|
|
||||||
|
function DOMBuilder(tag) {
|
||||||
|
var element = typeof tag === 'string' ? document.createElement(tag) : tag;
|
||||||
|
|
||||||
|
this.attr = function (attribute, value) {
|
||||||
|
if (value !== null) {
|
||||||
|
element.setAttribute(attribute, value);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hide = function () {
|
||||||
|
element.style.display = 'none';
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.show = function () {
|
||||||
|
element.style.display = 'block';
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggle = function () {
|
||||||
|
if (element.style.display === 'none') {
|
||||||
|
this.show();
|
||||||
|
} else{
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.click = function (callback) {
|
||||||
|
element.onclick = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.add = function (node) {
|
||||||
|
element.appendChild(node);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.html = function (html) {
|
||||||
|
element.innerHTML = html;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.text = function (text) {
|
||||||
|
element.appendChild(document.createTextNode(text));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.for = function (tag, list) {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
var dict = list[i];
|
||||||
|
|
||||||
|
if (typeof dict !== 'object') {
|
||||||
|
element.appendChild(KB.el(tag).text(dict).build());
|
||||||
|
} else {
|
||||||
|
var node = KB.el(tag);
|
||||||
|
|
||||||
|
for (var attribute in dict) {
|
||||||
|
if (attribute in this && typeof this[attribute] === 'function') {
|
||||||
|
node[attribute](dict[attribute]);
|
||||||
|
} else {
|
||||||
|
node.attr(attribute, dict[attribute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.appendChild(node.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.build = function () {
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DOMBuilder(tag);
|
||||||
|
};
|
||||||
|
|||||||
@@ -160,4 +160,6 @@ Kanboard.Popover.prototype.afterOpen = function() {
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: '#popover-container'
|
el: '#popover-container'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
KB.render();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
@import variables
|
@import variables
|
||||||
|
|
||||||
.text-editor
|
.text-editor
|
||||||
button
|
a
|
||||||
font-size: size('normal')
|
font-size: size('normal')
|
||||||
border: none
|
|
||||||
color: color('light')
|
color: color('light')
|
||||||
background: transparent
|
text-decoration: none
|
||||||
|
margin-right: 10px
|
||||||
&:hover
|
&:hover
|
||||||
color: link-color('primary')
|
color: link-color('primary')
|
||||||
cursor: pointer
|
|
||||||
.text-editor-preview-area
|
.text-editor-preview-area
|
||||||
border: 1px solid color('lighter')
|
border: 1px solid color('lighter')
|
||||||
width: 400px
|
width: 400px
|
||||||
height: 200px
|
height: 200px
|
||||||
overflow: auto
|
overflow: auto
|
||||||
.text-editor-toolbar
|
padding: 2px
|
||||||
button:first-child
|
|
||||||
padding-left: 0
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ var strip = require('gulp-strip-comments');
|
|||||||
|
|
||||||
var src = {
|
var src = {
|
||||||
js: [
|
js: [
|
||||||
'assets/js/components/*.js',
|
|
||||||
'assets/js/src/Namespace.js',
|
'assets/js/src/Namespace.js',
|
||||||
'assets/js/src/!(Namespace|Bootstrap|BoardDragAndDrop)*.js',
|
'assets/js/src/!(Namespace|Bootstrap|BoardDragAndDrop)*.js',
|
||||||
'assets/js/src/BoardDragAndDrop.js',
|
'assets/js/src/BoardDragAndDrop.js',
|
||||||
|
'assets/js/components/*.js',
|
||||||
'assets/js/src/Bootstrap.js'
|
'assets/js/src/Bootstrap.js'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user