* 优雅草 HTML 编辑器 (yeditor)
* 成都市一颗优雅草科技有限公司 · 卓伊凡
* https://gitee.com/youyacao/yeditor
*/
(function (global) {
'use strict';
var defaultOptions = {
placeholder: '在此输入内容…',
minHeight: 200,
maxHeight: 500,
toolbar: [
'fontSize',
'fontColor',
'|',
'bold',
'italic',
'underline',
'strike',
'eraser',
'|',
'alignLeft',
'alignCenter',
'alignRight',
'alignJustify',
'|',
'h2',
'h3',
'|',
'ul',
'ol',
'|',
'link',
'image',
'images',
'|',
'table',
'|',
'code',
'source'
]
};
var FONT_SIZES = [
{ value: '1', label: '12px' },
{ value: '2', label: '14px' },
{ value: '3', label: '16px' },
{ value: '4', label: '18px' },
{ value: '5', label: '20px' },
{ value: '6', label: '24px' },
{ value: '7', label: '32px' }
];
var PRESET_COLORS = [
'#000000', '#333333', '#666666', '#999999', '#cccccc', '#ffffff',
'#e74c3c', '#c0392b', '#e67e22', '#d35400', '#f39c12', '#f1c40f',
'#2ecc71', '#27ae60', '#1abc9c', '#16a085', '#3498db', '#2980b9',
'#9b59b6', '#8e44ad', '#e91e63', '#ad1457'
];
function createButton(icon, title, action) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'yeditor-btn';
btn.title = title;
btn.innerHTML = icon;
btn.setAttribute('aria-label', title);
if (typeof action === 'function') {
btn.addEventListener('click', action);
}
return btn;
}
function createDivider() {
var div = document.createElement('span');
div.className = 'yeditor-divider';
return div;
}
function createSelect(options, title, onChange) {
var wrap = document.createElement('span');
wrap.className = 'yeditor-select-wrap';
var sel = document.createElement('select');
sel.className = 'yeditor-select';
sel.title = title;
options.forEach(function (opt) {
var o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.label;
sel.appendChild(o);
});
if (typeof onChange === 'function') {
sel.addEventListener('change', function () { onChange(sel.value); });
}
wrap.appendChild(sel);
return wrap;
}
function createColorPicker(title, onChange) {
var wrap = document.createElement('span');
wrap.className = 'yeditor-color-wrap';
wrap.title = title;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'yeditor-btn yeditor-color-btn';
btn.innerHTML = 'A';
btn.style.color = '#333';
var picker = document.createElement('input');
picker.type = 'color';
picker.className = 'yeditor-color-input';
picker.value = '#333333';
picker.addEventListener('input', function () {
btn.style.color = picker.value;
if (typeof onChange === 'function') onChange(picker.value);
});
wrap.appendChild(btn);
wrap.appendChild(picker);
wrap.addEventListener('click', function (e) {
if (e.target === btn || e.target === wrap) picker.click();
});
return wrap;
}
var icons = {
bold: '<b>B</b>',
italic: '<i>I</i>',
underline: '<u>U</u>',
strike: '<s>S</s>',
eraser: '⌫',
h2: 'H2',
h3: 'H3',
ul: '<span class="ye-icon-list-ul">• • •</span>',
ol: '<span class="ye-icon-list-ol">1. 2. 3.</span>',
link: '🔗',
image: '<span class="yeditor-icon-image" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg></span>',
images: '<span class="yeditor-icon-images" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/><path d="M19 15v-2h-2v2h2zm0 4v-2h-2v2h2zm-4-4v-2h-2v2h2z" opacity=".7"/></svg></span>',
code: '</>',
source: '<>',
table: '▦',
alignLeft: '左',
alignCenter: '中',
alignRight: '右',
alignJustify: '齐'
};
function createImageUploadWrap(self, single) {
var wrap = document.createElement('span');
wrap.className = 'yeditor-image-wrap';
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'yeditor-btn yeditor-btn-image';
btn.title = single ? '插入图片(链接或本地上传)' : '插入多张图片(链接或本地上传)';
btn.innerHTML = single ? icons.image : icons.images;
btn.setAttribute('aria-label', btn.title);
var drop = document.createElement('div');
drop.className = 'yeditor-image-dropdown';
drop.innerHTML = '<button type="button" class="yeditor-image-opt" data-action="url">' + (single ? '输入链接' : '输入多链接') + '</button><button type="button" class="yeditor-image-opt" data-action="upload">' + (single ? '本地上传' : '本地上传多张') + '</button>';
wrap.appendChild(btn);
wrap.appendChild(drop);
btn.addEventListener('click', function (e) {
e.stopPropagation();
var open = drop.classList.contains('ye-open');
document.querySelectorAll('.yeditor-image-dropdown.ye-open').forEach(function (d) { d.classList.remove('ye-open'); });
if (!open) drop.classList.add('ye-open');
});
drop.querySelector('[data-action="url"]').addEventListener('click', function () {
drop.classList.remove('ye-open');
if (single) self.insertImage(); else self.insertImages();
});
drop.querySelector('[data-action="upload"]').addEventListener('click', function () {
drop.classList.remove('ye-open');
self.uploadImage(single);
});
drop.addEventListener('click', function (e) { e.stopPropagation(); });
document.addEventListener('click', function () { drop.classList.remove('ye-open'); });
return wrap;
}
function YEditor(selector, options) {
this.options = Object.assign({}, defaultOptions, options || {});
this.el = typeof selector === 'string' ? document.querySelector(selector) : selector;
if (!this.el) return;
this._build();
}
YEditor.prototype._build = function () {
var self = this;
var opt = this.options;
var wrap = document.createElement('div');
wrap.className = 'yeditor-wrap';
var toolbar = document.createElement('div');
toolbar.className = 'yeditor-toolbar';
var editor = document.createElement('div');
editor.className = 'yeditor-editor';
editor.contentEditable = true;
editor.setAttribute('data-placeholder', opt.placeholder);
editor.style.minHeight = (opt.minHeight || 200) + 'px';
if (opt.maxHeight) editor.style.maxHeight = opt.maxHeight + 'px';
var tools = opt.toolbar || defaultOptions.toolbar;
tools.forEach(function (item) {
if (item === '|') {
toolbar.appendChild(createDivider());
return;
}
switch (item) {
case 'fontSize':
toolbar.appendChild(createSelect(FONT_SIZES, '字体大小', function (val) {
self.exec('fontSize', val);
}));
break;
case 'fontColor':
toolbar.appendChild(createColorPicker('文字颜色', function (val) {
self.exec('foreColor', val);
}));
break;
case 'bold':
toolbar.appendChild(createButton(icons.bold, '粗体', function () { self.exec('bold'); }));
break;
case 'italic':
toolbar.appendChild(createButton(icons.italic, '斜体', function () { self.exec('italic'); }));
break;
case 'underline':
toolbar.appendChild(createButton(icons.underline, '下划线', function () { self.exec('underline'); }));
break;
case 'strike':
toolbar.appendChild(createButton(icons.strike, '删除线', function () { self.exec('strikeThrough'); }));
break;
case 'eraser':
toolbar.appendChild(createButton(icons.eraser, '清除格式', function () {
if (self._sourceMode) return;
document.execCommand('removeFormat', false, null);
document.execCommand('justifyLeft', false, null);
self.editor.focus();
}));
break;
case 'alignLeft':
toolbar.appendChild(createButton('<span class="ye-icon-align-left">' + icons.alignLeft + '</span>', '左对齐', function () { self.exec('justifyLeft'); }));
break;
case 'alignCenter':
toolbar.appendChild(createButton('<span class="ye-icon-align-center">' + icons.alignCenter + '</span>', '居中', function () { self.exec('justifyCenter'); }));
break;
case 'alignRight':
toolbar.appendChild(createButton('<span class="ye-icon-align-right">' + icons.alignRight + '</span>', '右对齐', function () { self.exec('justifyRight'); }));
break;
case 'alignJustify':
toolbar.appendChild(createButton('<span class="ye-icon-align-justify">' + icons.alignJustify + '</span>', '两端对齐', function () { self.exec('justifyFull'); }));
break;
case 'h2':
toolbar.appendChild(createButton(icons.h2, '标题2', function () { self.exec('formatBlock', 'h2'); }));
break;
case 'h3':
toolbar.appendChild(createButton(icons.h3, '标题3', function () { self.exec('formatBlock', 'h3'); }));
break;
case 'ul':
toolbar.appendChild(createButton(icons.ul, '无序列表', function () { self.exec('insertUnorderedList'); }));
break;
case 'ol':
toolbar.appendChild(createButton(icons.ol, '有序列表', function () { self.exec('insertOrderedList'); }));
break;
case 'link':
toolbar.appendChild(createButton(icons.link, '链接', function () { self.insertLink(); }));
break;
case 'image':
toolbar.appendChild(createImageUploadWrap(self, true));
break;
case 'images':
toolbar.appendChild(createImageUploadWrap(self, false));
break;
case 'table':
toolbar.appendChild(createButton(icons.table, '插入表格', function () { self.insertTable(); }));
break;
case 'code':
toolbar.appendChild(createButton(icons.code, '代码', function () { self.exec('formatBlock', 'pre'); }));
break;
case 'source':
var srcBtn = createButton(icons.source, '源码', function () { self.toggleSource(); });
toolbar.appendChild(srcBtn);
self._sourceBtn = srcBtn;
break;
default:
break;
}
});
wrap.appendChild(toolbar);
wrap.appendChild(editor);
var status = document.createElement('div');
status.className = 'yeditor-status';
status.textContent = '优雅草 HTML 编辑器 · yeditor';
wrap.appendChild(status);
this.wrap = wrap;
this.toolbar = toolbar;
this.editor = editor;
this.statusBar = status;
this._sourceMode = false;
this._sourceBtn = null;
this.el.innerHTML = '';
this.el.appendChild(wrap);
};
YEditor.prototype.exec = function (cmd, value) {
if (this._sourceMode) return;
if (cmd === 'fontSize' && value) {
document.execCommand('fontSize', false, value);
this.editor.focus();
return;
}
document.execCommand(cmd, false, value || null);
this.editor.focus();
};
YEditor.prototype.insertLink = function () {
if (this._sourceMode) return;
var url = prompt('请输入链接地址:', 'https://');
if (url) this.exec('createLink', url);
};
YEditor.prototype.insertTable = function () {
if (this._sourceMode) return;
var rows = prompt('请输入表格行数:', '3');
if (!rows) return;
rows = parseInt(rows, 10);
if (!rows || rows <= 0) return;
var cols = prompt('请输入表格列数:', '3');
if (!cols) return;
cols = parseInt(cols, 10);
if (!cols || cols <= 0) return;
if (rows > 50) rows = 50;
if (cols > 20) cols = 20;
var html = '<table border="1" style="border-collapse:collapse;width:100%;">';
for (var r = 0; r < rows; r++) {
html += '<tr>';
for (var c = 0; c < cols; c++) {
html += '<td> </td>';
}
html += '</tr>';
}
html += '</table>';
this.exec('insertHTML', html);
};
YEditor.prototype.insertImage = function () {
if (this._sourceMode) return;
var url = prompt('请输入图片地址(单张):', 'https://');
if (url && url.trim()) {
url = url.trim();
this.exec('insertImage', url);
}
};
YEditor.prototype.uploadImage = function (single) {
if (this._sourceMode) return;
var self = this;
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = !single;
input.style.display = 'none';
document.body.appendChild(input);
input.addEventListener('change', function () {
var files = Array.prototype.slice.call(input.files || []);
document.body.removeChild(input);
if (files.length === 0) return;
self._insertImagesFromFiles(files);
});
input.click();
};
YEditor.prototype._insertImagesFromFiles = function (files) {
var self = this;
var list = files.filter(function (f) { return f.type && f.type.indexOf('image/') === 0; });
if (list.length === 0) return;
this.editor.focus();
if (list.length === 1) {
var reader = new FileReader();
reader.onload = function () { self.exec('insertImage', reader.result); };
reader.readAsDataURL(list[0]);
return;
}
var i = 0;
function next() {
if (i >= list.length) return;
var file = list[i++];
var reader = new FileReader();
reader.onload = function () {
self._insertOneImage(reader.result, i >= list.length);
next();
};
reader.onerror = next;
reader.readAsDataURL(file);
}
next();
};
YEditor.prototype._insertOneImage = function (src, isLast) {
var img = document.createElement('img');
img.src = src;
img.alt = '';
img.style.maxWidth = '100%';
img.style.height = 'auto';
var sel = window.getSelection();
var range = sel.rangeCount && this.editor.contains(sel.getRangeAt(0).commonAncestorContainer)
? sel.getRangeAt(0)
: (function () {
var r = document.createRange();
r.selectNodeContents(this.editor);
r.collapse(false);
return r;
}.call(this));
range.deleteContents();
range.insertNode(img);
if (!isLast) {
var br = document.createElement('br');
range.setStartAfter(img);
range.insertNode(br);
range.setStartAfter(br);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
this.editor.focus();
};
YEditor.prototype.insertImages = function () {
if (this._sourceMode) return;
var raw = prompt('请输入多张图片地址,每行一个或使用逗号分隔:', 'https://\nhttps://');
if (!raw || !raw.trim()) return;
var urls = raw.split(/[\n,,]/).map(function (s) { return s.trim(); }).filter(Boolean);
if (urls.length === 0) return;
this.editor.focus();
var frag = document.createDocumentFragment();
urls.forEach(function (url, i) {
var img = document.createElement('img');
img.src = url;
img.alt = '';
img.style.maxWidth = '100%';
img.style.height = 'auto';
frag.appendChild(img);
if (i < urls.length - 1) frag.appendChild(document.createElement('br'));
});
var sel = window.getSelection();
var range;
if (sel.rangeCount && sel.getRangeAt(0).commonAncestorContainer && this.editor.contains(sel.getRangeAt(0).commonAncestorContainer)) {
range = sel.getRangeAt(0);
} else {
range = document.createRange();
range.selectNodeContents(this.editor);
range.collapse(false);
}
range.deleteContents();
range.insertNode(frag);
this.editor.focus();
};
YEditor.prototype.toggleSource = function () {
if (this._sourceMode) {
var raw = this.editor.innerText || '';
this.editor.innerHTML = raw;
this.editor.contentEditable = true;
this.editor.classList.remove('yeditor-source-mode');
if (this._sourceBtn) this._sourceBtn.classList.remove('active');
} else {
this._savedHTML = this.editor.innerHTML;
this.editor.textContent = this._savedHTML;
this.editor.contentEditable = false;
this.editor.classList.add('yeditor-source-mode');
if (this._sourceBtn) this._sourceBtn.classList.add('active');
}
this._sourceMode = !this._sourceMode;
};
YEditor.prototype.getHTML = function () {
if (this._sourceMode) {
var text = this.editor.innerText || '';
return text.replace(/</g, '<').replace(/>/g, '>').replace(/<br>/gi, '\n');
}
return this.editor.innerHTML || '';
};
YEditor.prototype.setHTML = function (html) {
this.editor.innerHTML = html || '';
if (this._sourceMode) this.toggleSource();
};
YEditor.prototype.getText = function () {
return this.editor.innerText || this.editor.textContent || '';
};
YEditor.prototype.focus = function () {
this.editor.focus();
};
YEditor.prototype.destroy = function () {
if (this.wrap && this.wrap.parentNode) {
this.wrap.parentNode.removeChild(this.wrap);
}
};
global.YEditor = YEditor;
})(typeof window !== 'undefined' ? window : this);