感谢reedseutozte的投稿,发现在Web上进行代码编辑的需求越来越多,也有一些开源的实现。 reedseutozte的这篇文章会告诉你如何基于dojo实现自己的代码编辑器。
------------------------------------------------------------------------------------
两年前,本人写了一篇Blog,描述了如何在IE上实现编辑器的功能,http://www.voidcn.com/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, 连续的非文本字符除空格外单独一个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}, fn, '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, {width: "", 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, "expanded", "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.store.getValue(tgt.item, 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 +")", modifiers), ' <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}, node, '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, refNode, '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', {className: 'linediv'}, 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]}, node, '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), this.focusNode, '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', this.domNode), 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; } } );