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