網上好像幾乎沒有研究ueditor源碼的文章,緣由多是ueditor源碼太複雜了,接近瀏覽器代碼和word/excel源碼。本文分析ueditor源碼總體流程邏輯以及重點難點細節。php
首先,編輯器是如何實現輸入的?本人開始始終不得其解,在源碼找不到輸入事件綁定的處理函數,後來在白雲峯同窗的提醒下才頓悟,整個iframe網頁就至關因而一個<textarea>元素:css
<body class="view" contenteditable="true" spellcheck="false" style="overflow-y: hidden; height: 500px; cursor: text;">html
</body>前端
頁面調用ueditor:
<script id="editor" type="text/plain" style="width:100%;height:500px;" ></script> // iframe的container元素
var editor = UE.getEditor('editor');node
屢次調用能夠多實例運行,每一個實例都是單獨的,編輯器實例保存在UE實例中,從UE.instants[]也能夠獲取到每一個編輯器實例,0就是第一個實例,以此類推,
所以能夠不用變量引用編輯器實例:
UE.getEditor('editor');
setTimeout(function(){
UE.instants.ueditorInstant0.setContent('<div>歡迎使用編輯器</div>');
},1000);webpack
執行ueditor文件以後產生三個全局對象:
UEDITORUI - 全部工具按鈕插件的api
UE - api入口
UEDITOR_CONFIG - 配置數據web
先看ueditor的全局api接口:ajax
window.UE = baidu.editor = window.UE || {}; // UE實例提供ueditor的入口接口,也就是api入口,調用UE的方法才建立真正的編輯器實例
var Editor = UE.Editor = function (options) { // 這是編輯器構造函數數據庫
/* 嘗試異步加載後臺配置 */
me.loadServerConfig(); //所謂異步加載就是用js構造<tag src=url加載文件,正常是直接在網頁寫<tag src=url 加載文件
UE.Editor.prototype.loadServerConfig = function(){
//用ajax請求http://localhost/plugins/ueditor/ueditor/php/controller.php?action=config&&noCache=1525847581688,返回後臺php配置參數,主要是涉及upload的配置參數,其實前端的配置數據能夠直接寫一個js文件直接從網頁寫<script src=加載。
element.onload = element.onreadystatechange = function () { // 這是構造<tag src=url加載文件以後再經過onload事件觸發執行一個回調
}
}
if(!utils.isEmptyObject(UE.I18N)){ // i18n是語言國際化,就是多語言包
//修改默認的語言類型
me.options.lang = checkCurLang(UE.I18N);
UE.plugin.load(me);
langReadied(me);
}else{
utils.loadFile(document, {
src: me.options.langPath + me.options.lang + "/" + me.options.lang + ".js", //加載zh-cn.js中英文對照語言包
}, function () { // 這個匿名函數回調會執行一次,具體是在哪一次加載文件時執行的不清楚
UE.plugin.load(me); // 這是加載內部插件,執行ueditor文件時會執行UE.plugin.register()註冊全部的插件,而後在這裏加載全部的插件
UE.plugin = function(){
var _plugins = {};
return {
register : function(pluginName,fn,oldOptionName,afterDisabled){
_plugins[pluginName] = {
optionName : oldOptionName || pluginName,
execFn : fn,
//當插件被禁用時執行
afterDisabled : afterDisabled
}
},
load : function(editor){ // 這就是load()函數,加載插件,全部的插件在執行ueditor文件時已經註冊
utils.each(_plugins,function(plugin){ //_plugins是執行register方法產生的插件集合
var _export = plugin.execFn.call(editor); //execFn是plugin的構造函數,執行構造函數產生plugin object {}
utils.each(_export,function(v,k){ // 針對plugin的每個屬性處理一次,把plugin的方法函數保存到編輯器實例中
switch(k.toLowerCase()){
case 'commands':
utils.each(v,function(execFn,execName){
editor.commands[execName] = execFn //editor.commands{}含全部的按鈕操做指令
});
});
utils.each(UE.plugins,function(plugin){ // 插件好像分兩部分有兩種寫法,這是針對舊寫法插件進行處理
plugin.call(editor); //執行插件構造函數
});
langReadied(me);
});
//loadFile代碼:
return function (doc, obj, fn) { // fn就是傳入的匿名函數,用<script src=url加載執行js文件以後再執行這個回調函數
doc.getElementsByTagName("head")[0].appendChild(element); // 構造<script src=加載執行zh-cn.js文件
}
}
UE.instants['ueditorInstant' + me.uid] = me; // 若是多實例運行,均存儲在UE中,每一個實例按id區分,多實例運行能夠利用UE.instants[id]找實例,把每一個編輯器實例保存在本身定義的全局對象中也能夠
}編程
Editor.prototype = {
render: function (container) { // container是iframe holder,以前已經構造iframe相關的幾個div插入網頁,render方法構造iframe代碼而且把iframe插入網頁生效
var me = this,
options = me.options, // options是實例裏面的參數,包含config參數
var html = 'iframe裏面的html代碼';
container.appendChild(domUtils.createElement(document, 'iframe', { // 插入iframe,並執行如下js代碼
editor = window.parent.UE.instants['ueditorInstant0']; // ueditor實例保存在iframe的父窗口也就是當前網頁窗口
editor._setup(document);
}));
},
_setup: function (doc) {
doc.body.contentEditable = true; // iframe html至關於一個input
}
}
先創建一個UE實例放在全局,入口初始化方法是getEditor。
再看編輯器初始化入口:
UE.getEditor = function (id, opt) {
var editor = instances[id];
if (!editor) {
editor = instances[id] = new UE.ui.Editor(opt); // UE是api入口實例,editor是編輯器實例
editor.render(id); //執行新的render,構造幾個/幾層container元素,再執行old render,構造iframe代碼插入網頁
}
return editor;
};
UE.ui.Editor = function (options) {
var editor = new UE.Editor(options); // 這是真正的編輯器實例
var oldRender = editor.render; // UE.editor的render方法(構造iframe插入網頁)
editor.render = function (holder) { // 從新構造一個render,構造幾個容器元素,而後再調old render構造iframe
utils.domReady(function () { //事件觸發異步調度執行
editor.langIsReady ? renderUI() : editor.addListener("langReady", renderUI);
function renderUI() { //事件觸發異步調度執行
new EditorUI(editor.options); // 沒有接收實例,在其它程序位置不能引用這個實例,在實例的方法中用this引用實例,在事件handler中引用實例(實例「複製」到handler方法中),這是建立實例後如何使用實例的高級方法
function EditorUI(options) {
this.initOptions(options);
UIBase.prototype = {
initOptions:function (options) {
//把options複製到EditorUI實例中
}
this.initEditorUI();
EditorUI.prototype = {
initEditorUI:function () {
//用addeventlistener綁定鼠標操做事件和處理函數
}
}
var newDiv = document.createElement('div'); //在這裏建立div插入網頁替換<script>元素,而且複製css代碼
holder.parentNode.insertBefore(newDiv, holder);
newDiv.style.cssText = holder.style.cssText;
holder = newDiv;
holder.innerHTML = 'xxx';
editor.ui.render(holder); //從新構造iframe外層html代碼以及iframe元素代碼,render方法代碼以下:
UIBase.prototype = {
render:function (holder) {
}
opt.initialFrameHeight = opt.minFrameHeight = holder.offset; //是在這裏設置跟隨頁面寫的height
oldRender.call(editor, editor.ui.getDom('iframeholder')); //再執行oldrender,構造iframe插入網頁
editor.fireEvent("afteruiready"); // 沒有on這個事件的,有何用?
});
};
return editor; //若是沒有返回語句,產生的實例是new UE.ui.Editor(opt)實例,因爲有返回語句,產生的實例是返回的實例new UE.Editor(options)實例
}
執行ueditor文件時註冊全部的插件,執行UE.getEditor()產生new UE.Editor(options)編輯器實例,初始化編輯器,加載插件,綁定鼠標操做事件handler,構造div和iframe。
ueditor的功能組件以plugin插件形式設計,插件代碼是ueditor最主要的功能代碼,其功能和複雜程度相似word/excel。
ueditor就是一個textarea框,輸入文字內容,自動產生html元素,插入圖片產生img元素,最終產生的數據是html代碼串,提交到後臺保存到數據庫。
正常顯示時是顯示iframe裏面的html網頁,預覽html代碼時,是顯示一個與iframe平級的div,隱藏iframe元素節點不顯示:
<div class="CodeMirror-scroll cm-s-default" tabindex="-1" style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;">
插入html代碼時,先構造一個div,而後用div.innerHTML=插入的html字符串 解析html字符串成爲DOM元素對象,再把div.firstChild插入到網頁中一個<p>元素裏面
生效,好比插入<div>hello</div>,顯示hello,不會把<div>顯示出來。
再看結束編輯獲取編輯器內容的代碼:
getContent: function (cmd, fn,notSetCursor,ignoreBlank,formatter) {
var root = UE.htmlparser(me.body.innerHTML,ignoreBlank); // html -> root -> html 解析處理過程很是複雜
return root.toHtml(formatter);
var htmlparser = UE.htmlparser = function (htmlstr,ignoreBlank) {
var re_tag = /<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g, //html標籤的正則匹配表達式,好比<TD vAlign=top background=../AAA.JPG>
htmlstr = htmlstr.replace(new RegExp('[\\r\\t\\n'+(ignoreBlank?'':' ')+']*<\/?(\\w+)\\s*(?:[^>]*)>[\\r\\t\\n'+(ignoreBlank?'':' ')+']*','g'), function(a,b){ // 正則匹配替換
toHtml:function (formatter) {
var arr = [];
nodeToHtml(this, arr, formatter, 0);
function nodeToHtml(node, arr, formatter, current) {
switch (node.type) {
case 'root':
for (var i = 0, ci; ci = node.children[i++];) {
nodeToHtml(ci, arr, formatter, current) // 遞歸子節點
return arr.join('')
可見獲取編輯器的內容就是獲取iframe網頁的內容body.innerHTML現成的html代碼,很簡單,但解析處理很是複雜,有內置過濾規則處理,有點相似框架的
template/vnode解析處理,要遞歸解析處理全部的子節點。
編輯器頭部是工具按鈕,都是以插件形式實現的,下面以點擊「模板」工具按鈕爲例分析ueditor的工具按鈕插件是如何實現的。
點擊模板(template)按鈕是插入模板,會顯示一個彈窗對話框:
<div id="edui221" class="edui-dialog edui-for-template edui-default edui-state-centered" style="left: 10px; top: 88px; z-index: 110;">
裏面是一個iframe加載一個網頁:
<iframe id="edui221_iframe" class="%%-iframe" height="100%" width="100%" frameborder="0" src="/ueditor/dialogs/template/template.html"></iframe>
這個網頁就是一個列表,選擇以後關閉彈窗,把選擇的內容插入編輯器中。
注意dialog會話彈窗層iframe不在編輯器層iframe裏面,而是在當前網頁裏面,當前網頁有幾個container容器,其中一個放編輯器iframe,一個放dialog iframe,還有編輯器的頭部/底部都是單獨的容器,都不在編輯器容器裏面。凡是跨iframe都有傳遞數據問題,所以dialog彈窗也有傳遞數據問題,後面會分析它如何傳遞數據。
iframe網頁會執行internal.js創建環境:
dialog = parent.$EDITORUI[window.frameElement.id.replace( /_iframe$/, '' )]; // dialog實例是從父網頁獲取的,含父網頁中編輯器實例
editor = dialog.editor; //當前打開dialog的編輯器實例
dialog.onok = function () {
me.execCommand( "template", obj ); //執行template命令, obj.html就是選取的template代碼,me是editor實例
//重寫execCommand命令,用於處理框選時的處理
var oldExecCommand = me.execCommand;
me.execCommand = function (cmd, datatat) {
result = oldExecCommand.apply(me, arguments); // oldExecCommand代碼以下
execCommand: function (cmdName) {
result = this._callCmdFn('execCommand', arguments);
_callCmdFn: function (fnName, args) {
return cmdFn.apply(this, args);
UE.plugins['template'] = function () {
UE.commands['template'] = {
execCommand:function (cmd, obj) { // cmd=template
obj.html && this.execCommand("inserthtml", obj.html);//再次遞歸editor實例的execCommand,但此次是執行inserthtml命令,會執行到如下代碼
UE.commands['inserthtml'] = {
execCommand: function (command,html,notNeedFilter){ // 把選取的template插入編輯器網頁
range = me.selection.getRange(); // 獲取編輯器中dialog會話彈窗以前光標選中的區域,也就是template插入的位置
getRange:function () {
var range = new baidu.editor.dom.Range( me.document ); // 建立range對象,數據結構與js原生selection對象同樣
var sel = me.getNative(); // 調window.getSelection()返回點擊選取的節點數據,此時已經點擊工具按鈕,以前點擊選取的光標狀態已經不在,獲取不到點擊選取數據,單步看獲取的數據是空的,所以要使用以前保存的selection數據。
getNative:function () {
return domUtils.getWindow( doc ).getSelection();
},
if ( this._bakRange && domUtils.inDoc( this._bakRange.startContainer, this.document ) ){
return this._bakRange; //以前在編輯器點擊選取觸發執行getNative保存的selection數據,container=text,offset=1(沒有意義)
}
}
//若是當前位置選中了fillchar要幹掉,要不會產生空行
if(range.inFillChar()){ // 插入第一個子節點第一次執行時range是#text填充符
child = range.startContainer; // 是text節點
if(domUtils.isFillChar(child)){ // 插入第一個子節點第一次執行時start container是#text填充符
range.setStartBefore(child).collapse(true); //設置container=p,offset=0(#text節點在p中的index),collapse(摺疊)意思是設置end=start,若是選中一段再插入就有start container/end container問題。 setStartBefore(child)意思就是要插入到child以前,但要獲取child在父節點中的offset,插入時在父節點中按offset再獲取child,再插入到child以前
domUtils.remove(child); // 刪除#text節點,那麼在p節點內部offset=0是br節點
}else if(domUtils.isFillChar(child,true)){
child.nodeValue = child.nodeValue.replace(fillCharReg,'');
range.startOffset--;
range.collapsed && range.collapse(true)
}
}
while ( child = div.firstChild ) { // 遞歸循環div的子節點把div的子節點一個一個插入(div不插入),若是把div整個插入,就多了div層,其實能夠用frag,把frag整個插入便可,但插入第一個子節點時start container是p節點,插入以後要調整start container=body,從第二個子節點開始都是插入body,因此還不能整個一次插入,挺複雜的。
if(hadBreak){ //第一次執行時hadBreak=0,不執行這段,以後再執行時hadBreak=1,會執行這段,hadBreak表示已經切割container元素,
var p = me.document.createElement('p');
while(child && (child.nodeType == 3 || !dtd.$block[child.tagName])){ // 若是要插入的節點是#text節點則套一層p,爲什麼?
nextNode = child.nextSibling;
p.appendChild(child); //child是引用div子節點,那麼div子節點插入到p就從div移動到p,div中已經沒有child,因此循環n次以後div就變空了
child = nextNode;
} // 若是是文本節點就插入到p元素裏面,若是有一批文本節點就循環所有插入到p元素裏面,再把p作爲child節點
if(p.firstChild){
child = p //
}
} //第n次執行時插入的node是內容節點,不會外套一層p,這段不起做用
range.insertNode( child ); // 第一次插入子節點時把node插入到<p>的<br>以前,以後插入到body裏面<p>以前
insertNode:function (node) { // 編輯器頭尾有​空格文本節點,第一次執行時是插入填充節點,第n次執行時是插入內容節點
var first = node, length = 1;
var start = this.startContainer, //單步看插入第二個子節點時,是<body>,offset是2指向<p>
offset = this.startOffset;
var nextNode = start.childNodes[ offset ]; // <body>​<h1></h1><p><br></p> body[2]=<p>
if (nextNode) {
start.insertBefore(node, nextNode);
} else {
start.appendChild(node);
}
return this.setStartBefore(first); // 第一個子節點插入以後,根據第一個子節點調整插入指針,第一個子節點此時還在p中,
range.startContainer = p
range.startOffset = 0 (node在p中的index)
此時p變爲<p>node<br></p>
nextNode = child.nextSibling; // child插入以後的nextsibling就是原來的佔位節點br
if ( !hadBreak && child.nodeType == domUtils.NODE_ELEMENT && domUtils.isBlockElm( child ) ){ // 第一次循環插入<h1>子節點時hadBreak=0會執行一次
parent = domUtils.findParent( child,function ( node ){ return domUtils.isBlockElm( node ); } ); // 遞歸向上找父節點,parent是p
domUtils.breakParent( child, pre || tmp ); // 把p切開變爲<p></p>node<p><br></p>
//去掉break後前一個多餘的節點 <p>|</p> ==> <p></p><div></div><p>|</p>
var pre = child.previousSibling;
domUtils.trimWhiteTextNode(pre);
if(!pre.childNodes.length){ // 若是node前面的p是空的則刪除,變爲node<p><br></p>
domUtils.remove(pre);
}
next.appendChild(me.document.createElement('br')); // 在p添加一個br
hadBreak = 1; // 切割元素問題只在第一次循環插入子節點時處理一次
}
if(!div.firstChild && next && domUtils.isBlockElm(next)){ // 若是div變空,就是全部子節點都循環處理完了
range.setStart(next,0).collapse(true); // 把start container設置爲插入節點後面的next佔位節點(應該是p),offset=0(p裏面的第一個子節點),
break;
}
range.setEndAfter( child ).collapse(); //關鍵在這,此時第一個子節點已經插入到<p>裏面,<p>已經分裂成node<p><br></p>,所以node.parentNode=body,
container變爲body,offset是node在body中的index=2。雖然是setendcontainer,但updatecollapse會更新startcontainer=endcontainer,因此實際上就是設置startcontainer=body,一旦第一個子節點插入成功,後續再插入時都是插入到以前插入的node的nextSibling節點p,offset是p在body裏面的index,按offset找p節點,把node插入到p以前(insertBefore)。
}
inserthtml命令的函數代碼就是把template插入編輯器網頁,處理流程邏輯很是複雜深奧,涉及到很細小的細節好比空白換行符處理以及很細微的瀏覽器兼容性問題。通過長時間細緻debug看數據研究源代碼,最後發現插入子節點的過程原理以下:
假定回車換行,而後插入模板,把模板插入到當前行位置,這是最簡單的狀況,回車換行時編輯器會自動產生<p><br></p>。
插入div的第一個子節點時是插入到<p>中,更準確地說是插入到p中的offset=0位置以前,也就是填充節點以前。
插入以後,把<p>節點分裂成兩個<p>以下所示:
<p></p>node<p><br></p>
而後把空的<p>節點也刪除,就變成了node<p><br></p>,以後再插入其它子節點時,是插入到<body>中,offset位置是<p>節點在<body>中的index,也就是插入到body中<p>節點以前,每次插入一個node以後,node的nextSibling就是<p>節點佔位元素。
這段代碼是最複雜的插入程序,插入原本很簡單,但編輯器插入很是複雜,由於能夠在編輯器點擊或選取任何位置區域作爲插入位置,那麼就複雜了,選取的區域是否要保留?是插入到選取區域的頭部仍是尾部?若是插入到一個元素的中間,那麼元素要被分割成兩個元素,而有些元素好比<a>元素是不能分割的。所以插入處理流程邏輯以及細節很是複雜,還涉及到#text不可見文本節點,咱們在開發應用時通常不會涉及到這麼細節這麼複雜的問題。
下面是insertHtml程序用到的幾段函數代碼:
根據start container判斷當前選區range內容是否佔位符:
inFillChar : function(){
var start = this.startContainer;
if(this.collapsed && start.nodeType == 3
&& start.nodeValue.replace(new RegExp('^' + domUtils.fillChar),'').length + 1 == start.nodeValue.length
//domUtils.fillChar是空格,這個表達式其實意思是找開頭是否有空格,那麼上述#text文本節點符合這個判斷表達式
){
return true;
}
return false;
判斷給定的節點是不是一個「填充」節點:
isFillChar:function (node,isInStart) {
if(node.nodeType != 3)
return false;
var text = node.nodeValue;
if(isInStart){
return new RegExp('^' + domUtils.fillChar).test(text) // 以空格開頭
}
return !text.replace(new RegExp(domUtils.fillChar,'g'), '').length // 在字符串找全部的空格去掉
對於上述#text文本節點, 其nodevalue是"",去掉空格以後長度爲0,所以判斷爲填充節點,返回true。
這兩個方法都是根據nodetype=3和nodevalue含空格來判斷,有何區別?一個是判斷range,一個是判斷node。
若是佔位元素是填充節點,就插入到填充節點以前,再刪除填充節點,由於若是填充節點是<br>,會換行。
將Range開始位置設置到node節點以前:
setStartBefore:function (node) {
return this.setStart(node.parentNode, domUtils.getNodeIndex(node)); //返回修改以後的range
},
檢測節點node在父節點中的索引位置:
getNodeIndex:function (node, ignoreTextNode) {
var preNode = node,
i = 0;
while (preNode = preNode.previousSibling) {
if (ignoreTextNode && preNode.nodeType == 3) {
if(preNode.nodeType != preNode.nextSibling.nodeType ){
i++;
}
continue;
}
i++;
}
return i;
},
若是選中一段,再插入,又要保留選中的段,就比較複雜,能夠插入到選中段的開頭,也能夠插入到選中段的尾部,也就是說選中的段能夠在插入段的前面或後面。
編輯器range含start container和end container,就是選中的段的頭尾節點。
collapse:function (toStart) {
var me = this;
if (toStart) { //插入到range的頭部
me.endContainer = me.startContainer;
me.endOffset = me.startOffset;
} else { //插入到range的尾部
me.startContainer = me.endContainer;
me.startOffset = me.endOffset;
}
me.collapsed = true;
return me;
設置Range的開始容器節點和偏移量
* @method setStart
* @remind 若是給定的節點是元素節點,那麼offset指的是其子元素中索引爲offset的元素,
* 若是是文本節點,那麼offset指的是其文本內容的第offset個字符
* @remind 若是提供的容器節點是一個不能包含子元素的節點, 則該選區的開始容器將被設置
* 爲該節點的父節點, 此時, 其距離開始容器的偏移量也變成了該節點在其父節點
* 中的索引
setStart:function (node, offset) { // node是p,offset是裏面子節點的offset,0是第一個子節點,但也多是node在父元素中的offset
return setEndPoint(true, node, offset, this);
},
function setEndPoint(toStart, node, offset, range) {
//若是node是自閉合標籤要處理
if (node.nodeType == 1 && (dtd.$empty[node.tagName] || dtd.$nonChild[node.tagName])) {
offset = domUtils.getNodeIndex(node) + (toStart ? 0 : 1);
node = node.parentNode;
}
if (toStart) {
range.startContainer = node;
range.startOffset = offset;
if (!range.endContainer) {
range.collapse(true);
}
} else {
range.endContainer = node;
range.endOffset = offset;
if (!range.startContainer) {
range.collapse(false);
}
}
updateCollapse(range);
return range;
下面研究點擊對話框「肯定」按鈕以後是如何處理的?如何能執行dialog.onok?
debug看dialog iframe網頁代碼中「肯定」按鈕代碼是:
<div id="edui223_body" unselectable="on" class="edui-button-body edui-default" onmousedown="return $EDITORUI["edui223"]._onMouseDown(event, this);" onclick="return $EDITORUI["edui223"]._onClick(event, this);">
<div class="edui-box edui-icon edui-default"></div>
<div class="edui-box edui-label edui-default">確認</div>
</div>
點擊「肯定」按鈕是執行$EDITORUI["edui223"]._onClick(event, this),這段代碼是如何產生的?
dialog插件定義代碼:
// ui/dialog.js
(function (){
Dialog = baidu.editor.ui.Dialog = function (options){
this.initOptions(utils.extend({
onok: function (){},
oncancel: function (){},
onclose: function (t, ok){
return ok ? this.onok() : this.oncancel();
},
},options));
this.initDialog();
};
Dialog.prototype = {
initDialog: function (){
},
_hide: function (){
wrapNode.style.display = 'none';
},
open: function (){
this.render();
this.open();
},
close: function (ok){
this._hide();
}
debug看工具按鈕好比模板按鈕是;
<div id="edui225_body" unselectable="on" title="模板" class="edui-button-body edui-default" onmousedown="return $EDITORUI["edui225"]._onMouseDown(event, this);" onclick="return $EDITORUI["edui225"]._onClick(event, this);">
<div class="edui-box edui-icon edui-default"></div>
<div class="edui-box edui-label edui-default"></div>
</div>
按鈕html代碼是由button對象的代碼構造出來的:
Button = baidu.editor.ui.Button = function (options){}
Button.prototype = {
getHtmlTpl: function (){
return '<div id="##" class="edui-box %%">' +
'<div id="##_state" stateful>' +
'<div class="%%-wrap"><div id="##_body" unselectable="on" ' + (this.title ? 'title="' + this.title + '"' : '') +
' class="%%-body" onmousedown="return $$._onMouseDown(event, this);" onclick="return $$._onClick(event, this);">'
加debug看$$就是$EDITORUI['edui225']。
$EDITORUI['edui225']._onClick(event, this)代碼是:
_onClick: function (){
if (!this.isDisabled()) {
this.fireEvent('click');
// ueditor本身的事件系統,對應editor.addListener,觸發事件就是執行listener
var EventBase = UE.EventBase = function () {};
EventBase.prototype = { // ueditor本身的邏輯事件系統
addListener:function (types, listener) {
//把listener存儲到listeners[]中
},
fireEvent:function () { // fireEvent就是到listeners[]中找listener執行
t = listeners[k].apply(this, arguments);
r = t.apply(this, arguments);
}
}
},
插件綁定了click事件;
UE.plugins['template'] = function () {
this.addListener("click", function (type, evt) { //在template操做過程當中並無執行這個handler
var el = evt.target || evt.srcElement,
range = this.selection.getRange();
var tnode = domUtils.findParent(el, function (node) {
if (node.className && domUtils.hasClass(node, "ue_t")) {
return node;
}
}, true);
tnode && range.selectNode(tnode).shrinkBoundary().select();
});
工具欄按鈕點擊事件綁定:
var btnCmds = ['undo', 'redo', 'formatmatch',
'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase',
'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent',
'blockquote', 'pasteplain', 'pagebreak',
'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink',
'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow',
'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts'];
for (var i = 0, ci; ci = btnCmds[i++];) {
editorui[ci] = function (cmd) {
var ui = new editorui.Button({
onclick:function () {
editor.execCommand(cmd);
},
但「模板」按鈕不在其中,有dialog的按鈕是在這兒定義的:
var dialogBtns = {
noOk:['searchreplace', 'help', 'spechars', 'webapp','preview'],
ok:['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap', 'insertframe', 'wordimage',
'insertvideo', 'insertframe', 'edittip', 'edittable', 'edittd', 'scrawl', 'template', 'music', 'background', 'charts']
};
var ui = new editorui.Button({
onclick:function () {
if (dialog) {
switch (cmd) {
default:
dialog.render();
UIBase.prototype = {
render:function (holder) {
holder.appendChild(el); //構造dialog el插入網頁中佔位元素(一個固定的浮動塊)中
this.postRender();
postRender: function (){
this.addListener('show', function (){
me.modalMask.show(this.getDom().style.zIndex - 2);
});
this.buttons[i].postRender();
postRender: function (){
this.Stateful_postRender();
Stateful_postRender: function (){
if (this.disabled && !this.hasState('disabled')) {
this.addState('disabled');
this.setDisabled(this.disabled)
},
}
}
dialog.open();
open: function (){
this.showAtCenter(); // 執行這個方法顯示會話彈窗
showAtCenter: function (){
//設置定位
this._show();
_show: function (){
//dialog和編輯器兩個平級浮動塊要比z-index,要高過編輯器的zindxe
this.editor.container.style.zIndex && (this.getDom().style.zIndex = this.editor.container.style.zIndex * 1 + 10);
this.fireEvent('show');
baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex = this.getDom().style.zIndex - 4;
}
}
}
}
}
下面來分析一下編輯器初始化代碼,由於在編輯器點擊一下,而後點擊模板按鈕插入模板,是要插入到以前點擊的位置,那麼以前在編輯框內點擊時編輯器如何獲取位置或selection選取區域以及如何保存是個問題。
_setup: function (doc) {
me.selection = new dom.Selection(doc);
this.selection.getNative()
this._initEvents();
_initEvents: function () {
domUtils.on(doc, ['click', 'contextmenu', 'mousedown', 'keydown', 'keyup', 'keypress', 'mouseup', 'mouseover', 'mouseout', 'selectstart'], me._proxyDomEvent);
domUtils.on(win, ['focus', 'blur'], me._proxyDomEvent);
_proxyDomEvent: function (evt) {
this.fireEvent(evt.type.replace(/^on/, ''), evt)
}
domUtils.on(doc, ['mouseup', 'keydown'], function (evt) {
me._selectionChange(250, evt);
_selectionChange: function (delay, evt) {
me.fireEvent('selectionchange', !!evt);
}
}
//編輯器不能爲空內容 if (domUtils.isEmptyNode(me.body)) { me.body.innerHTML = '<p>' + (browser.ie ? '' : '<br/>') + '</p>';
}
//若是要求focus, 就把光標定位到內容開始
if (options.focus) {
setTimeout(function () {
me.focus(me.options.focusInEnd);
//若是自動清除開着,就不須要作selectionchange;
!me.options.autoClearinitialContent && me._selectionChange();
}, 0);
}
從初始化事件代碼看,綁定物理事件清清楚楚,對於mouseup事件,是按selectionchange事件去找handler執行,這個selectionchange事件有幾十個handler都要執行,由於源碼中有幾十個editor.addListener('selectionchange',handler)語句,
就不知道是哪一個handler是處理range的,代碼在哪裏?要在89個函數中查找分析哪一個函數是響應點擊選取處理range的,難度很大,源代碼很是複雜深奧高超。
通過不懈的努力,還好所幸最後終於發現有一個處理mouseup物理事件的handler代碼處理了range:
UE.plugins['table'] = function () {
me.ready(function () {
me.addListener("mouseup", mouseUpEvent);
function mouseUpEvent(type, evt) {
range = new dom.Range(me.document);
range.setStart(target, 0).setCursor(false, true);
me._selectionChange(250, evt);
//變化選區
_selectionChange: function (delay, evt) {
me.selection.cache(); // 獲取選區保存到cache
/**緩存當前選區的range和選區的開始節點
cache:function () {
this.clear(); // 先清除歷史range再保留最近的range
this._cachedRange = this.getRange();
getRange:function () {
var sel = me.getNative();
//因爲已經清除歷史range,此刻cache是空的,無cache數據可用,則執行下面代碼
if ( sel && sel.rangeCount ) { //根據sel數據設置range數據最後返回range數據
var firstRange = sel.getRangeAt( 0 );
var lastRange = sel.getRangeAt( sel.rangeCount - 1 );
range.setStart( firstRange.startContainer, firstRange.startOffset ).setEnd( lastRange.endContainer, lastRange.endOffset );
if ( range.collapsed && domUtils.isBody( range.startContainer ) && !range.startOffset ) {
optimze( range );
}
return this._bakRange = range;
}
所以,在編輯框內點擊或選取時,是執行mouseUpEvent處理range,獲取和保存當前點擊位置或選取的區域,以後再插入模板時要獲取插入位置,就是從cache取這個保存的range數據,插入模板時不可能再調用getRange()獲取點擊位置或選取區域,由於點擊工具按鈕以後,以前在編輯框內的點擊或選取的光標狀態已經改變不存在了。
下面看幾個ueditor源碼中的正則匹配表達式,學習一下正則:
var re_tag = /<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g,
匹配如下html標籤寫法:
</xxx> //好比</br>
<!--xxx-->
<xxx "xxx" />
<xxx 'xxx' />
<xxx xxx />
re_attr = /([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g;
匹配如下html標籤屬性寫法:
xxx:xxx-xxx.xxx = "xxx"
xxx = 'xxx'
xxx = xxx
xxx
是否是暈? ?:能夠忽略,就好看一點了。
源碼中運用正則替換修改字符串的例子:
htmlstr = htmlstr.replace(new RegExp('[\\r\\t\\n'+(ignoreBlank?'':' ')+']*<\/?(\\w+)\\s*(?:[^>]*)>[\\r\\t\\n'+(ignoreBlank?'':' ')+']*','g'), function(a,b){
以 \n <div >xxx</div > \n 爲例,匹配到兩次,第一次匹配 \n <div>,第二次匹配 </div> \n,調用function兩次,a參數就是匹配的串,b參數是匹配的串裏面的子匹配串
(\\w+)文字符串,就是div,replace方法裏面若是寫function,function返回的就是替換內容。
return a.replace(new RegExp('^[\\r\\n'+(ignoreBlank?'':' ')+']+'),'').replace(new RegExp('[\\r\\n'+(ignoreBlank?'':' ')+']+$'),'');
按pattern找到串以後去掉頭尾的換行符返回作爲替換內容,結果就是把源字符串中標籤頭尾的換行符去掉,返回修改以後的字符串。
本文到這裏差很少就結束了,ueditor的工具按鈕都以plugin插件方式定義,機制都同樣,只是功能不一樣,本文再也不一一分析,本文只以插入模板這個工具按鈕爲例進行了重點分析,其它插件應該都是相似的。
經過源代碼分析學習,本人感受ueditor是學習網頁元素處理的頂峯,而前端框架是學習用對象編程技術實現組件機制和語義化表達式解析的頂峯,webuploader則是學習模塊化編程的典範,只不過模塊化編程如今已經被放棄了被webpack取代了。
本人水平有限,文中錯誤之處歡迎你們指正和交流。