textarea-editor
Version:
Simple markdown editor for textareas
476 lines (369 loc) • 12.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Formats = undefined;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _escapeStringRegexp = require('escape-string-regexp');
var _escapeStringRegexp2 = _interopRequireDefault(_escapeStringRegexp);
var _formats = require('./formats');
var _formats2 = _interopRequireDefault(_formats);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* TextareaEditor class.
*
* @param {HTMLElement} el - the textarea element to wrap around
*/
var TextareaEditor = function () {
function TextareaEditor(el) {
_classCallCheck(this, TextareaEditor);
this.el = el;
}
/**
* Set or get selection range.
*
* @param {Array} [range]
* @return {Array|TextareaEditor}
*/
_createClass(TextareaEditor, [{
key: 'range',
value: function range(_range) {
var el = this.el;
if (_range == null) {
return [el.selectionStart || 0, el.selectionEnd || 0];
}
this.focus();
var _range2 = _slicedToArray(_range, 2);
el.selectionStart = _range2[0];
el.selectionEnd = _range2[1];
return this;
}
/**
* Insert given text at the current cursor position.
*
* @param {String} text - text to insert
* @return {TextareaEditor}
*/
}, {
key: 'insert',
value: function insert(text) {
var inserted = true;
this.el.contentEditable = true;
this.focus();
try {
document.execCommand('insertText', false, text);
} catch (e) {
inserted = false;
}
this.el.contentEditable = false;
if (inserted) return this;
try {
document.execCommand('ms-beginUndoUnit');
} catch (e) {}
var _selection = this.selection(),
before = _selection.before,
after = _selection.after;
this.el.value = before + text + after;
try {
document.execCommand('ms-endUndoUnit');
} catch (e) {}
var event = document.createEvent('Event');
event.initEvent('input', true, true);
this.el.dispatchEvent(event);
return this;
}
/**
* Set foucs on the TextareaEditor's element.
*
* @return {TextareaEditor}
*/
}, {
key: 'focus',
value: function focus() {
if (document.activeElement !== this.el) this.el.focus();
return this;
}
/**
* Get selected text.
*
* @return {Object}
* @private
*/
}, {
key: 'selection',
value: function selection() {
var _range3 = this.range(),
_range4 = _slicedToArray(_range3, 2),
start = _range4[0],
end = _range4[1];
var value = normalizeNewlines(this.el.value);
return {
before: value.slice(0, start),
content: value.slice(start, end),
after: value.slice(end)
};
}
/**
* Get format by name.
*
* @param {String|Object} format
* @return {Object}
* @private
*/
}, {
key: 'getFormat',
value: function getFormat(format) {
if ((typeof format === 'undefined' ? 'undefined' : _typeof(format)) == 'object') {
return normalizeFormat(format);
}
if (!_formats2.default.hasOwnProperty(format)) {
throw new Error('Invalid format ' + format);
}
return normalizeFormat(_formats2.default[format]);
}
/**
* Toggle given `format` on current selection.
* Any additional arguments are passed on to `.format()`.
*
* @param {String|Object} format - name of format or an object
* @return {TextareaEditor}
*/
}, {
key: 'toggle',
value: function toggle(format) {
if (this.hasFormat(format)) return this.unformat(format);
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return this.format.apply(this, [format].concat(args));
}
/**
* Format current selcetion with given `format`.
*
* @param {String|Object} name - name of format or an object
* @return {TextareaEditor}
*/
}, {
key: 'format',
value: function format(name) {
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
var format = this.getFormat(name);
var prefix = format.prefix,
suffix = format.suffix,
multiline = format.multiline;
var _selection2 = this.selection(),
before = _selection2.before,
content = _selection2.content,
after = _selection2.after;
var lines = multiline ? content.split('\n') : [content];
var _range5 = this.range(),
_range6 = _slicedToArray(_range5, 2),
start = _range6[0],
end = _range6[1];
// format lines
lines = lines.map(function (line, i) {
var pval = maybeCall.apply(undefined, [prefix.value, line, i + 1].concat(args));
var sval = maybeCall.apply(undefined, [suffix.value, line, i + 1].concat(args));
if (!multiline || !content.length) {
start += pval.length;
end += pval.length;
} else {
end += pval.length + sval.length;
}
return pval + line + sval;
});
var insert = lines.join('\n');
// newlines before and after block
if (format.block) {
var nlb = matchLength(before, /\n+$/);
var nla = matchLength(after, /^\n+/);
if (before) {
while (nlb < 2) {
insert = '\n' + insert;
start++;
end++;
nlb++;
}
}
if (after) {
while (nla < 2) {
insert = insert + '\n';
nla++;
}
}
}
this.insert(insert);
this.range([start, end]);
return this;
}
/**
* Remove given `format` from current selection.
*
* @param {String|Object} name - name of format or an object
* @return {TextareaEditor}
*/
}, {
key: 'unformat',
value: function unformat(name) {
if (!this.hasFormat(name)) return this;
var format = this.getFormat(name);
var prefix = format.prefix,
suffix = format.suffix,
multiline = format.multiline;
var _selection3 = this.selection(),
before = _selection3.before,
content = _selection3.content,
after = _selection3.after;
var lines = multiline ? content.split('\n') : [content];
var _range7 = this.range(),
_range8 = _slicedToArray(_range7, 2),
start = _range8[0],
end = _range8[1];
// If this is not a multiline format, include prefixes and suffixes just
// outside the selection.
if ((!multiline || lines.length == 1) && hasSuffix(before, prefix) && hasPrefix(after, suffix)) {
start -= suffixLength(before, prefix);
end += prefixLength(after, suffix);
this.range([start, end]);
lines = [this.selection().content];
}
// remove formatting from lines
lines = lines.map(function (line) {
var plen = prefixLength(line, prefix);
var slen = suffixLength(line, suffix);
return line.slice(plen, line.length - slen);
});
// insert and set selection
var insert = lines.join('\n');
this.insert(insert);
this.range([start, start + insert.length]);
return this;
}
/**
* Check if current seletion has given format.
*
* @param {String|Object} name - name of format or an object
* @return {Boolean}
*/
}, {
key: 'hasFormat',
value: function hasFormat(name) {
var format = this.getFormat(name);
var prefix = format.prefix,
suffix = format.suffix,
multiline = format.multiline;
var _selection4 = this.selection(),
before = _selection4.before,
content = _selection4.content,
after = _selection4.after;
var lines = content.split('\n');
// prefix and suffix outside selection
if (!multiline || lines.length == 1) {
return hasSuffix(before, prefix) && hasPrefix(after, suffix) || hasPrefix(content, prefix) && hasSuffix(content, suffix);
}
// check which line(s) are formatted
var formatted = lines.filter(function (line) {
return hasPrefix(line, prefix) && hasSuffix(line, suffix);
});
return formatted.length === lines.length;
}
}]);
return TextareaEditor;
}();
// Expose formats
exports.default = TextareaEditor;
exports.Formats = _formats2.default;
/**
* Check if given prefix is present.
* @private
*/
function hasPrefix(text, prefix) {
var exp = new RegExp('^' + prefix.pattern);
var result = exp.test(text);
if (prefix.antipattern) {
var _exp = new RegExp('^' + prefix.antipattern);
result = result && !_exp.test(text);
}
return result;
}
/**
* Check if given suffix is present.
* @private
*/
function hasSuffix(text, suffix) {
var exp = new RegExp(suffix.pattern + '$');
var result = exp.test(text);
if (suffix.antipattern) {
var _exp2 = new RegExp(suffix.antipattern + '$');
result = result && !_exp2.test(text);
}
return result;
}
/**
* Get length of match.
* @private
*/
function matchLength(text, exp) {
var match = text.match(exp);
return match ? match[0].length : 0;
}
/**
* Get prefix length.
* @private
*/
function prefixLength(text, prefix) {
var exp = new RegExp('^' + prefix.pattern);
return matchLength(text, exp);
}
/**
* Get suffix length.
* @private
*/
function suffixLength(text, suffix) {
var exp = new RegExp(suffix.pattern + '$');
return matchLength(text, exp);
}
/**
* Normalize newlines.
* @private
*/
function normalizeNewlines(str) {
return str.replace('\r\n', '\n');
}
/**
* Normalize format.
* @private
*/
function normalizeFormat(format) {
var clone = Object.assign({}, format);
clone.prefix = normalizePrefixSuffix(format.prefix);
clone.suffix = normalizePrefixSuffix(format.suffix);
return clone;
}
/**
* Normalize prefixes and suffixes.
* @private
*/
function normalizePrefixSuffix() {
var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == 'object') return value;
return {
value: value,
pattern: (0, _escapeStringRegexp2.default)(value)
};
}
/**
* Call if function.
* @private
*/
function maybeCall(value) {
for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
args[_key3 - 1] = arguments[_key3];
}
return typeof value == 'function' ? value.apply(undefined, args) : value;
}