基于Dojo的简单IDE编辑器----DOM浏览器上的实现

感谢reedseutozte的投稿,发现在Web上进行代码编辑的需求越来越多,也有一些开源的实现。 reedseutozte的这篇文章会告诉你如何基于dojo实现自己的代码编辑器。

------------------------------------------------------------------------------------

两年前,本人写了一篇Blog,描述了如何在IE上实现编辑器的功能,http://blog.csdn.net/reedseutozte/article/details/5761665当时由于产品只要求支持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;
        }
    }
);







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值