]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/javascript/tiny_mce/classes/dom/TridentSelection.js
Release 6.5.0
[Github/sugarcrm.git] / include / javascript / tiny_mce / classes / dom / TridentSelection.js
1 /**
2  * TridentSelection.js
3  *
4  * Copyright 2009, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://tinymce.moxiecode.com/license
8  * Contributing: http://tinymce.moxiecode.com/contributing
9  */
10
11 (function() {
12         function Selection(selection) {
13                 var self = this, dom = selection.dom, TRUE = true, FALSE = false;
14
15                 function getPosition(rng, start) {
16                         var checkRng, startIndex = 0, endIndex, inside,
17                                 children, child, offset, index, position = -1, parent;
18
19                         // Setup test range, collapse it and get the parent
20                         checkRng = rng.duplicate();
21                         checkRng.collapse(start);
22                         parent = checkRng.parentElement();
23
24                         // Check if the selection is within the right document
25                         if (parent.ownerDocument !== selection.dom.doc)
26                                 return;
27
28                         // IE will report non editable elements as it's parent so look for an editable one
29                         while (parent.contentEditable === "false") {
30                                 parent = parent.parentNode;
31                         }
32
33                         // If parent doesn't have any children then return that we are inside the element
34                         if (!parent.hasChildNodes()) {
35                                 return {node : parent, inside : 1};
36                         }
37
38                         // Setup node list and endIndex
39                         children = parent.children;
40                         endIndex = children.length - 1;
41
42                         // Perform a binary search for the position
43                         while (startIndex <= endIndex) {
44                                 index = Math.floor((startIndex + endIndex) / 2);
45
46                                 // Move selection to node and compare the ranges
47                                 child = children[index];
48                                 checkRng.moveToElementText(child);
49                                 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
50
51                                 // Before/after or an exact match
52                                 if (position > 0) {
53                                         endIndex = index - 1;
54                                 } else if (position < 0) {
55                                         startIndex = index + 1;
56                                 } else {
57                                         return {node : child};
58                                 }
59                         }
60
61                         // Check if child position is before or we didn't find a position
62                         if (position < 0) {
63                                 // No element child was found use the parent element and the offset inside that
64                                 if (!child) {
65                                         checkRng.moveToElementText(parent);
66                                         checkRng.collapse(true);
67                                         child = parent;
68                                         inside = true;
69                                 } else
70                                         checkRng.collapse(false);
71
72                                 checkRng.setEndPoint(start ? 'EndToStart' : 'EndToEnd', rng);
73
74                                 // Fix for edge case: <div style="width: 100px; height:100px;"><table>..</table>ab|c</div>
75                                 if (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) > 0) {
76                                         checkRng = rng.duplicate();
77                                         checkRng.collapse(start);
78
79                                         offset = -1;
80                                         while (parent == checkRng.parentElement()) {
81                                                 if (checkRng.move('character', -1) == 0)
82                                                         break;
83
84                                                 offset++;
85                                         }
86                                 }
87
88                                 offset = offset || checkRng.text.replace('\r\n', ' ').length;
89                         } else {
90                                 // Child position is after the selection endpoint
91                                 checkRng.collapse(true);
92                                 checkRng.setEndPoint(start ? 'StartToStart' : 'StartToEnd', rng);
93
94                                 // Get the length of the text to find where the endpoint is relative to it's container
95                                 offset = checkRng.text.replace('\r\n', ' ').length;
96                         }
97
98                         return {node : child, position : position, offset : offset, inside : inside};
99                 };
100
101                 // Returns a W3C DOM compatible range object by using the IE Range API
102                 function getRange() {
103                         var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail;
104
105                         // If selection is outside the current document just return an empty range
106                         element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
107                         if (element.ownerDocument != dom.doc)
108                                 return domRange;
109
110                         collapsed = selection.isCollapsed();
111
112                         // Handle control selection
113                         if (ieRange.item) {
114                                 domRange.setStart(element.parentNode, dom.nodeIndex(element));
115                                 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
116
117                                 return domRange;
118                         }
119
120                         function findEndPoint(start) {
121                                 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
122
123                                 container = endPoint.node;
124                                 offset = endPoint.offset;
125
126                                 if (endPoint.inside && !container.hasChildNodes()) {
127                                         domRange[start ? 'setStart' : 'setEnd'](container, 0);
128                                         return;
129                                 }
130
131                                 if (offset === undef) {
132                                         domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
133                                         return;
134                                 }
135
136                                 if (endPoint.position < 0) {
137                                         sibling = endPoint.inside ? container.firstChild : container.nextSibling;
138
139                                         if (!sibling) {
140                                                 domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
141                                                 return;
142                                         }
143
144                                         if (!offset) {
145                                                 if (sibling.nodeType == 3)
146                                                         domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
147                                                 else
148                                                         domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
149
150                                                 return;
151                                         }
152
153                                         // Find the text node and offset
154                                         while (sibling) {
155                                                 nodeValue = sibling.nodeValue;
156                                                 textNodeOffset += nodeValue.length;
157
158                                                 // We are at or passed the position we where looking for
159                                                 if (textNodeOffset >= offset) {
160                                                         container = sibling;
161                                                         textNodeOffset -= offset;
162                                                         textNodeOffset = nodeValue.length - textNodeOffset;
163                                                         break;
164                                                 }
165
166                                                 sibling = sibling.nextSibling;
167                                         }
168                                 } else {
169                                         // Find the text node and offset
170                                         sibling = container.previousSibling;
171
172                                         if (!sibling)
173                                                 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
174
175                                         // If there isn't any text to loop then use the first position
176                                         if (!offset) {
177                                                 if (container.nodeType == 3)
178                                                         domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
179                                                 else
180                                                         domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
181
182                                                 return;
183                                         }
184
185                                         while (sibling) {
186                                                 textNodeOffset += sibling.nodeValue.length;
187
188                                                 // We are at or passed the position we where looking for
189                                                 if (textNodeOffset >= offset) {
190                                                         container = sibling;
191                                                         textNodeOffset -= offset;
192                                                         break;
193                                                 }
194
195                                                 sibling = sibling.previousSibling;
196                                         }
197                                 }
198
199                                 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
200                         };
201
202                         try {
203                                 // Find start point
204                                 findEndPoint(true);
205
206                                 // Find end point if needed
207                                 if (!collapsed)
208                                         findEndPoint();
209                         } catch (ex) {
210                                 // IE has a nasty bug where text nodes might throw "invalid argument" when you
211                                 // access the nodeValue or other properties of text nodes. This seems to happend when
212                                 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
213                                 if (ex.number == -2147024809) {
214                                         // Get the current selection
215                                         bookmark = self.getBookmark(2);
216
217                                         // Get start element
218                                         tmpRange = ieRange.duplicate();
219                                         tmpRange.collapse(true);
220                                         element = tmpRange.parentElement();
221
222                                         // Get end element
223                                         if (!collapsed) {
224                                                 tmpRange = ieRange.duplicate();
225                                                 tmpRange.collapse(false);
226                                                 element2 = tmpRange.parentElement();
227                                                 element2.innerHTML = element2.innerHTML;
228                                         }
229
230                                         // Remove the broken elements
231                                         element.innerHTML = element.innerHTML;
232
233                                         // Restore the selection
234                                         self.moveToBookmark(bookmark);
235
236                                         // Since the range has moved we need to re-get it
237                                         ieRange = selection.getRng();
238
239                                         // Find start point
240                                         findEndPoint(true);
241
242                                         // Find end point if needed
243                                         if (!collapsed)
244                                                 findEndPoint();
245                                 } else
246                                         throw ex; // Throw other errors
247                         }
248
249                         return domRange;
250                 };
251
252                 this.getBookmark = function(type) {
253                         var rng = selection.getRng(), start, end, bookmark = {};
254
255                         function getIndexes(node) {
256                                 var node, parent, root, children, i, indexes = [];
257
258                                 parent = node.parentNode;
259                                 root = dom.getRoot().parentNode;
260
261                                 while (parent != root) {
262                                         children = parent.children;
263
264                                         i = children.length;
265                                         while (i--) {
266                                                 if (node === children[i]) {
267                                                         indexes.push(i);
268                                                         break;
269                                                 }
270                                         }
271
272                                         node = parent;
273                                         parent = parent.parentNode;
274                                 }
275
276                                 return indexes;
277                         };
278
279                         function getBookmarkEndPoint(start) {
280                                 var position;
281
282                                 position = getPosition(rng, start);
283                                 if (position) {
284                                         return {
285                                                 position : position.position,
286                                                 offset : position.offset,
287                                                 indexes : getIndexes(position.node),
288                                                 inside : position.inside
289                                         };
290                                 }
291                         };
292
293                         // Non ubstructive bookmark
294                         if (type === 2) {
295                                 // Handle text selection
296                                 if (!rng.item) {
297                                         bookmark.start = getBookmarkEndPoint(true);
298
299                                         if (!selection.isCollapsed())
300                                                 bookmark.end = getBookmarkEndPoint();
301                                 } else
302                                         bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))};
303                         }
304
305                         return bookmark;
306                 };
307
308                 this.moveToBookmark = function(bookmark) {
309                         var rng, body = dom.doc.body;
310
311                         function resolveIndexes(indexes) {
312                                 var node, i, idx, children;
313
314                                 node = dom.getRoot();
315                                 for (i = indexes.length - 1; i >= 0; i--) {
316                                         children = node.children;
317                                         idx = indexes[i];
318
319                                         if (idx <= children.length - 1) {
320                                                 node = children[idx];
321                                         }
322                                 }
323
324                                 return node;
325                         };
326                         
327                         function setBookmarkEndPoint(start) {
328                                 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef;
329
330                                 if (endPoint) {
331                                         moveLeft = endPoint.position > 0;
332
333                                         moveRng = body.createTextRange();
334                                         moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
335
336                                         offset = endPoint.offset;
337                                         if (offset !== undef) {
338                                                 moveRng.collapse(endPoint.inside || moveLeft);
339                                                 moveRng.moveStart('character', moveLeft ? -offset : offset);
340                                         } else
341                                                 moveRng.collapse(start);
342
343                                         rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
344
345                                         if (start)
346                                                 rng.collapse(true);
347                                 }
348                         };
349
350                         if (bookmark.start) {
351                                 if (bookmark.start.ctrl) {
352                                         rng = body.createControlRange();
353                                         rng.addElement(resolveIndexes(bookmark.start.indexes));
354                                         rng.select();
355                                 } else {
356                                         rng = body.createTextRange();
357                                         setBookmarkEndPoint(true);
358                                         setBookmarkEndPoint();
359                                         rng.select();
360                                 }
361                         }
362                 };
363
364                 this.addRange = function(rng) {
365                         var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body;
366
367                         function setEndPoint(start) {
368                                 var container, offset, marker, tmpRng, nodes;
369
370                                 marker = dom.create('a');
371                                 container = start ? startContainer : endContainer;
372                                 offset = start ? startOffset : endOffset;
373                                 tmpRng = ieRng.duplicate();
374
375                                 if (container == doc || container == doc.documentElement) {
376                                         container = body;
377                                         offset = 0;
378                                 }
379
380                                 if (container.nodeType == 3) {
381                                         container.parentNode.insertBefore(marker, container);
382                                         tmpRng.moveToElementText(marker);
383                                         tmpRng.moveStart('character', offset);
384                                         dom.remove(marker);
385                                         ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
386                                 } else {
387                                         nodes = container.childNodes;
388
389                                         if (nodes.length) {
390                                                 if (offset >= nodes.length) {
391                                                         dom.insertAfter(marker, nodes[nodes.length - 1]);
392                                                 } else {
393                                                         container.insertBefore(marker, nodes[offset]);
394                                                 }
395
396                                                 tmpRng.moveToElementText(marker);
397                                         } else {
398                                                 // Empty node selection for example <div>|</div>
399                                                 marker = doc.createTextNode('\uFEFF');
400                                                 container.appendChild(marker);
401                                                 tmpRng.moveToElementText(marker.parentNode);
402                                                 tmpRng.collapse(TRUE);
403                                         }
404
405                                         ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
406                                         dom.remove(marker);
407                                 }
408                         }
409
410                         // Setup some shorter versions
411                         startContainer = rng.startContainer;
412                         startOffset = rng.startOffset;
413                         endContainer = rng.endContainer;
414                         endOffset = rng.endOffset;
415                         ieRng = body.createTextRange();
416
417                         // If single element selection then try making a control selection out of it
418                         if (startContainer == endContainer && startContainer.nodeType == 1 && startOffset == endOffset - 1) {
419                                 if (startOffset == endOffset - 1) {
420                                         try {
421                                                 ctrlRng = body.createControlRange();
422                                                 ctrlRng.addElement(startContainer.childNodes[startOffset]);
423                                                 ctrlRng.select();
424                                                 return;
425                                         } catch (ex) {
426                                                 // Ignore
427                                         }
428                                 }
429                         }
430
431                         // Set start/end point of selection
432                         setEndPoint(true);
433                         setEndPoint();
434
435                         // Select the new range and scroll it into view
436                         ieRng.select();
437                 };
438
439                 // Expose range method
440                 this.getRangeAt = getRange;
441         };
442
443         // Expose the selection object
444         tinymce.dom.TridentSelection = Selection;
445 })();