UNPKG

s94-editor

Version:

富文本编辑器的基础模块

333 lines (323 loc) 11.5 kB
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, "&lt;").replace(/>/g, "&gt;").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;