Rewrite markdown editor in vanilla Javascript

This commit is contained in:
Frederic Guillot
2016-11-21 21:53:30 -05:00
parent ba900817b6
commit 5188ed8cfe
9 changed files with 262 additions and 180 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,158 +1,140 @@
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;
KB.component('text-editor', function (containerElement, options) {
var textarea, viewModeElement, writeModeElement, previewElement, selectionStart, selectionEnd;
while (true) {
i++;
uniqueId = 'text-editor-textarea-' + i;
this.render = function() {
writeModeElement = buildWriteMode();
viewModeElement = buildViewMode();
if (! document.getElementById(uniqueId)) {
break;
}
containerElement.appendChild(KB.el('div')
.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;
}
},
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);
insertText(lines.join('\n'));
}
}
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);
}
});

View File

@@ -3,4 +3,6 @@ var _KB = null;
jQuery(document).ready(function() {
_KB = new Kanboard.App();
_KB.execute();
KB.render();
});

View File

@@ -1,3 +1,111 @@
'use strict';
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);
};

View File

@@ -160,4 +160,6 @@ Kanboard.Popover.prototype.afterOpen = function() {
new Vue({
el: '#popover-container'
});
KB.render();
};

View File

@@ -1,19 +1,16 @@
@import variables
.text-editor
button
a
font-size: size('normal')
border: none
color: color('light')
background: transparent
text-decoration: none
margin-right: 10px
&: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
padding: 2px