source: Dev/trunk/src/client/dijit/_editor/range.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 15.8 KB
Line 
1define([
2        "dojo/_base/array", // array.every
3        "dojo/_base/declare", // declare
4        "dojo/_base/lang" // lang.isArray
5], function(array, declare, lang){
6
7        // module:
8        //              dijit/_editor/range
9
10        var rangeapi = {
11                // summary:
12                //              W3C range API
13
14                getIndex: function(/*DomNode*/ node, /*DomNode*/ parent){
15                        var ret = [], retR = [];
16                        var onode = node;
17
18                        var pnode, n;
19                        while(node != parent){
20                                var i = 0;
21                                pnode = node.parentNode;
22                                while((n = pnode.childNodes[i++])){
23                                        if(n === node){
24                                                --i;
25                                                break;
26                                        }
27                                }
28                                //if(i>=pnode.childNodes.length){
29                                //console.debug("Error finding index of a node in dijit/range.getIndex()");
30                                //}
31                                ret.unshift(i);
32                                retR.unshift(i - pnode.childNodes.length);
33                                node = pnode;
34                        }
35
36                        //normalized() can not be called so often to prevent
37                        //invalidating selection/range, so we have to detect
38                        //here that any text nodes in a row
39                        if(ret.length > 0 && onode.nodeType == 3){
40                                n = onode.previousSibling;
41                                while(n && n.nodeType == 3){
42                                        ret[ret.length - 1]--;
43                                        n = n.previousSibling;
44                                }
45                                n = onode.nextSibling;
46                                while(n && n.nodeType == 3){
47                                        retR[retR.length - 1]++;
48                                        n = n.nextSibling;
49                                }
50                        }
51
52                        return {o: ret, r:retR};
53                },
54
55                getNode: function(/*Array*/ index, /*DomNode*/ parent){
56                        if(!lang.isArray(index) || index.length == 0){
57                                return parent;
58                        }
59                        var node = parent;
60                        //      if(!node)debugger
61                        array.every(index, function(i){
62                                if(i >= 0 && i < node.childNodes.length){
63                                        node = node.childNodes[i];
64                                }else{
65                                        node = null;
66                                        //console.debug('Error: can not find node with index',index,'under parent node',parent );
67                                        return false; //terminate array.every
68                                }
69                                return true; //carry on the every loop
70                        });
71
72                        return node;
73                },
74
75                getCommonAncestor: function(n1, n2, root){
76                        root = root || n1.ownerDocument.body;
77                        var getAncestors = function(n){
78                                var as = [];
79                                while(n){
80                                        as.unshift(n);
81                                        if(n !== root){
82                                                n = n.parentNode;
83                                        }else{
84                                                break;
85                                        }
86                                }
87                                return as;
88                        };
89                        var n1as = getAncestors(n1);
90                        var n2as = getAncestors(n2);
91
92                        var m = Math.min(n1as.length, n2as.length);
93                        var com = n1as[0]; //at least, one element should be in the array: the root (BODY by default)
94                        for(var i = 1; i < m; i++){
95                                if(n1as[i] === n2as[i]){
96                                        com = n1as[i]
97                                }else{
98                                        break;
99                                }
100                        }
101                        return com;
102                },
103
104                getAncestor: function(/*DomNode*/ node, /*RegEx?*/ regex, /*DomNode?*/ root){
105                        root = root || node.ownerDocument.body;
106                        while(node && node !== root){
107                                var name = node.nodeName.toUpperCase();
108                                if(regex.test(name)){
109                                        return node;
110                                }
111
112                                node = node.parentNode;
113                        }
114                        return null;
115                },
116
117                BlockTagNames: /^(?:P|DIV|H1|H2|H3|H4|H5|H6|ADDRESS|PRE|OL|UL|LI|DT|DE)$/,
118
119                getBlockAncestor: function(/*DomNode*/ node, /*RegEx?*/ regex, /*DomNode?*/ root){
120                        root = root || node.ownerDocument.body;
121                        regex = regex || rangeapi.BlockTagNames;
122                        var block = null, blockContainer;
123                        while(node && node !== root){
124                                var name = node.nodeName.toUpperCase();
125                                if(!block && regex.test(name)){
126                                        block = node;
127                                }
128                                if(!blockContainer && (/^(?:BODY|TD|TH|CAPTION)$/).test(name)){
129                                        blockContainer = node;
130                                }
131
132                                node = node.parentNode;
133                        }
134                        return {blockNode:block, blockContainer:blockContainer || node.ownerDocument.body};
135                },
136
137                atBeginningOfContainer: function(/*DomNode*/ container, /*DomNode*/ node, /*Int*/ offset){
138                        var atBeginning = false;
139                        var offsetAtBeginning = (offset == 0);
140                        if(!offsetAtBeginning && node.nodeType == 3){ //if this is a text node, check whether the left part is all space
141                                if(/^[\s\xA0]+$/.test(node.nodeValue.substr(0, offset))){
142                                        offsetAtBeginning = true;
143                                }
144                        }
145                        if(offsetAtBeginning){
146                                var cnode = node;
147                                atBeginning = true;
148                                while(cnode && cnode !== container){
149                                        if(cnode.previousSibling){
150                                                atBeginning = false;
151                                                break;
152                                        }
153                                        cnode = cnode.parentNode;
154                                }
155                        }
156                        return atBeginning;
157                },
158
159                atEndOfContainer: function(/*DomNode*/ container, /*DomNode*/ node, /*Int*/ offset){
160                        var atEnd = false;
161                        var offsetAtEnd = (offset == (node.length || node.childNodes.length));
162                        if(!offsetAtEnd && node.nodeType == 3){ //if this is a text node, check whether the right part is all space
163                                if(/^[\s\xA0]+$/.test(node.nodeValue.substr(offset))){
164                                        offsetAtEnd = true;
165                                }
166                        }
167                        if(offsetAtEnd){
168                                var cnode = node;
169                                atEnd = true;
170                                while(cnode && cnode !== container){
171                                        if(cnode.nextSibling){
172                                                atEnd = false;
173                                                break;
174                                        }
175                                        cnode = cnode.parentNode;
176                                }
177                        }
178                        return atEnd;
179                },
180
181                adjacentNoneTextNode: function(startnode, next){
182                        var node = startnode;
183                        var len = (0 - startnode.length) || 0;
184                        var prop = next ? 'nextSibling' : 'previousSibling';
185                        while(node){
186                                if(node.nodeType != 3){
187                                        break;
188                                }
189                                len += node.length;
190                                node = node[prop];
191                        }
192                        return [node,len];
193                },
194
195                create: function(/*Window?*/ win){      // TODO: for 2.0, replace optional window param w/mandatory window or document param
196                        win = win || window;
197                        if(win.getSelection){
198                                return win.document.createRange();
199                        }else{//IE
200                                return new W3CRange();
201                        }
202                },
203
204                getSelection: function(/*Window*/ window, /*Boolean?*/ ignoreUpdate){
205                        if(window.getSelection){
206                                return window.getSelection();
207                        }else{//IE
208                                var s = new ie.selection(window);
209                                if(!ignoreUpdate){
210                                        s._getCurrentSelection();
211                                }
212                                return s;
213                        }
214                }
215        };
216
217        // TODO: convert to has() test?   But remember IE9 issues with quirks vs. standards in main frame vs. iframe.
218        if(!window.getSelection){
219                var ie = rangeapi.ie = {
220                        cachedSelection: {},
221                        selection: function(window){
222                                this._ranges = [];
223                                this.addRange = function(r, /*boolean*/ internal){
224                                        this._ranges.push(r);
225                                        if(!internal){
226                                                r._select();
227                                        }
228                                        this.rangeCount = this._ranges.length;
229                                };
230                                this.removeAllRanges = function(){
231                                        //don't detach, the range may be used later
232                                        //                              for(var i=0;i<this._ranges.length;i++){
233                                        //                                      this._ranges[i].detach();
234                                        //                              }
235                                        this._ranges = [];
236                                        this.rangeCount = 0;
237                                };
238                                var _initCurrentRange = function(){
239                                        var r = window.document.selection.createRange();
240                                        var type = window.document.selection.type.toUpperCase();
241                                        if(type == "CONTROL"){
242                                                //TODO: multiple range selection(?)
243                                                return new W3CRange(ie.decomposeControlRange(r));
244                                        }else{
245                                                return new W3CRange(ie.decomposeTextRange(r));
246                                        }
247                                };
248                                this.getRangeAt = function(i){
249                                        return this._ranges[i];
250                                };
251                                this._getCurrentSelection = function(){
252                                        this.removeAllRanges();
253                                        var r = _initCurrentRange();
254                                        if(r){
255                                                this.addRange(r, true);
256                                                this.isCollapsed = r.collapsed;
257                                        }else{
258                                                this.isCollapsed = true;
259                                        }
260                                };
261                        },
262                        decomposeControlRange: function(range){
263                                var firstnode = range.item(0), lastnode = range.item(range.length - 1);
264                                var startContainer = firstnode.parentNode, endContainer = lastnode.parentNode;
265                                var startOffset = rangeapi.getIndex(firstnode, startContainer).o[0];
266                                var endOffset = rangeapi.getIndex(lastnode, endContainer).o[0] + 1;
267                                return [startContainer, startOffset,endContainer, endOffset];
268                        },
269                        getEndPoint: function(range, end){
270                                var atmrange = range.duplicate();
271                                atmrange.collapse(!end);
272                                var cmpstr = 'EndTo' + (end ? 'End' : 'Start');
273                                var parentNode = atmrange.parentElement();
274
275                                var startnode, startOffset, lastNode;
276                                if(parentNode.childNodes.length > 0){
277                                        array.every(parentNode.childNodes, function(node, i){
278                                                var calOffset;
279                                                if(node.nodeType != 3){
280                                                        atmrange.moveToElementText(node);
281
282                                                        if(atmrange.compareEndPoints(cmpstr, range) > 0){
283                                                                //startnode = node.previousSibling;
284                                                                if(lastNode && lastNode.nodeType == 3){
285                                                                        //where shall we put the start? in the text node or after?
286                                                                        startnode = lastNode;
287                                                                        calOffset = true;
288                                                                }else{
289                                                                        startnode = parentNode;
290                                                                        startOffset = i;
291                                                                        return false;
292                                                                }
293                                                        }else{
294                                                                if(i == parentNode.childNodes.length - 1){
295                                                                        startnode = parentNode;
296                                                                        startOffset = parentNode.childNodes.length;
297                                                                        return false;
298                                                                }
299                                                        }
300                                                }else{
301                                                        if(i == parentNode.childNodes.length - 1){//at the end of this node
302                                                                startnode = node;
303                                                                calOffset = true;
304                                                        }
305                                                }
306                                                //                      try{
307                                                if(calOffset && startnode){
308                                                        var prevnode = rangeapi.adjacentNoneTextNode(startnode)[0];
309                                                        if(prevnode){
310                                                                startnode = prevnode.nextSibling;
311                                                        }else{
312                                                                startnode = parentNode.firstChild; //firstChild must be a text node
313                                                        }
314                                                        var prevnodeobj = rangeapi.adjacentNoneTextNode(startnode);
315                                                        prevnode = prevnodeobj[0];
316                                                        var lenoffset = prevnodeobj[1];
317                                                        if(prevnode){
318                                                                atmrange.moveToElementText(prevnode);
319                                                                atmrange.collapse(false);
320                                                        }else{
321                                                                atmrange.moveToElementText(parentNode);
322                                                        }
323                                                        atmrange.setEndPoint(cmpstr, range);
324                                                        startOffset = atmrange.text.length - lenoffset;
325
326                                                        return false;
327                                                }
328                                                //                      }catch(e){ debugger }
329                                                lastNode = node;
330                                                return true;
331                                        });
332                                }else{
333                                        startnode = parentNode;
334                                        startOffset = 0;
335                                }
336
337                                //if at the end of startnode and we are dealing with start container, then
338                                //move the startnode to nextSibling if it is a text node
339                                //TODO: do this for end container?
340                                if(!end && startnode.nodeType == 1 && startOffset == startnode.childNodes.length){
341                                        var nextnode = startnode.nextSibling;
342                                        if(nextnode && nextnode.nodeType == 3){
343                                                startnode = nextnode;
344                                                startOffset = 0;
345                                        }
346                                }
347                                return [startnode, startOffset];
348                        },
349                        setEndPoint: function(range, container, offset){
350                                //text node
351                                var atmrange = range.duplicate(), node, len;
352                                if(container.nodeType != 3){ //normal node
353                                        if(offset > 0){
354                                                node = container.childNodes[offset - 1];
355                                                if(node){
356                                                        if(node.nodeType == 3){
357                                                                container = node;
358                                                                offset = node.length;
359                                                                //pass through
360                                                        }else{
361                                                                if(node.nextSibling && node.nextSibling.nodeType == 3){
362                                                                        container = node.nextSibling;
363                                                                        offset = 0;
364                                                                        //pass through
365                                                                }else{
366                                                                        atmrange.moveToElementText(node.nextSibling ? node : container);
367                                                                        var parent = node.parentNode;
368                                                                        var tempNode = parent.insertBefore(node.ownerDocument.createTextNode(' '), node.nextSibling);
369                                                                        atmrange.collapse(false);
370                                                                        parent.removeChild(tempNode);
371                                                                }
372                                                        }
373                                                }
374                                        }else{
375                                                atmrange.moveToElementText(container);
376                                                atmrange.collapse(true);
377                                        }
378                                }
379                                if(container.nodeType == 3){
380                                        var prevnodeobj = rangeapi.adjacentNoneTextNode(container);
381                                        var prevnode = prevnodeobj[0];
382                                        len = prevnodeobj[1];
383                                        if(prevnode){
384                                                atmrange.moveToElementText(prevnode);
385                                                atmrange.collapse(false);
386                                                //if contentEditable is not inherit, the above collapse won't make the end point
387                                                //in the correctly position: it always has a -1 offset, so compensate it
388                                                if(prevnode.contentEditable != 'inherit'){
389                                                        len++;
390                                                }
391                                        }else{
392                                                atmrange.moveToElementText(container.parentNode);
393                                                atmrange.collapse(true);
394
395                                                // Correct internal cursor position
396                                                // http://bugs.dojotoolkit.org/ticket/15578
397                                                atmrange.move('character', 1);
398                                                atmrange.move('character', -1);
399                                        }
400
401                                        offset += len;
402                                        if(offset > 0){
403                                                if(atmrange.move('character', offset) != offset){
404                                                        console.error('Error when moving!');
405                                                }
406                                        }
407                                }
408
409                                return atmrange;
410                        },
411                        decomposeTextRange: function(range){
412                                var tmpary = ie.getEndPoint(range);
413                                var startContainer = tmpary[0], startOffset = tmpary[1];
414                                var endContainer = tmpary[0], endOffset = tmpary[1];
415
416                                if(range.htmlText.length){
417                                        if(range.htmlText == range.text){ //in the same text node
418                                                endOffset = startOffset + range.text.length;
419                                        }else{
420                                                tmpary = ie.getEndPoint(range, true);
421                                                endContainer = tmpary[0],endOffset = tmpary[1];
422                                                //                                      if(startContainer.tagName == "BODY"){
423                                                //                                              startContainer = startContainer.firstChild;
424                                                //                                      }
425                                        }
426                                }
427                                return [startContainer, startOffset, endContainer, endOffset];
428                        },
429                        setRange: function(range, startContainer, startOffset, endContainer, endOffset, collapsed){
430                                var start = ie.setEndPoint(range, startContainer, startOffset);
431
432                                range.setEndPoint('StartToStart', start);
433                                if(!collapsed){
434                                        var end = ie.setEndPoint(range, endContainer, endOffset);
435                                }
436                                range.setEndPoint('EndToEnd', end || start);
437
438                                return range;
439                        }
440                };
441
442                var W3CRange = rangeapi.W3CRange = declare(null, {
443                        constructor: function(){
444                                if(arguments.length>0){
445                                        this.setStart(arguments[0][0],arguments[0][1]);
446                                        this.setEnd(arguments[0][2],arguments[0][3]);
447                                }else{
448                                        this.commonAncestorContainer = null;
449                                        this.startContainer = null;
450                                        this.startOffset = 0;
451                                        this.endContainer = null;
452                                        this.endOffset = 0;
453                                        this.collapsed = true;
454                                }
455                        },
456                        _updateInternal: function(){
457                                if(this.startContainer !== this.endContainer){
458                                        this.commonAncestorContainer = rangeapi.getCommonAncestor(this.startContainer, this.endContainer);
459                                }else{
460                                        this.commonAncestorContainer = this.startContainer;
461                                }
462                                this.collapsed = (this.startContainer === this.endContainer) && (this.startOffset == this.endOffset);
463                        },
464                        setStart: function(node, offset){
465                                offset=parseInt(offset);
466                                if(this.startContainer === node && this.startOffset == offset){
467                                        return;
468                                }
469                                delete this._cachedBookmark;
470
471                                this.startContainer = node;
472                                this.startOffset = offset;
473                                if(!this.endContainer){
474                                        this.setEnd(node, offset);
475                                }else{
476                                        this._updateInternal();
477                                }
478                        },
479                        setEnd: function(node, offset){
480                                offset=parseInt(offset);
481                                if(this.endContainer === node && this.endOffset == offset){
482                                        return;
483                                }
484                                delete this._cachedBookmark;
485
486                                this.endContainer = node;
487                                this.endOffset = offset;
488                                if(!this.startContainer){
489                                        this.setStart(node, offset);
490                                }else{
491                                        this._updateInternal();
492                                }
493                        },
494                        setStartAfter: function(node, offset){
495                                this._setPoint('setStart', node, offset, 1);
496                        },
497                        setStartBefore: function(node, offset){
498                                this._setPoint('setStart', node, offset, 0);
499                        },
500                        setEndAfter: function(node, offset){
501                                this._setPoint('setEnd', node, offset, 1);
502                        },
503                        setEndBefore: function(node, offset){
504                                this._setPoint('setEnd', node, offset, 0);
505                        },
506                        _setPoint: function(what, node, offset, ext){
507                                var index = rangeapi.getIndex(node, node.parentNode).o;
508                                this[what](node.parentNode, index.pop()+ext);
509                        },
510                        _getIERange: function(){
511                                var r = (this._body || this.endContainer.ownerDocument.body).createTextRange();
512                                ie.setRange(r, this.startContainer, this.startOffset, this.endContainer, this.endOffset, this.collapsed);
513                                return r;
514                        },
515                        getBookmark: function(){
516                                this._getIERange();
517                                return this._cachedBookmark;
518                        },
519                        _select: function(){
520                                var r = this._getIERange();
521                                r.select();
522                        },
523                        deleteContents: function(){
524                                var s = this.startContainer, r = this._getIERange();
525                                if(s.nodeType === 3 && !this.startOffset){
526                                        //if the range starts at the beginning of a
527                                        //text node, move it to before the textnode
528                                        //to make sure the range is still valid
529                                        //after deleteContents() finishes
530                                        this.setStartBefore(s);
531                                }
532                                r.pasteHTML('');
533                                this.endContainer = this.startContainer;
534                                this.endOffset = this.startOffset;
535                                this.collapsed = true;
536                        },
537                        cloneRange: function(){
538                                var r = new W3CRange([this.startContainer,this.startOffset,
539                                        this.endContainer,this.endOffset]);
540                                r._body = this._body;
541                                return r;
542                        },
543                        detach: function(){
544                                this._body = null;
545                                this.commonAncestorContainer = null;
546                                this.startContainer = null;
547                                this.startOffset = 0;
548                                this.endContainer = null;
549                                this.endOffset = 0;
550                                this.collapsed = true;
551                        }
552                });
553        } //if(!window.getSelection)
554
555        // remove for 2.0
556        lang.setObject("dijit.range", rangeapi);
557
558        return rangeapi;
559});
Note: See TracBrowser for help on using the repository browser.