1. 引言
wangEditor——一款輕量級html富文本編輯器(開源軟體)
- 網站:http://www.wangeditor.com/
- demo示範:http://www.wangeditor.com/wangEditor/demo.html
- 下載下傳(github):https://github.com/wangfupeng1988/wangEditor
- QQ群:164999061
從我釋出wangEditor到現在,大概有七八個月了,随着近期增加的插入視訊,表情,地圖這三個功能,目前為止基本的功能已經大體完善了。這期間也修改了幾個bug,都是各位網友反映的。至于程式是不是已經很穩定了,我不敢說。畢竟應用的人不是特别多,目前隻有幾十個關注wangEditor的人在應用。他們會偶爾提出一些bug,不過隻要告訴我,我會第一時間解決,至少大家對我修改bug增加功能的速度和态度,還是比較認可的。
根據github記載,目前有105個commits,即我已經送出了105次代碼更新,這個數量也會繼續增加。大家有bug,有需求可以通過QQ群向我送出。
2. 介紹源碼結構
wangEditor.js源碼目前2200多行,用書寫文字書寫部落格的方式介紹它的結構,還真不是一件簡單的事兒。是以,這裡我就長話短說,盡量簡單的介紹一下重點,不要搞的太羅嗦,否則大家最後會不耐煩的。
如果讓我自己對這個源碼的設計和架構做一個評價的話,我會打70分。它并不是完美的,但是它已經滿足了我基本的需求。比方說,我最近新增的幾個功能(插入視訊,地圖,表情)都是通過修改其中的配置項增加上去的,而沒有改動源碼中的核心部分。開放封閉原則——對擴充開放,對修改封閉,我想我已經基本做到了這一點。
最後,我分享wangEditor源碼設計的目的,為的是讓大家給一些意見。提出一些疑問,一些建議,或者我目前還沒有意識到的一些問題。總之,我是希望這個軟體越做越好。
3. 一個jQuery插件
wangEditor是一款jQuery插件,也是基于jquery開發的(不了解jquery插件的同學,請自行補課,本文不講)。定義一個jquery插件其實很簡單,wangEditor.js源碼的最後幾十行定義了。
//------------------------------------生成jquery插件------------------------------------
$.fn.extend({
/*
* options: {
* $initContent: $elem, //配置要初始化内容
* menuConfig: [...], //配置要顯示的菜單(menuConfig會覆寫掉hideMenuConfig)
* onchange: function(){...}, //配置onchange事件,
* uploadUrl: 'string' //圖檔上傳的位址
* }
*/
'wangEditor': function(options){
if(this[0].nodeName !== 'TEXTAREA'){
//隻支援textarea
alert('wangEditor提示:請使用textarea擴充富文本框。詳情可參見作者的demo.html');
return;
}
var options = options || {},
menuConfig = options.menuConfig,
$initContent = options.$initContent || $('<p><br/></p>'),
onchange = options.onchange,
uploadUrl = options.uploadUrl;
//擷取editor對象
var editor = $E(this, $initContent, menuConfig, onchange, uploadUrl);
//渲染editor,并隐藏textarea
this.before(editor.$editorContainer);
this.hide();
//頁面剛加載時,初始化selection
editor.initSelection();
return editor;
}
});
以上代碼其實都很簡單,就是接受一些配置項然後調用一個 $E 函數,傳回一個 editor 對象,最後渲染到頁面上。最關鍵的就是 $E 函數這一句話。
//擷取editor對象
var editor = $E(this, $initContent, menuConfig, onchange, uploadUrl);
大家看這種方式是不是有點 var $div = $('div'); 的意思?——對了,這的設計我就是模仿着jquery來的。
4. 仿jQuery的對象化設計
上文中提到的 $E 函數是這樣定義的。
//全局的構造函數
$E = function($textarea, $initContent, menuConfig, onchange, uploadUrl){
return new $E.fn.init($textarea, $initContent, menuConfig, onchange, uploadUrl);
};
如上代碼,其實構造函數是 $E.fn.init 。$E 隻不過是一個入口,傳回這個構造函數 new 出來的一個對象。
那麼 $E.fn 是什麼呢? ——它是 $E.prototype 的簡寫而已——好多js系統都喜歡這麼幹,我也就随着高大上一些啦!
//prototype簡寫為fn
$E.fn = $E.prototype;
既然 $E.fn.init 是構造函數,那麼它 new 出來的對象(即上文中的 editor)的原型要指向:$E.fn.init.prototype ,這樣豈不是太長?不如來個簡單一些的,将原型指向 $E.fn 吧。
$E.fn.init.prototype = $E.fn;
到了這裡,沒有看過jquery設計或者源碼的人,一定覺得繞暈了——那是很正常的。我一開始接觸jquery時,也是繞不過來。不過後來看多了,再後來自己用起來,還真覺得挺簡單易用。大家在做自己的js代碼時候,也不放試一試!
5. 工具函數 & 對象函數
其實這裡也是仿照jquery來設計的。在jquery中,函數都是 $ 的屬性,例如 $.trim() ,對象函數都是 $.fn 的屬性,例如 $('div').html() 的 html 方法就是 $.fn.html 定義的。
在wangEditor.js也一樣。有許多工具函數(例如log輸出,引号轉譯,url安全性檢查等)都是 $E 的屬性;許多對象函數(例如text,append,change等)都是 $E.fn 的屬性。
為什麼把函數定義在 $E.fn 上即可成為對象函數呢?——因為構造函數是 $E.fn.init ,而 $E.fn.init.prototype = $E.fn; 不知道大家明白了沒有?
6. menu配置項
wangEditor目前有28個功能菜單,不可能為每一個菜單都寫一遍執行代碼。因為我們是面向對象的程式設計,我們是遵循“開放封閉原則”的設計。
還别說,在第一個版本中,我還真就是一個菜單寫一遍執行代碼,後來發現那樣根本無法擴充。現在我的宗旨是:寫一個菜單處理引擎(包括菜單初始化,頁面彈出關閉,指令執行),菜單的擴充通過配置項實作。這個菜單處理引擎今天就不在本文講解了,那塊挺麻煩的,有時間再通過視訊的方式跟大家分享吧。
首先,我們需要把所有的菜單歸歸類,否則如何确定配置項啊?我把所有的菜單分為4類:
- command類型:點選按鈕即可執行指令,如“粗體”,“下劃線”
- dropMenu類型:點選按鈕彈出下拉menu,再選擇指令。如“字型”,“字号”
- dropPanel類型:點選按鈕彈出panel,再選擇指令。如“背景色”,“表情”
- modal類型:點選按鈕彈出對話框,需要填寫内容,再執行指令。如“插入圖檔”,“插入地圖位置”
下面是一個菜單按鈕配置時的說明:
'menuId-1': {
'title': (字元串,必須)标題,
'type':(字元串,必須)類型,可以是 btn / dropMenu / dropPanel / modal,
'txt': (字元串,必須)fontAwesome字型樣式,例如 'fa fa-head',
'style': (字元串,可選)設定btn的樣式
'hotKey':(字元串,可選)快捷鍵,如'ctrl + b', 'ctrl,shift + i', 'alt,meta + y'等,支援 ctrl, shift, alt, meta 四個功能鍵(隻有type===btn才有效)
'command':(字元串)document.execCommand的指令名,如'fontName';也可以是自定義的指令名,如“撤銷”、“插入表格”按鈕(type===modal時,command無效),
'dropMenu': ($ul,可選)type===dropMenu時,要傳回一個$ul,作為下拉菜單,
'dropPanel':($div,可選)type===dropPanel是,要傳回一個$div,作為彈出框
'modal':($div,可選)type===modal是,要傳回一個$div,作為彈出框,
'callback':(函數,可選)回調函數,
},
再配置一個菜單時,必須要遵守這個規則,否則解析引擎無法正确解析配置項。在此,為每個類型的菜單按鈕,粘貼幾個簡單的配置項:
'fontFamily': {
'title': '字型',
'type': 'dropMenu',
'txt': 'icon-wangEditor-font',
'command': 'fontName ',
'dropMenu': function(){
var arr = [],
//注意,此處commandValue必填項,否則程式不會跟蹤
temp = '<li><a href="#" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" commandValue="${value}" style="font-family:${family};">${txt}</a></li>',
$ul;
$.each($E.styleConfig.fontFamilyOptions, function(key, value){
arr.push(
temp.replace('${value}', value)
.replace('${family}', value)
.replace('${txt}', value)
);
});
$ul = $( $E.htmlTemplates.dropMenu.replace('{content}', arr.join('')) );
return $ul;
},
'callback': function(editor){
//console.log(editor);
}
},
'bold': {
'title': '加粗',
'type': 'btn',
'hotKey': 'ctrl + b',
'txt':'icon-wangEditor-bold',
'command': 'bold',
'callback': function(editor){
//console.log(editor);
}
},
'foreColor': {
'title': '前景色',
'type': 'dropPanel',
'txt': 'icon-wangEditor-pencil', //如果要顔色: 'txt': 'fa fa-pencil|color:#4a7db1'
'style': 'color:blue;',
'command': 'foreColor',
'dropPanel': function(){
var arr = [],
//注意,此處commandValue必填項,否則程式不會跟蹤
temp = '<a href="#" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" commandValue="${value}" style="background-color:${color};" title="${txt}" class="forColorItem"> </a>',
$panel;
$.each($E.styleConfig.colorOptions, function(key, value){
var floatItem = temp.replace('${value}', key)
.replace('${color}', key)
.replace('${txt}', value);
arr.push(
$E.htmlTemplates.dropPanel_floatItem.replace('{content}', floatItem)
);
});
$panel = $(
$E.htmlTemplates.dropPanel.replace('{content}', arr.join(''))
);
return $panel;
}
},
'createLink': {
'title': '插傳入連結接',
'type': 'modal',
'txt': 'icon-wangEditor-link',
'modal': function (editor) {
var urlTxtId = $E.getUniqeId(),
titleTxtId = $E.getUniqeId(),
blankCheckId = $E.getUniqeId(),
btnId = $E.getUniqeId();
content = '連結:<input id="' + urlTxtId + '" type="text" style="width:300px;"/><br />' +
'标題:<input id="' + titleTxtId + '" type="text" style="width:300px;"/><br />' +
'新視窗:<input id="' + blankCheckId + '" type="checkbox" checked="checked"/><br />' +
'<button id="' + btnId + '" type="button" class="wangEditor-modal-btn">插傳入連結接</button>',
$link_modal = $(
$E.htmlTemplates.modalSmall.replace('{content}', content)
);
$link_modal.find('#' + btnId).click(function(e){
//注意,該方法中的 $link_modal 不要跟其他modal中的變量名重複!!否則程式會混淆
//具體原因還未查證???
var url = $.trim($('#' + urlTxtId).val()),
title = $.trim($('#' + titleTxtId).val()),
isBlank = $('#' + blankCheckId).is(':checked'),
link_callback = function(){
//create link callback
$('#' + urlTxtId).val('');
$('#' + titleTxtId).val('');
};
if(url !== ''){
//xss過濾
if($E.filterXSSForUrl(url) === false){
alert('您的輸入内容有不安全字元,請重新輸入!')
return;
}
if(title === '' && !isBlank){
editor.command(e, 'createLink', url, link_callback);
}else{
editor.command(e, 'customCreateLink', {'url':url, 'title':title, 'isBlank':isBlank}, link_callback);
}
}
});
return $link_modal;
}
}
7. 總結
以上隻是一些重點部分,其他的還有很多。例如富文本編輯器的核心技術:execCommand,如何支援IE6的fontIcon,菜單按鈕如何解析,以及表情,地圖是如何實作的。時間有限,就不一一說明了,大家有興趣可以去看源碼。
最後還是歡迎大家多多指正!
- 網站:http://www.wangeditor.com/
- demo示範:http://www.wangeditor.com/wangEditor/demo.html
- 下載下傳(github):https://github.com/wangfupeng1988/wangEditor
- QQ群:164999061
-------------------------------------------------------------------------------------------------------------
歡迎關注我的教程:《從設計到模式》《深入了解javascript原型和閉包系列》《css知多少》《微軟petshop4.0源碼解讀視訊》《json2.js源碼解讀視訊》
也歡迎關注我的開源項目——wangEditor,輕量化web富文本編輯器
-------------------------------------------------------------------------------------------------------------
轉載于:https://www.cnblogs.com/wangfupeng1988/p/4492962.html