s94-editor
Version:
富文本编辑器的基础模块
333 lines (323 loc) • 11.5 kB
JavaScript
import s94 from "s94-js";
import EditorMenu from './editor-menu.mjs.js';
var Editor = (function(global){
let document = global?.document;
//html代码转dom对象
function html_to_dom(str){
if (!str || typeof str !== 'string') return [];
var istbody = !!str.match(/^<tbody[^>]*>([\s\S.]*)<\/tbody>$/);
var istr = !!str.match(/^<tr[^>]*>([\s\S.]*)<\/tr>$/);
var istd = !!str.match(/^<td[^>]*>([\s\S.]*)<\/td>$/);
var isth = !!str.match(/^<th[^>]*>([\s\S.]*)<\/th>$/);
var hasbody = /<body[\s>]/i.test(str);
var hashead = /<head/i.test(str);
var outer = document.createElement(istd||isth?'tr':(istr?'tbody':(istbody?'table': (hasbody?'html':'body') )));
outer.innerHTML = str;
var nodes = [];
for (var i = (hasbody && !hashead ? 1 : 0); i < outer.childNodes.length; i++) {
nodes.push(outer.childNodes[i]);
}
return nodes;
}
function css(dom, name){
return (dom instanceof Element) ? global.getComputedStyle(dom).getPropertyValue(name) : '';
}
//粘贴html的处理操作
function paste_html(html, style_list){
if (!style_list || typeof style_list !== 'object') style_list = [];
html = html.match(/<\!--StartFragment-->([\s\S.]+)<\!--EndFragment-->/)?.[1];
style_list = Object.values(style_list);
let notes = html_to_dom(html);
s94.eachloop(notes, 'childen', function (row){
var copy_style = {};
style_list.forEach(function(name){
if(row.style[name]) copy_style[name] = row.style[name];
})
row.setAttribute('style', '');
Object.keys(copy_style).forEach(function (name){
row.style[name] = copy_style[name];
})
return true;
})
return notes;
}
//粘贴text的处理操作
function paste_text(text){
var font = document.createElement('font');
font.innerHTML = text.replace(/</g, "<").replace(/>/g, ">").replace(/\n/g,'<br />').replace(/\t/g,'<span style="white-space:pre">\t</span>');
return [font];
}
//默认配置
var CONFIG={
colors: ['#333','rgb(194, 250, 94)','rgb(115, 251, 223)','rgb(251, 236, 131)','rgb(250, 217, 126)','rgb(252, 164, 150)','rgb(253, 146, 200)','rgb(155, 226, 30)','rgb(255, 36, 2)'],
fontsizes: ['12px','13px','16px','18px','24px','32px','48px'],
};
Editor.prototype = {
__event_listener_list: {'change':[], 'range':[]},
on(name, func){
if (typeof func === 'function' && name in this.__event_listener_list){
this.__event_listener_list[name].push(func);
}
},
emit(name, data){
var editor = this;
(this.__event_listener_list[name] || []).forEach(function (func){
func.call(editor, data);
})
},
//向上遍历获取符合条件的节点
belongNode: function(dom, test){
if(!(dom instanceof Node) || dom==this.container) return false;
test = typeof(test)=="function" ? test : function(){return this.nodeType===1}
return test.call(dom) ? dom : this.belongNode(dom.parentNode, test);
},
//设定选区
setRange: function(range){
if(range instanceof Range){
this.range = range;
}else{
range = this.range;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
return this;
},
updateRange: function(range){
if( !(this instanceof Editor))return;
var editor = this;
if(editor.updateRange.ht) clearTimeout(editor.updateRange.ht);
if(range instanceof Range){
editor.range = range;
editor.emit('range', editor.range);
}else{
editor.updateRange.ht = setTimeout(function(){
editor.updateRange.ht=false
var sel = window.getSelection();
if(!sel.rangeCount) return;
var r = sel.getRangeAt(0);
if(!editor.container.contains(r.commonAncestorContainer)) return;
editor.range = r;
editor.emit('range', editor.range);
}, 100);
}
return this;
},
updateValue: function(){
var value = this.html();
var editor = this;
this.textarea.value = value;
editor.emit('change', value);
return this;
},
updateHtml: function(){
this.container.innerHTML = this.encode_tag(this.textarea.value);
return this;
},
//编辑操作
execCommand: function(name, data){
data = data || null;
this.setRange();
switch (name) {
case 'createLink':{
var text='', collapsed=this.range.collapsed;
if(typeof(data)=='object' && data.href){
text = data.text;
data = data.href;
}
document.execCommand(name, false, data);
if(collapsed){
document.execCommand('insertText', false, (text || data));
}else if(text){
this.range.deleteContents();
document.execCommand('insertText', false, text);
}
}break;
case 'foreColor':case 'hiliteColor':case 'bold':case 'strikeThrough':case 'italic':case 'underline':{
var addzws = data && this.range.collapsed;
document.execCommand(name, false, data);
if(addzws) document.execCommand('insertText', false, "\u200B");
}break;
default: document.execCommand(name, false, data);break;
}
this.updateRange();
this.updateValue();
this.lastRow();
return this;
},
alignType: function(){
switch (css(this.belongNode(this.range.commonAncestorContainer), 'text-align')) {
case 'end':case 'right': return 'right';
case 'center': return 'center';
case 'justify': return 'full';
default: return 'left';
}
},
isbold: function(){
return ['600','700','bold','bolder'].indexOf(css(this.belongNode(this.range.commonAncestorContainer),'font-weight')) !== -1;
},
isstrike: function(sr){
return css(this.belongNode(this.range.commonAncestorContainer),'text-decoration').indexOf('line-through') !== -1;
},
isitalic: function(sr){
return css(this.belongNode(this.range.commonAncestorContainer),'font-style').indexOf('italic') !== -1;
},
isunderline: function(sr){
return css(this.belongNode(this.range.commonAncestorContainer),'text-decoration').indexOf('underline') !== -1;
},
//粘贴内容处理
pasteContent: function(data, type){
switch (type){
case 'text/html': return paste_html(data, this.config.pasteStyles || []);
case 'text/plain': return paste_text(data);
}
},
rangePath: function(){
var res=[];
this.belongNode(this.range.commonAncestorContainer, function(){
if(this.nodeType==1) res.unshift(this.nodeName);
return false;
})
return res;
},
lastRow: function(){
var lastNode, len = this.container.children.length;
if(len){
lastNode = this.container.children[len-1];
if(/^<br[\s]*\/?>$/.test(lastNode.innerHTML)) return lastNode;
}
lastNode = document.createElement('p');
lastNode.innerHTML = '<br>';
this.container.appendChild(lastNode);
return lastNode;
},
encode_tag: function(html){
var map = {
'form': 'form-editor-change-tagname',
'script': 'script-editor-change-tagname'
}
return html.replace(new RegExp('<('+Object.keys(map).join('|')+')([^>]*)>','g'), function(){
return '<'+map[arguments[1]]+arguments[2]+'>';
}).replace(new RegExp('</('+Object.keys(map).join('|')+')>','g'), function(){
return '</'+map[arguments[1]]+'>';
})
},
decode_tag: function(html){
var map = {
'form-editor-change-tagname': 'form',
'script-editor-change-tagname': 'script'
}
return html.replace(new RegExp('<('+Object.keys(map).join('|')+')([^>]*)>','g'), function(){
return '<'+map[arguments[1]]+arguments[2]+'>';
}).replace(new RegExp('</('+Object.keys(map).join('|')+')>','g'), function(){
return '</'+map[arguments[1]]+'>';
})
},
html: function(){
var html = this.decode_tag(this.container.innerHTML);
return html.replace(/<p><br[\s]*\/?><\/p>$/,"");
},
}
/**编辑器构建函数
* @param {HTMLTextAreaElement} textarea 编辑容器
* @param {Object} config 配置参数
* @returns {Editor}
* @constructor
*/
function Editor(textarea, config){
if (!document) throw '缺少 document 对象!';
if(!(this instanceof Editor)) return new Editor(textarea, config);
if(!(textarea instanceof HTMLTextAreaElement)) throw new Error( "editor的容器必须为textarea" );
var editor = textarea.editor = this;
//配置
this.config = Object.assign({}, CONFIG, config);
//复制创建容器
this.parent = textarea.parentNode;
this.textarea = textarea;
this.container = document.createElement("div");
var atts = textarea.attributes;
for (var i = 0; i < atts.length; i++) {
this.container.setAttribute(atts[i].name, atts[i].value);
}
this.container.setAttribute("contenteditable", "true");
this.container.style.outline = 'none';
this.parent.insertBefore(this.container, textarea);
//切换为显示容器,隐藏textarea
this.container.style.display = css(textarea, 'display');
textarea.style.display='none';
this.updateHtml();
if(css(this.container, 'overflow')=='visible'){
this.container.style['overflow'] = 'auto';
}
if(css(this.container, 'position')=='static'){
this.container.style['position'] = 'relative';
}
document.execCommand("defaultParagraphSeparator", false, "p");
this.range = document.createRange();
var lastNode = this.lastRow();
this.range.setStart(lastNode, 0);
this.range.setEnd(lastNode, 0);
//编辑容器配置
this.container.addEventListener('click', function (e){
switch (e.target.nodeName) {
case 'IMG':{
editor.range.setStart(e.target, 0);
editor.range.setEnd(e.target, 0);
return editor.updateRange(editor.range);
}
case 'A':{
e.preventDefault();e.stopPropagation();
}break;
}
editor.updateRange();
}, true); //阻止容器里面的默认点击事件,比如a标签
this.container.addEventListener('paste', function (e){
e.preventDefault();
var data;
s94.each(['text/html', 'text/plain'],function(row){
if(data = e.clipboardData.getData(row)){
data = editor.pasteContent(data, row);
return false;
}
})
//config中传入onpaste回调函数自定义出来粘贴内容,接收是Node[]数组,要求返回Node[]数组
if(typeof(editor.config.onpaste)=="function") data = editor.config.onpaste(data);
//内容替换
editor.range.deleteContents();
for (var i = data.length-1; i >= 0; i--) {
editor.range.insertNode(data[i]);
}
editor.updateValue();
editor.updateRange(editor.range);
editor.lastRow();
})
document.addEventListener('touchend', function(e){ editor.updateRange() });
document.addEventListener('mouseup', function(e){ editor.updateRange() });
this.container.addEventListener('keydown', function(e){
switch (e.code) {
case 'Tab':{
e.preventDefault();
editor.execCommand('insertText','\t');
}break;
case 'ArrowUp':case 'ArrowDown':case 'ArrowLeft':case 'ArrowRight':{
editor.updateRange();
}break;
}
})
this.container.addEventListener('input', function(){
editor.updateValue();
editor.updateRange();
});
if(typeof(this.config.Menu)=='function'){
this.menu = new this.config.Menu(this);
}else{
this.menu = new Editor.Menu(this);
}
}
Editor.Menu = EditorMenu;
return Editor;
})(typeof globalThis !== 'undefined' ? globalThis :
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window :
typeof global !== "undefined" ? global : {})
export default Editor;