感谢reedseutozte的投稿,发现在Web上进行代码编辑的需求越来越多,也有一些开源的实现。reedseutozte的这篇文章会告诉你如何基于dojo实现自己的代码编辑器。
------------------------------------------------------------------------------------
两年前,本人写了一篇Blog,描述了如何在IE上实现编辑器的功能,http://www.jb51.cc/article/p-eiqgxkkh-bgo.html当时由于产品只要求支持IE浏览器上实现,而且在此过程中。本人一直认为整个文本编辑器的文本应该是一个整体,也就是我操作的核心是文本,而不是编辑器中的DOM结构,所以IE的文本范围非常适合。相反,DOM浏览器(Gecko,Webkit)实现的是Dom范围,按照之前操作文本的思路,几乎不可能在Gecko或者webkit核心的浏览器上实现。
两年后项目要求在DOM浏览器上实现脚本编辑器。一次闲暇的功夫,我又捧起了Nicholas的《JavaScript高级程序》,阅读了关于Dom范围的相关章节(11章第4节)。大师就是大师,对于相关API的描述远胜于W3C网站上枯燥的API描述。基于之前UCD同事代码的思路,我认为要实现脚本编辑器,必须严格屏蔽个排版引擎生成的html差异,让DOM结构完全由自己掌握。我最终定义的DOM结构为每行一个DIV(Opera和Webkit核心浏览器每行就是一个DIV无需作额外的控制,而Gecko核心浏览器是通过<BR>换行的—---这个使我想到了IE有一个怪癖模式,恩,FF就是一个怪胎,呵呵),每个单词一个SPAN,连续的非文本字符除空格外单独一个SPAN,连续的空格或者TAB也在一个SPAN 中,例:
if (sub.SUB_LEVEL == '12' and cust.CUST_SUB.SUB_LEVEL =12 ) 在我的代码中控制其DOM结构为
<div class="linediv"> <span class="keyWord">if</span> <span> </span> <span>(</span> <span>sub.SUB_LEVEL</span> <span> </span> <span>==</span> <span> </span> <span>'12'</span> <span> </span> <span class="keyWord">and</span> <span> </span> <span>cust.CUST_SUB.SUB_LEVEL</span> <span> </span> <span>=</span> <span>12</span> <span> </span> <span>)</span> </div>
对于键盘操作需要作特殊处理 每次操作之前通过如下代码获得光标所在的DOM节点。由于DOM结构发生变化,每次处理完后,利用range设置光标位置。操作者通过肉眼感知不到这些变化,同时用focusNode属性保存当前光标所在的节点(autocomplete特性需要知道关标所在节点,这样做文本替换的时候就知道文本中的前几个字母已经输入了)获得光标所在节点的代码如下
var range = window.getSelection().getRangeAt(0); // DOM下 var node = range.startContainer; //node就是光标所在节点
这里有一个细节,就是keypress事件响应的时候,文本还没有发生变化,所以需要利用setTimeout函数调用该处理函数。
键盘处理分为如下三种情形
1) 空格及字符
几个大分支
a) 如果是首次编辑,编辑器中没有任何子节点,这个时候需要创建一个DIV,并且要插入一个SPAN到这个DIV中
b) 如果在一个空白行中,则需要创建一个SPAN到该DIV中
c) 提供一个通用处理函数,对于所在节点中的文本按照上文中所描述的分割原则分割
2)回车 -----只需要考虑FF的情况 将<BR>替换成div
3)退格及删除
退格,删除的时候要考虑换行删除,单词间空格删除完毕的情况。
数据的收集与设置
脚本数据的收集通过叠代DIV获得每个DIV的textcontent后每行用\n连接而成
设置则是先把\r符号全部现删除(IE中的换行的innerText会返回\r\n)然后用\n分割得到每一行,按照每个单词一个SPAN,连续的空格或者TAB也在一个SPAN 中的原则分割这一行,放到不同SPAN中设置这个DIV的innerHTML即可
完整代码,同两年前的IE版本一样,基于dojo的autocomplete测试页也和IE的一样,只需要将dijit.form.AutoCompleteEditor替换为dijit.form.ScriptPane_DOM即可
dojo.provide("dijit.form.ScriptPane_DOM"); dojo.require("dijit.form.ComboBox"); dojo.declare( "dijit.form.ScriptPane_DOM",[dijit._Widget,dijit._TemplatedMixin],{ // summary: // Implements the base functionality for ComboBox/FilteringSelect // description: // All widgets that mix in dijit.form.ComboBoxMixin must extend dijit.form._FormValueWidget // item: Object // This is the item returned by the dojo.data.store implementation that // provides the data for this coboBox,it's the currently selected item. item: null,// pageSize: Integer // Argument to data provider. // Specifies number of search results per page (before hitting "next" button) pageSize: Infinity,// store: Object // Reference to data provider object used by this ComboBox store: null,// fetchProperties: Object // Mixin to the dojo.data store's fetch. // For example,to set the sort order of the ComboBox menu,pass: // {sort:{attribute:"name",descending:true}} fetchProperties:{},// query: Object // A query that can be passed to 'store' to initially filter the items,// before doing further filtering based on `searchAttr` and the key. // Any reference to the `searchAttr` is ignored. query: {},// autoComplete: Boolean // If you type in a partial string,and then tab out of the `<input>` Box,// automatically copy the first entry displayed in the drop down list to // the `<input>` field autoComplete: false,// highlightMatch: String // One of: "first","all" or "none". // If the ComboBox opens with the serach results and the searched // string can be found it will be highlighted. // This value is not considered when labelType!="text" to not // screw up any mark up the label might contain. highlightMatch: "first",// searchDelay: Integer // Delay in milliseconds between when user types something and we start // searching based on that value searchDelay: 100,// searchAttr: String // Searches pattern match against this field searchAttr: "name",// labelAttr: String // Optional. The text that actually appears in the drop down. // If not specified,the searchAttr text is used instead. labelAttr: "",// labelType: String // "html" or "text" labelType: "text",// queryExpr: String // dojo.data query expression pattern. // `${0}` will be substituted for the user text. // `*` is used for wildcards. // `${0}*` means "starts with",`*${0}*` means "contains",`${0}` means "is" queryExpr: "${0}*",// ignoreCase: Boolean // Set true if the ComboBox should ignore case when matching possible items ignoreCase: true,// hasDownArrow: Boolean // Set this textBox to have a down arrow button. // Defaults to true. hasDownArrow:false,templateString: '<div style="height:100px;font-size=small;border:1px solid #7594bc;width:100%" contentEditable="true" autocomplete="off" dojoAttachEvent="onkeypress:_onKeyPress,onfocus:compositionend,onpaste:_onPaste"\ dojoAttachPoint="textBox,focusNode" waiRole="textBox" waiState="haspopup-true,autocomplete-list" type="text"> </div>',baseClass:"dijitComboBox",keyWords:['and','or','if','else','return','switch','case'],noliterWords:['(',')','=','>','<',decodeURI('%C2%A0'),':','{','}','$'],entityStore: null,subStoreMap: {},leftString: '',_getCaretPos: function(/*DomNode*/ element){ var range = window.getSelection().getRangeAt(0).cloneRange(); range.setStart(element,0); return range.toString().length; },_setCaretPos: function(/*DomNode*/ element,/*Number*/ location){ // location = parseInt(location); // dijit.selectInputText(element,location,location); var selecttion = window.getSelection(); selecttion.removeAllRanges(); var range = document.createRange(); if (element.nodeType == 1) { element = element.childNodes[0] || element; } if (location > element.textContent.length) { range.selectNodeContents(element); range.collapse(false); } else { range.setStart(element,location); range.setEnd(element,location); } selecttion.addRange(range); //this.domNode.focus(); },_setDisabledAttr: function(/*Boolean*/ value){ // summary: // Call this from superclass as part of _setDisabledAttr() method. // Superclass _must_ define _setDisabledAttr(). // description: // Additional code to set disabled state of comboBox node //dijit.setWaiState(this.comboNode,"disabled",value); },_onKeyPress: function(/*Event*/ evt){ // summary: handles keyboard events var key = evt.charOrCode; //except for cutting/pasting case - ctrl + x/v if(evt.altKey || (evt.ctrlKey && (key != 'x' && key != 'v')) || evt.key == dojo.keys.SHIFT){ return; // throw out weird key combinations and spurIoUs events } var doSearch = false,processDom = true; var pw = this._popupWidget; var dk = dojo.keys; if(this._isShowingNow){ pw.handleKey(evt); } switch(key){ case dk.PAGE_DOWN: case dk.DOWN_ARROW: if(!this._isShowingNow||this._prev_key_esc){ this._arrowPressed(); //doSearch=true; }else{ //this._announceOption(pw.getHighlightedOption()); processDom = false; dojo.stopEvent(evt); } this._prev_key_backspace = false; this._prev_key_esc = false; break; case dk.PAGE_UP: case dk.UP_ARROW: if(this._isShowingNow){ //this._announceOption(pw.getHighlightedOption()); processDom = false; dojo.stopEvent(evt); } this._prev_key_backspace = false; this._prev_key_esc = false; break; case dk.ENTER: // prevent submitting form if user presses enter. Also // prevent accepting the value if either Next or PrevIoUs // are selected var highlighted; if(this._isShowingNow && (highlighted = pw.getHighlightedOption()) ){ // only stop event on prev/next if(highlighted == pw.nextButton){ this._nextSearch(1); dojo.stopEvent(evt); break; }else if(highlighted == pw.prevIoUsButton){ this._nextSearch(-1); dojo.stopEvent(evt); break; } else { this._announceOption(pw.getHighlightedOption()); this._hideResultList(); dojo.stopEvent(evt); } processDom = false; }else{ // Update 'value' (ex: KY) according to currently displayed text //this._setDisplayedValueAttr(this.attr('displayedValue'),true); } // default case: // prevent submit,but allow event to bubble //evt.preventDefault(); // fall through this._hideResultList(); break; case dk.TAB: var newvalue = this.attr('displayedValue'); // #4617: // if the user had More Choices selected fall into the // _onBlur handler if(pw && ( newvalue == pw._messages["prevIoUsMessage"] || newvalue == pw._messages["nextMessage"]) ){ break; } if(this._isShowingNow){ this._prev_key_backspace = false; this._prev_key_esc = false; if(pw.getHighlightedOption()){ //pw.attr('value',{ target: pw.getHighlightedOption() }); } this._lastQuery = null; // in case results come back later this._hideResultList(); processDom = false; } break; case ' ': this._prev_key_backspace = false; this._prev_key_esc = false; if(this._isShowingNow && pw.getHighlightedOption()){ dojo.stopEvent(evt); this._announceOption(pw.getHighlightedOption()); this._hideResultList(); processDom = false; }else{ this._hideResultList(); doSearch = true; } this.leftString = ''; break; case dk.ESCAPE: this._prev_key_backspace = false; this._prev_key_esc = true; if(this._isShowingNow){ dojo.stopEvent(evt); this._hideResultList(); processDom = false; }else{ this.inherited(arguments); } break; case dk.DELETE: case dk.BACKSPACE: this._prev_key_esc = false; this._prev_key_backspace = true; if (dojo.trim(this.focusNode.textContent).length > 1) { doSearch = true; } else { this._hideResultList(); } break; case dk.RIGHT_ARROW: // fall through case dk.LEFT_ARROW: this._prev_key_backspace = false; this._prev_key_esc = false; break; case '.': var searchKey = this.focusNode.textContent; if (this.subStoreMap[searchKey]) { this.set('store',this.subStoreMap[searchKey]); doSearch = true; } else { if (searchKey.length > 1) { this.set('store',this.getLazyLoadSubStore(searchKey)); doSearch = true; } } default: // non char keys (F1-F12 etc..) shouldn't open list this._prev_key_backspace = false; this._prev_key_esc = false; doSearch = typeof key == 'string'; } if(this.searchTimer){ clearTimeout(this.searchTimer); } if(doSearch){ // need to wait a tad before start search so that the event // bubbles through DOM and we have value visible setTimeout(dojo.hitch(this,"_startSearchFromInput"),10); } if (processDom) { setTimeout(dojo.hitch(this,"_processDomStruct",key),1); } },getLazyLoadSubStore: function(key) { return this.store; },_autoCompleteText: function(/*String*/ text){ // summary: // Fill in the textBox with the first item from the drop down // list,and highlight the characters that were // auto-completed. For example,if user typed "CA" and the // drop down list appeared,the textBox would be changed to // "California" and "ifornia" would be highlighted. var fn = this.focusNode; if (fn.nodeType == 3 && fn.parentNode.tagName == 'DIV') { fn = dojo.create('SPAN',{},fn,'after'); } // IE7: clear selection so next highlight works all the time //dijit.selectInputText(fn,fn.textContent.length); // does text autoComplete the value in the textBox? var caseFilter = this.ignoreCase? 'toLowerCase' : 'substr'; // if(text[caseFilter](0).indexOf(this.focusNode.textContent[caseFilter](0)) == 0){ // var cpos = this._getCaretPos(fn); // // only try to extend if we added the last character at the end of the input // if((cpos+1) > fn.textContent.length){ // // only add to input node as we would overwrite Capitalisation of chars // // actually,that is ok // fn.textContent = text;//.substr(cpos); // // visually highlight the autocompleted characters // dijit.selectInputText(fn,cpos); // } // } // else { // text does not autoComplete; replace the whole value and highlight if (dojo.trim(this.leftString)) { fn.textContent = this.leftString + text; } else if (this.leftString) { this.focusNode = dojo.create('SPAN',{innerHTML: text},'after'); } else { fn.textContent = text; } this.leftString = ''; this._setCaretPos(fn,fn.textContent.length); } },_openResultList: function(/*Object*/ results,/*Object*/ dataObject){ if( this.disabled || this.readOnly || (dataObject.query[this.searchAttr] != this._lastQuery) ){ return; } this._popupWidget.clearResultList(); if(!results.length){ this._hideResultList(); return; } // Fill in the textBox with the first item from the drop down list,// and highlight the characters that were auto-completed. For // example,if user typed "CA" and the drop down list appeared,the // textBox would be changed to "California" and "ifornia" would be // highlighted. var zerothvalue = new String(this.store.getValue(results[0],this.searchAttr)); if(zerothvalue && this.autoComplete && !this._prev_key_backspace && (dataObject.query[this.searchAttr] != "*")){ // when the user clicks the arrow button to show the full list,// startSearch looks for "*". // it does not make sense to autocomplete // if they are just previewing the options available. this._autoCompleteText(zerothvalue); } dataObject._maxOptions = this._maxOptions; this._popupWidget.createOptions( results,dataObject,dojo.hitch(this,"_getMenuLabelFromItem") ); // show our list (only if we have content,else nothing) this._showResultList(); // #4091: // tell the screen reader that the paging callback finished by // shouting the next choice if(dataObject.direction){ if(1 == dataObject.direction){ this._popupWidget.highlightFirstOption(); }else if(-1 == dataObject.direction){ this._popupWidget.highlightLastOption(); } this._announceOption(this._popupWidget.getHighlightedOption()); } },_showResultList: function(){ this._hideResultList(); var items = this._popupWidget.getItems(),visibleCount = Math.min(items.length,this.maxListLength); this._arrowPressed(); // hide the tooltip // this.displayMessage(""); // Position the list and if it's too big to fit on the screen then // size it to the maximum possible height // Our dear friend IE doesnt take max-height so we need to // calculate that on our own every time // TODO: want to redo this,see // http://trac.dojotoolkit.org/ticket/3272 // and // http://trac.dojotoolkit.org/ticket/4108 // natural size of the list has changed,so erase old // width/height settings,which were hardcoded in a prevIoUs // call to this function (via dojo.marginBox() call) dojo.style(this._popupWidget.domNode,{width: "",height: ""}); var best = this.open(); // #3212: // only set auto scroll bars if necessary prevents issues with // scroll bars appearing when they shouldn't when node is made // wider (fractional pixels cause this) var popupBox = dojo.marginBox(this._popupWidget.domNode); this._popupWidget.domNode.style.overflow = ((best.h==popupBox.h)&&(best.w==popupBox.w)) ? "hidden" : "auto"; // #4134: // borrow TextArea scrollbar test so content isn't covered by // scrollbar and horizontal scrollbar doesn't appear var newwidth = best.w; if(best.h < this._popupWidget.domNode.scrollHeight){ newwidth += 16; } dojo.marginBox(this._popupWidget.domNode,{ h: best.h,w: Math.max(newwidth,this.domNode.offsetWidth) }); if (this.focusNode) { var refNode = this.focusNode; if (this.focusNode.nodeType == 3) { refNode = this.focusNode.parentNode; } var pos = dojo.position(refNode); var left = pos.x + this._getCaretPos(this.focusNode) * 8; var posout = dojo.position(this.domNode,true); if (left > posout.x + posout.w - best.w) { left = posout.x + posout.w - best.w; } dojo.style(this._popupWidget.domNode.parentNode,{position:'absolute',left: left + "px",top: (pos.h + pos.y + 5) + "px"}); dojo.style(this._popupWidget.domNode,height: ""}); } // dijit.setWaiState(this.comboNode,"expanded","true"); },_hideResultList: function(){ if(this._isShowingNow){ dijit.popup.close(this._popupWidget); this._arrowIdle(); this._isShowingNow=false; // dijit.setWaiState(this.comboNode,"false"); // dijit.removeWaiState(this.focusNode,"activedescendant"); } },_setBlurValue: function(){ // if the user clicks away from the textBox OR tabs away,set the // value to the textBox value // #4617: // if value is now more choices or prevIoUs choices,revert // the value var newvalue=this.attr('displayedValue'); var pw = this._popupWidget; if(pw && ( newvalue == pw._messages["prevIoUsMessage"] || newvalue == pw._messages["nextMessage"] ) ){ this._setValueAttr(this._lastValueReported,true); }else{ // Update 'value' (ex: KY) according to currently displayed text this.attr('displayedValue',newvalue); } },_onBlur: function(){ // summary: called magically when focus has shifted away from this widget and it's dropdown this._hideResultList(); this._arrowIdle(); this.inherited(arguments); },_announceOption: function(/*Node*/ node){ // summary: // a11y code that puts the highlighted option in the textBox // This way screen readers will know what is happening in the // menu if(node == null){ return; } // pull the text value from the item attached to the DOM node var newValue; if( node == this._popupWidget.nextButton || node == this._popupWidget.prevIoUsButton){ newValue = node.innerHTML; }else{ newValue = this.store.getValue(node.item,this.searchAttr); } // get the text that the user manually entered (cut off autocompleted text) this.focusNode.textContent = this.focusNode.textContent.substring(0,this._getCaretPos(this.focusNode)); //set up ARIA activedescendant // dijit.setWaiState(this.focusNode,"activedescendant",dojo.attr(node,"id")); // autocomplete the rest of the option to announce change this._autoCompleteText(newValue); this.set('store',this.entityStore); },_selectOption: function(/*Event*/ evt){ var tgt = null; if(!evt){ evt ={ target: this._popupWidget.getHighlightedOption()}; } // what if nothing is highlighted yet? if(!evt.target){ // handle autocompletion where the the user has hit ENTER or TAB this.attr('displayedValue',this.attr('displayedValue')); return; // otherwise the user has accepted the autocompleted value }else{ tgt = evt.target; } if(!evt.noHide){ this._hideResultList(); this._setCaretPos(this.focusNode,this.store.getValue(tgt.item,this.searchAttr).length); } this._doSelect(tgt); },_doSelect: function(tgt){ this.item = tgt.item; this.attr('value',this.searchAttr)); },_onArrowMouseDown: function(evt){ // summary: callback when arrow is clicked if(this.disabled || this.readOnly){ return; } dojo.stopEvent(evt); this.focus(); if(this._isShowingNow){ this._hideResultList(); }else{ // forces full population of results,if they click // on the arrow it means they want to see more options this._startSearch(""); } },_startSearchFromInput: function(){ this._startSearch(this.focusNode.textContent.replace(/([\\\*\?])/g,"\\$1")); },_getQueryString: function(/*String*/ text){ return dojo.string.substitute(this.queryExpr,[text]); },_startSearch: function(/*String*/ key){ if (key && dojo.trim(key)) { if (key.indexOf('.') == -1) { this.set('store',this.entityStore); } if (dojo.every(key,function(char){ return dojo.indexOf(this.noliterWords,char) > -1 },this)) { this.set('store',this.entityStore); this.leftString = ''; return; } var beginIndex = key.lastIndexOf('.'); if (beginIndex > -1) { this.leftString = key.substr(0,beginIndex + 1); key = key.substr(beginIndex + 1,key.length - 1); } } else { this.leftString = ''; return; } if(!this._popupWidget){ var popupId = this.id + "_popup"; dojo.extend(dijit.form._ComboBoxMenu,{ // these functions are called in showResultList getItems: function(){ return this.domNode.childNodes; },getListLength: function(){ return this.domNode.childNodes.length-2; } }); this._popupWidget = new dijit.form._ComboBoxMenu({ onChange: dojo.hitch(this,this._selectOption),id:popupId }); this.connect(this._popupWidget,'_onMouseUp',function(event){ var value = this.store.getValue(this._popupWidget.getHighlightedOption().item,this.searchAttr); this.focusNode.textContent = value; this._hideResultList(); this._setCaretPos(this.focusNode,value.length); }); //dijit.removeWaiState(this.focusNode,"activedescendant"); //dijit.setWaiState(this.textBox,"owns",popupId); // associate popup with textBox } // create a new query to prevent accidentally querying for a hidden // value from FilteringSelect's keyField this.item = null; // #4872 var query = dojo.clone(this.query); // #5970 this._lastInput = key; // Store exactly what was entered by the user. this._lastQuery = query[this.searchAttr] = this._getQueryString(key); // #5970: set _lastQuery,*then* start the timeout // otherwise,if the user types and the last query returns before the timeout,// _lastQuery won't be set and their input gets rewritten this.searchTimer=setTimeout(dojo.hitch(this,function(query,_this){ var fetch = { queryOptions: { ignoreCase: this.ignoreCase,deep: true },query: query,onBegin: dojo.hitch(this,"_setMaxOptions"),onComplete: dojo.hitch(this,"_openResultList"),onError: function(errText){ console.error('dijit.form.ComboBox: ' + errText); dojo.hitch(_this,"_hideResultList")(); },start:0,count:this.pageSize }; dojo.mixin(fetch,_this.fetchProperties); var dataObject = _this.store.fetch(fetch); var nextSearch = function(dataObject,direction){ dataObject.start += dataObject.count*direction; // #4091: // tell callback the direction of the paging so the screen // reader knows which menu option to shout dataObject.direction = direction; this.store.fetch(dataObject); }; this._nextSearch = this._popupWidget.onPage = dojo.hitch(this,nextSearch,dataObject); },query,this),this.searchDelay); },_setMaxOptions: function(size,request){ this._maxOptions = size; },_getValueField:function(){ return this.searchAttr; },/////////////// Event handlers ///////////////////// _arrowPressed: function(){ if(!this.disabled && !this.readOnly && this.hasDownArrow){ dojo.addClass(this.downArrowNode,"dijitArrowButtonActive"); } },_arrowIdle: function(){ if(!this.disabled && !this.readOnly && this.hasDownArrow){ dojo.removeClass(this.downArrowNode,"dojoArrowButtonPushed"); } },// FIXME: // this is public so we can't remove until 2.0,but the name // SHOULD be "compositionEnd" compositionend: function(/*Event*/ evt){ // summary: // When inputting characters using an input method,such as // Asian languages,it will generate this event instead of // onKeyDown event Note: this event is only triggered in FF // (not in IE) var range = window.getSelection().getRangeAt(0); // DOM下 this.focusNode = range.startContainer; this._onKeyPress({charCode:-1}); },//////////// INITIALIZATION METHODS /////////////////////////////////////// constructor: function(){ this.query={}; this.fetchProperties={}; },postMixInProperties: function(){ this.store = this.entityStore; if(!this.hasDownArrow){ this.baseClass = "dijitTextBox"; } if(!this.store){ var srcNodeRef = this.srcNodeRef; // if user didn't specify store,then assume there are option tags this.store = new dijit.form._ComboBoxDataStore(srcNodeRef); // if there is no value set and there is an option list,set // the value to the first value to be consistent with native // Select // Firefox and Safari set value // IE6 and Opera set selectedIndex,which is automatically set // by the selected attribute of an option tag // IE6 does not set value,Opera sets value = selectedIndex if( !this.value || ( (typeof srcNodeRef.selectedIndex == "number") && srcNodeRef.selectedIndex.toString() === this.value) ){ var item = this.store.fetchSelectedItem(); if(item){ this.value = this.store.getValue(item,this._getValueField()); } } } },_postCreate:function(){ //find any associated label element and add to comboBox node. var label=dojo.query('label[for="'+this.id+'"]'); if(label.length){ label[0].id = (this.id+"_label"); var cn=this.comboNode; //dijit.setWaiState(cn,"labelledby",label[0].id); } },uninitialize:function(){ if(this._popupWidget){ this._hideResultList(); this._popupWidget.destroy(); } },_getMenuLabelFromItem:function(/*Item*/ item){ var label = this.store.getValue(item,this.labelAttr || this.searchAttr); var labelType = this.labelType; // If labelType is not "text" we don't want to screw any markup ot whatever. if (this.highlightMatch!="none" && this.labelType=="text" && this._lastInput){ label = this.doHighlight(label,this._escapeHtml(this._lastInput)); labelType = "html"; } return {html: labelType=="html",label: label}; },doHighlight:function(/*String*/label,/*String*/find){ // summary: // Highlights the string entered by the user in the menu,by default this // highlights the first occurence found. Override this method // to implement your custom highlighing. // Add greedy when this.highlightMatch=="all" var modifiers = "i"+(this.highlightMatch=="all"?"g":""); var escapedLabel = this._escapeHtml(label); var ret = escapedLabel.replace(new RegExp("^("+ find +")",modifiers),'<span class="dijitComboBoxHighlightMatch">$1</span>'); if (escapedLabel==ret){ // Nothing replaced,try to replace at word boundaries. ret = escapedLabel.replace(new RegExp(" ("+ find +")",' <span class="dijitComboBoxHighlightMatch">$1</span>'); } return ret;// returns String,(almost) valid HTML (entities encoded) },_escapeHtml:function(/*string*/str){ // TODO Should become dojo.html.entities(),when exists use instead // summary: // Adds escape sequences for special characters in XML: &<>"' str = String(str).replace(/&/gm,"&").replace(/</gm,"<") .replace(/>/gm,">").replace(/"/gm,"""); return str; // string },open:function(){ this._isShowingNow=true; return dijit.popup.open({ popup: this._popupWidget,around: this.domNode,parent: this }); },reset:function(){ // summary: // Additionally reset the .item (to clean up). this.item = null; this.inherited(arguments); },_onPaste: function(e) { setTimeout(dojo.hitch(this,'_processPasteText'),1); },_processPasteText: function() { var html = this.domNode.innerHTML; if (html.toLowerCase().indexOf('<div>') > 0) { html = '<div>' + html + '</div>'; } html = html.split('<br>').join('</div><div>'); this.domNode.innerHTML = html; this.setData(this.getData()); },_processDomStruct: function(key) { if (key) { var range = window.getSelection().getRangeAt(0); // DOM下 var node = range.startContainer; console.log('Node type is ' + node.nodeType + ' and key is:' + key); if (isNaN(key)) { //literalInput this._changeDomStructure(0,node); } else { if (key == dojo.keys.ENTER) { this._changeDomStructure(1,node); } else if (key == dojo.keys.DELETE) { this._changeDomStructure(2,node); } else if (key == dojo.keys.BACKSPACE) { this._changeDomStructure(3,node); } else if (key === ' ' || key == dojo.keys.TAB) { this._changeDomStructure(4,node); } } } },//type 0 literal 1 ENTER 2 DELETE 3 BACKSPACE 4 SPACE or TAB _changeDomStructure: function(type,node) { var TEXT_NODE = 3,ELEMENT_NODE = 1; if (type == 0 || type == 4) { if (node.nodeType == TEXT_NODE) { //First Input if (node.parentNode == this.domNode) { var text = node.textContent; var trimText = dojo.trim(text); var lineNode = dojo.create('DIV',{className: 'linediv'},node,'after'); if (trimText) { this.focusNode = dojo.create('SPAN',{innerHTML: trimText},lineNode); } else { this.focusNode = this.createBlanSpan(text.length); dojo.place(this.focusNode,lineNode); } dojo.destroy(node); this._setCaretPos(this.focusNode,this.focusNode.textContent.length); } else if (node.parentNode.tagName == 'DIV') { dojo.addClass(node.parentNode,'linediv'); this.focusNode = dojo.create('SPAN',{innerHTML: node.textContent},'after'); dojo.destroy(node); this._setCaretPos(this.focusNode,this.focusNode.textContent.length); } else if (node.parentNode.tagName == 'SPAN') { var text = node.textContent,spanNode = node.parentNode; var trimText = dojo.trim(text); if (trimText) { var pos = this._getCaretPos(node.parentNode); var tempStrings = this.processLine(node.textContent); var count = 0,findflag = true,refNode = node.parentNode; for (var i = 0; i < tempStrings.length; i++) { count += tempStrings[i].length; if (tempStrings[i]) { if (dojo.trim(tempStrings[i])) { var span = dojo.create('SPAN',{innerHTML: tempStrings[i]},refNode,'after'); } else { var span = this.createBlanSpan(tempStrings[i].length); dojo.place(span,'after'); } if (pos <= count && findflag) { this.focusNode = span; this._setCaretPos(span,tempStrings[i].length - (count - pos)); findflag = false; } refNode = span; } else { continue; } } dojo.destroy(node.parentNode); this.highlightText(); } else { this.focusNode = node; } } if (dojo.isFF) { var lineNode = (this.focusNode.nodeType == TEXT_NODE)?this.focusNode.parentNode.parentNode:this.focusNode.parentNode; var spanNode = dojo.query('SPAN[type="FixFF"]',lineNode)[0]; dojo.destroy(spanNode); } } else if (node.nodeType == ELEMENT_NODE) { //It seemed this branch not triggerd } } else if (type == 1) { if (dojo.isFF) { if (node.nodeType == TEXT_NODE) { //It seemed this branch not triggerd } else if (node.nodeType == ELEMENT_NODE) { if (node.tagName == 'SPAN') { var srcLineNode = node.parentNode; var pos = this._getCaretPos(node); var leftRange = document.createRange(); leftRange.selectNodeContents(srcLineNode); var rightRange = leftRange.cloneRange(); //set left range 1)clooapse to begin 2)set end leftRange.collapse(true); leftRange.setEnd(node.childNodes[0],pos); //set right range 1)clooapse to end 2)set Start rightRange.collapse(false); rightRange.setStart(node.childNodes[0],pos); var leftPart = leftRange.extractContents(); var rightPart = rightRange.extractContents(); dojo.empty(srcLineNode); srcLineNode.appendChild(leftPart); var destNode = dojo.create('DIV',srcLineNode,'after'); destNode.appendChild(rightPart); this.focusNode = dojo.query('SPAN',destNode)[0]; dojo.forEach(dojo.query('BR',this.focusNode),function(x){ dojo.destroy(x); }); if (!this.focusNode.textContent) { dojo.create('SPAN',{innerHTML: '',type: 'FixFF'},this.focusNode,'after'); } this._setCaretPos(this.focusNode.parentNode,0); } else if (node.tagName == 'DIV') { var htmlArr = node.innerHTML.split('<br>'); if (node == this.domNode) { var divHtmlArr = []; divHtmlArr.push('<DIV class=\'linediv\'>'); divHtmlArr.push(htmlArr[0]); divHtmlArr.push('</DIV>'); divHtmlArr.push('<DIV class=\'linediv\'>'); divHtmlArr.push(htmlArr[1]?htmlArr[1]:''); divHtmlArr.push('</DIV>'); node.innerHTML = divHtmlArr.join(''); var divs = dojo.query('DIV',node); this.focusNode = divs[0]; this._setCaretPos(this.focusNode,this.focusNode.textContent.length); } else { node.innerHTML = htmlArr[0]; this.focusNode = dojo.create('DIV',{className: 'linediv',innerHTML: htmlArr[1]},'after'); this._setCaretPos(this.focusNode,0); } } } } else { this.focusNode = node; } } else if (type == 2 || type == 3) { if (node.nodeType == TEXT_NODE) { var elementNode = node.parentNode; if (elementNode.tagName == 'SPAN') { this._processLineafterDeletion(elementNode.parentNode,node) } else if (elementNode.tagName == 'DIV' && elementNode != this.domNode) { this._processLineafterDeletion(elementNode,node) } } else if (node.nodeType == ELEMENT_NODE) { if (node.tagName == 'SPAN') { this._processLineafterDeletion(node.parentNode,node) } else if (node.tagName == 'DIV' && node != this.domNode) { this._processLineafterDeletion(node,node) } } } this.highlightText(); },highlightText: function() { dojo.forEach(dojo.query('DIV.linediv',this.domNode),function(line){ var lineSpans = dojo.query('SPAN',line); var tempvalbegin = -1; for (var i = 0; i < lineSpans.length; i++) { dojo.removeClass(lineSpans[i],'keyWord'); dojo.removeClass(lineSpans[i],'tempVal'); if (tempvalbegin == -1 && lineSpans[i].textContent.indexOf('${') == 0) { tempvalbegin = i; } if (tempvalbegin == -1 && dojo.indexOf(this.keyWords,lineSpans[i].textContent) != -1) { dojo.addClass(lineSpans[i],'keyWord'); } if (tempvalbegin > -1 && lineSpans[i].textContent.indexOf('}') == 0) { for (var j = tempvalbegin; j <= i; j++) { dojo.addClass(lineSpans[j],'tempVal'); } tempvalbegin = -1; } } },this); },_processLineafterDeletion: function(lineNode,focusNode) { if (!dojo.trim(focusNode.textContent)) { this.leftString = ''; } var TEXT_NODE = 3,ELEMENT_NODE = 1; var spanNodes = dojo.query('SPAN',lineNode); if (spanNodes.length == 1) { if (spanNodes[0].textContent) { this.focusNode = focusNode; this._setCaretPos(this.focusNode,this._getCaretPos(focusNode)); } else { if (dojo.isFF) { dojo.attr(spanNodes[0],{type: 'FixFF'}); spanNodes[0].innerHTML = ''; this.focusNode = spanNodes[0]; this._setCaretPos(this.focusNode,0); } else { dojo.destroy(spanNodes[0]); this.focusNode = lineNode; this._setCaretPos(this.focusNode,this.focusNode.textContent.length); } } } else { var emptySpans = dojo.filter(spanNodes,function(x){ return !x.textContent; }); if (emptySpans.length >= 1) { this.focusNode = emptySpans[0]; var leftText = this.focusNode.prevIoUsSibling?this.focusNode.prevIoUsSibling.textContent:''; var rightText = this.focusNode.nextSibling?this.focusNode.nextSibling.textContent:''; if (leftText) { dojo.destroy(this.focusNode.prevIoUsSibling); } if (rightText) { dojo.destroy(this.focusNode.nextSibling); } if (dojo.trim(leftText) && (dojo.trim(rightText) && dojo.indexOf(this.noliterWords,rightText) == -1)) { this.focusNode.textContent = leftText + rightText; } else { this.focusNode.textContent = leftText; dojo.place(this.createBlanSpan(rightText.length),'after'); } this._setCaretPos(this.focusNode,leftText.length); } else { this.focusNode = focusNode; this._setCaretPos(this.focusNode,this._getCaretPos(focusNode)); } } },createBlanSpan: function(length) { return dojo.create('SPAN',{innerHTML: dojo.string.pad('',length * 6,'')}); },getData: function() { var codes = []; dojo.forEach(dojo.query('DIV',function(line) { var tempcode = line.textContent; codes.push(tempcode); }); codes = codes.join("\r\n"); return codes; },setData: function(data) { var linkArray = data.split('\r\n').join('\n').split('\r').join('\n').split('\n'); dojo.empty(this.domNode); var htmlArr = []; for (var i = 0; i < linkArray.length; i++) { htmlArr.push('<DIV class=\'linediv\'>'); dojo.forEach(this.processLine(linkArray[i]),function(word){ htmlArr.push('<SPAN>'); htmlArr.push(word); htmlArr.push('</SPAN>'); }); htmlArr.push('</DIV>'); } this.domNode.innerHTML = htmlArr.join(''); this.highlightText(); },processLine: function(text) { if (!text) { return ['']; } var subBegin = 0,result = [],beginFlag = 0//literal if (!dojo.trim(text[0])) { beginFlag = 1;//space } else if (dojo.indexOf(this.noliterWords,text[0]) > -1) { beginFlag = 2; //none literal } for (var i = 1; i < text.length; i++) { if (beginFlag == 0 && (dojo.indexOf(this.noliterWords,text[i]) == -1 && dojo.trim(text[i]))) { continue; } else if (beginFlag == 1 && !dojo.trim(text[i])) { continue; } else if (beginFlag == 2 && dojo.indexOf(this.noliterWords,text[i]) > -1) { continue; } else { result.push(text.substring(subBegin,i)); subBegin = i; if (!dojo.trim(text[i])) { beginFlag = 1;//space } else if (dojo.indexOf(this.noliterWords,text[i]) > -1) { beginFlag = 2; //none literal } else { beginFlag = 0; //literal } } } if (subBegin < text.length) { result.push(text.substring(subBegin,text.length)); } return result; } } );