|
1 /** |
|
2 * RangeUtils.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * This class contains a few utility methods for ranges. |
|
13 * |
|
14 * @class tinymce.dom.RangeUtils |
|
15 */ |
|
16 define("tinymce/dom/RangeUtils", [ |
|
17 "tinymce/util/Tools", |
|
18 "tinymce/dom/TreeWalker" |
|
19 ], function(Tools, TreeWalker) { |
|
20 var each = Tools.each; |
|
21 |
|
22 function getEndChild(container, index) { |
|
23 var childNodes = container.childNodes; |
|
24 |
|
25 index--; |
|
26 |
|
27 if (index > childNodes.length - 1) { |
|
28 index = childNodes.length - 1; |
|
29 } else if (index < 0) { |
|
30 index = 0; |
|
31 } |
|
32 |
|
33 return childNodes[index] || container; |
|
34 } |
|
35 |
|
36 function RangeUtils(dom) { |
|
37 /** |
|
38 * Walks the specified range like object and executes the callback for each sibling collection it finds. |
|
39 * |
|
40 * @method walk |
|
41 * @param {Object} rng Range like object. |
|
42 * @param {function} callback Callback function to execute for each sibling collection. |
|
43 */ |
|
44 this.walk = function(rng, callback) { |
|
45 var startContainer = rng.startContainer, |
|
46 startOffset = rng.startOffset, |
|
47 endContainer = rng.endContainer, |
|
48 endOffset = rng.endOffset, |
|
49 ancestor, startPoint, |
|
50 endPoint, node, parent, siblings, nodes; |
|
51 |
|
52 // Handle table cell selection the table plugin enables |
|
53 // you to fake select table cells and perform formatting actions on them |
|
54 nodes = dom.select('td.mce-item-selected,th.mce-item-selected'); |
|
55 if (nodes.length > 0) { |
|
56 each(nodes, function(node) { |
|
57 callback([node]); |
|
58 }); |
|
59 |
|
60 return; |
|
61 } |
|
62 |
|
63 /** |
|
64 * Excludes start/end text node if they are out side the range |
|
65 * |
|
66 * @private |
|
67 * @param {Array} nodes Nodes to exclude items from. |
|
68 * @return {Array} Array with nodes excluding the start/end container if needed. |
|
69 */ |
|
70 function exclude(nodes) { |
|
71 var node; |
|
72 |
|
73 // First node is excluded |
|
74 node = nodes[0]; |
|
75 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { |
|
76 nodes.splice(0, 1); |
|
77 } |
|
78 |
|
79 // Last node is excluded |
|
80 node = nodes[nodes.length - 1]; |
|
81 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { |
|
82 nodes.splice(nodes.length - 1, 1); |
|
83 } |
|
84 |
|
85 return nodes; |
|
86 } |
|
87 |
|
88 /** |
|
89 * Collects siblings |
|
90 * |
|
91 * @private |
|
92 * @param {Node} node Node to collect siblings from. |
|
93 * @param {String} name Name of the sibling to check for. |
|
94 * @return {Array} Array of collected siblings. |
|
95 */ |
|
96 function collectSiblings(node, name, end_node) { |
|
97 var siblings = []; |
|
98 |
|
99 for (; node && node != end_node; node = node[name]) { |
|
100 siblings.push(node); |
|
101 } |
|
102 |
|
103 return siblings; |
|
104 } |
|
105 |
|
106 /** |
|
107 * Find an end point this is the node just before the common ancestor root. |
|
108 * |
|
109 * @private |
|
110 * @param {Node} node Node to start at. |
|
111 * @param {Node} root Root/ancestor element to stop just before. |
|
112 * @return {Node} Node just before the root element. |
|
113 */ |
|
114 function findEndPoint(node, root) { |
|
115 do { |
|
116 if (node.parentNode == root) { |
|
117 return node; |
|
118 } |
|
119 |
|
120 node = node.parentNode; |
|
121 } while (node); |
|
122 } |
|
123 |
|
124 function walkBoundary(start_node, end_node, next) { |
|
125 var siblingName = next ? 'nextSibling' : 'previousSibling'; |
|
126 |
|
127 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { |
|
128 parent = node.parentNode; |
|
129 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); |
|
130 |
|
131 if (siblings.length) { |
|
132 if (!next) { |
|
133 siblings.reverse(); |
|
134 } |
|
135 |
|
136 callback(exclude(siblings)); |
|
137 } |
|
138 } |
|
139 } |
|
140 |
|
141 // If index based start position then resolve it |
|
142 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { |
|
143 startContainer = startContainer.childNodes[startOffset]; |
|
144 } |
|
145 |
|
146 // If index based end position then resolve it |
|
147 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { |
|
148 endContainer = getEndChild(endContainer, endOffset); |
|
149 } |
|
150 |
|
151 // Same container |
|
152 if (startContainer == endContainer) { |
|
153 return callback(exclude([startContainer])); |
|
154 } |
|
155 |
|
156 // Find common ancestor and end points |
|
157 ancestor = dom.findCommonAncestor(startContainer, endContainer); |
|
158 |
|
159 // Process left side |
|
160 for (node = startContainer; node; node = node.parentNode) { |
|
161 if (node === endContainer) { |
|
162 return walkBoundary(startContainer, ancestor, true); |
|
163 } |
|
164 |
|
165 if (node === ancestor) { |
|
166 break; |
|
167 } |
|
168 } |
|
169 |
|
170 // Process right side |
|
171 for (node = endContainer; node; node = node.parentNode) { |
|
172 if (node === startContainer) { |
|
173 return walkBoundary(endContainer, ancestor); |
|
174 } |
|
175 |
|
176 if (node === ancestor) { |
|
177 break; |
|
178 } |
|
179 } |
|
180 |
|
181 // Find start/end point |
|
182 startPoint = findEndPoint(startContainer, ancestor) || startContainer; |
|
183 endPoint = findEndPoint(endContainer, ancestor) || endContainer; |
|
184 |
|
185 // Walk left leaf |
|
186 walkBoundary(startContainer, startPoint, true); |
|
187 |
|
188 // Walk the middle from start to end point |
|
189 siblings = collectSiblings( |
|
190 startPoint == startContainer ? startPoint : startPoint.nextSibling, |
|
191 'nextSibling', |
|
192 endPoint == endContainer ? endPoint.nextSibling : endPoint |
|
193 ); |
|
194 |
|
195 if (siblings.length) { |
|
196 callback(exclude(siblings)); |
|
197 } |
|
198 |
|
199 // Walk right leaf |
|
200 walkBoundary(endContainer, endPoint); |
|
201 }; |
|
202 |
|
203 /** |
|
204 * Splits the specified range at it's start/end points. |
|
205 * |
|
206 * @private |
|
207 * @param {Range/RangeObject} rng Range to split. |
|
208 * @return {Object} Range position object. |
|
209 */ |
|
210 this.split = function(rng) { |
|
211 var startContainer = rng.startContainer, |
|
212 startOffset = rng.startOffset, |
|
213 endContainer = rng.endContainer, |
|
214 endOffset = rng.endOffset; |
|
215 |
|
216 function splitText(node, offset) { |
|
217 return node.splitText(offset); |
|
218 } |
|
219 |
|
220 // Handle single text node |
|
221 if (startContainer == endContainer && startContainer.nodeType == 3) { |
|
222 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { |
|
223 endContainer = splitText(startContainer, startOffset); |
|
224 startContainer = endContainer.previousSibling; |
|
225 |
|
226 if (endOffset > startOffset) { |
|
227 endOffset = endOffset - startOffset; |
|
228 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; |
|
229 endOffset = endContainer.nodeValue.length; |
|
230 startOffset = 0; |
|
231 } else { |
|
232 endOffset = 0; |
|
233 } |
|
234 } |
|
235 } else { |
|
236 // Split startContainer text node if needed |
|
237 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { |
|
238 startContainer = splitText(startContainer, startOffset); |
|
239 startOffset = 0; |
|
240 } |
|
241 |
|
242 // Split endContainer text node if needed |
|
243 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { |
|
244 endContainer = splitText(endContainer, endOffset).previousSibling; |
|
245 endOffset = endContainer.nodeValue.length; |
|
246 } |
|
247 } |
|
248 |
|
249 return { |
|
250 startContainer: startContainer, |
|
251 startOffset: startOffset, |
|
252 endContainer: endContainer, |
|
253 endOffset: endOffset |
|
254 }; |
|
255 }; |
|
256 |
|
257 /** |
|
258 * Normalizes the specified range by finding the closest best suitable caret location. |
|
259 * |
|
260 * @private |
|
261 * @param {Range} rng Range to normalize. |
|
262 * @return {Boolean} True/false if the specified range was normalized or not. |
|
263 */ |
|
264 this.normalize = function(rng) { |
|
265 var normalized, collapsed; |
|
266 |
|
267 function normalizeEndPoint(start) { |
|
268 var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; |
|
269 var directionLeft, isAfterNode; |
|
270 |
|
271 function hasBrBeforeAfter(node, left) { |
|
272 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); |
|
273 |
|
274 while ((node = walker[left ? 'prev' : 'next']())) { |
|
275 if (node.nodeName === "BR") { |
|
276 return true; |
|
277 } |
|
278 } |
|
279 } |
|
280 |
|
281 function isPrevNode(node, name) { |
|
282 return node.previousSibling && node.previousSibling.nodeName == name; |
|
283 } |
|
284 |
|
285 // Walks the dom left/right to find a suitable text node to move the endpoint into |
|
286 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG |
|
287 function findTextNodeRelative(left, startNode) { |
|
288 var walker, lastInlineElement, parentBlockContainer; |
|
289 |
|
290 startNode = startNode || container; |
|
291 parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; |
|
292 |
|
293 // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 |
|
294 // This: <p><br>|</p> becomes <p>|<br></p> |
|
295 if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { |
|
296 container = startNode.parentNode; |
|
297 offset = dom.nodeIndex(startNode); |
|
298 normalized = true; |
|
299 return; |
|
300 } |
|
301 |
|
302 // Walk left until we hit a text node we can move to or a block/br/img |
|
303 walker = new TreeWalker(startNode, parentBlockContainer); |
|
304 while ((node = walker[left ? 'prev' : 'next']())) { |
|
305 // Break if we hit a non content editable node |
|
306 if (dom.getContentEditableParent(node) === "false") { |
|
307 return; |
|
308 } |
|
309 |
|
310 // Found text node that has a length |
|
311 if (node.nodeType === 3 && node.nodeValue.length > 0) { |
|
312 container = node; |
|
313 offset = left ? node.nodeValue.length : 0; |
|
314 normalized = true; |
|
315 return; |
|
316 } |
|
317 |
|
318 // Break if we find a block or a BR/IMG/INPUT etc |
|
319 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { |
|
320 return; |
|
321 } |
|
322 |
|
323 lastInlineElement = node; |
|
324 } |
|
325 |
|
326 // Only fetch the last inline element when in caret mode for now |
|
327 if (collapsed && lastInlineElement) { |
|
328 container = lastInlineElement; |
|
329 normalized = true; |
|
330 offset = 0; |
|
331 } |
|
332 } |
|
333 |
|
334 container = rng[(start ? 'start' : 'end') + 'Container']; |
|
335 offset = rng[(start ? 'start' : 'end') + 'Offset']; |
|
336 isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; |
|
337 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); |
|
338 directionLeft = start; |
|
339 |
|
340 if (container.nodeType == 1 && offset > container.childNodes.length - 1) { |
|
341 directionLeft = false; |
|
342 } |
|
343 |
|
344 // If the container is a document move it to the body element |
|
345 if (container.nodeType === 9) { |
|
346 container = dom.getRoot(); |
|
347 offset = 0; |
|
348 } |
|
349 |
|
350 // If the container is body try move it into the closest text node or position |
|
351 if (container === body) { |
|
352 // If start is before/after a image, table etc |
|
353 if (directionLeft) { |
|
354 node = container.childNodes[offset > 0 ? offset - 1 : 0]; |
|
355 if (node) { |
|
356 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { |
|
357 return; |
|
358 } |
|
359 } |
|
360 } |
|
361 |
|
362 // Resolve the index |
|
363 if (container.hasChildNodes()) { |
|
364 offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); |
|
365 container = container.childNodes[offset]; |
|
366 offset = 0; |
|
367 |
|
368 // Don't walk into elements that doesn't have any child nodes like a IMG |
|
369 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { |
|
370 // Walk the DOM to find a text node to place the caret at or a BR |
|
371 node = container; |
|
372 walker = new TreeWalker(container, body); |
|
373 |
|
374 do { |
|
375 // Found a text node use that position |
|
376 if (node.nodeType === 3 && node.nodeValue.length > 0) { |
|
377 offset = directionLeft ? 0 : node.nodeValue.length; |
|
378 container = node; |
|
379 normalized = true; |
|
380 break; |
|
381 } |
|
382 |
|
383 // Found a BR/IMG element that we can place the caret before |
|
384 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { |
|
385 offset = dom.nodeIndex(node); |
|
386 container = node.parentNode; |
|
387 |
|
388 // Put caret after image when moving the end point |
|
389 if (node.nodeName == "IMG" && !directionLeft) { |
|
390 offset++; |
|
391 } |
|
392 |
|
393 normalized = true; |
|
394 break; |
|
395 } |
|
396 } while ((node = (directionLeft ? walker.next() : walker.prev()))); |
|
397 } |
|
398 } |
|
399 } |
|
400 |
|
401 // Lean the caret to the left if possible |
|
402 if (collapsed) { |
|
403 // So this: <b>x</b><i>|x</i> |
|
404 // Becomes: <b>x|</b><i>x</i> |
|
405 // Seems that only gecko has issues with this |
|
406 if (container.nodeType === 3 && offset === 0) { |
|
407 findTextNodeRelative(true); |
|
408 } |
|
409 |
|
410 // Lean left into empty inline elements when the caret is before a BR |
|
411 // So this: <i><b></b><i>|<br></i> |
|
412 // Becomes: <i><b>|</b><i><br></i> |
|
413 // Seems that only gecko has issues with this. |
|
414 // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p> |
|
415 if (container.nodeType === 1) { |
|
416 node = container.childNodes[offset]; |
|
417 |
|
418 // Offset is after the containers last child |
|
419 // then use the previous child for normalization |
|
420 if (!node) { |
|
421 node = container.childNodes[offset - 1]; |
|
422 } |
|
423 |
|
424 if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && |
|
425 !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { |
|
426 findTextNodeRelative(true, node); |
|
427 } |
|
428 } |
|
429 } |
|
430 |
|
431 // Lean the start of the selection right if possible |
|
432 // So this: x[<b>x]</b> |
|
433 // Becomes: x<b>[x]</b> |
|
434 if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { |
|
435 findTextNodeRelative(false); |
|
436 } |
|
437 |
|
438 // Set endpoint if it was normalized |
|
439 if (normalized) { |
|
440 rng['set' + (start ? 'Start' : 'End')](container, offset); |
|
441 } |
|
442 } |
|
443 |
|
444 collapsed = rng.collapsed; |
|
445 |
|
446 normalizeEndPoint(true); |
|
447 |
|
448 if (!collapsed) { |
|
449 normalizeEndPoint(); |
|
450 } |
|
451 |
|
452 // If it was collapsed then make sure it still is |
|
453 if (normalized && collapsed) { |
|
454 rng.collapse(true); |
|
455 } |
|
456 |
|
457 return normalized; |
|
458 }; |
|
459 } |
|
460 |
|
461 /** |
|
462 * Compares two ranges and checks if they are equal. |
|
463 * |
|
464 * @static |
|
465 * @method compareRanges |
|
466 * @param {DOMRange} rng1 First range to compare. |
|
467 * @param {DOMRange} rng2 First range to compare. |
|
468 * @return {Boolean} true/false if the ranges are equal. |
|
469 */ |
|
470 RangeUtils.compareRanges = function(rng1, rng2) { |
|
471 if (rng1 && rng2) { |
|
472 // Compare native IE ranges |
|
473 if (rng1.item || rng1.duplicate) { |
|
474 // Both are control ranges and the selected element matches |
|
475 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) { |
|
476 return true; |
|
477 } |
|
478 |
|
479 // Both are text ranges and the range matches |
|
480 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { |
|
481 return true; |
|
482 } |
|
483 } else { |
|
484 // Compare w3c ranges |
|
485 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; |
|
486 } |
|
487 } |
|
488 |
|
489 return false; |
|
490 }; |
|
491 |
|
492 /** |
|
493 * Gets the caret range for the given x/y location. |
|
494 * |
|
495 * @static |
|
496 * @method getCaretRangeFromPoint |
|
497 * @param {Number} x X coordinate for range |
|
498 * @param {Number} y Y coordinate for range |
|
499 * @param {Document} doc Document that x/y are relative to |
|
500 * @returns {Range} caret range |
|
501 */ |
|
502 RangeUtils.getCaretRangeFromPoint = function(x, y, doc) { |
|
503 var rng, point; |
|
504 |
|
505 if (doc.caretPositionFromPoint) { |
|
506 point = doc.caretPositionFromPoint(x, y); |
|
507 rng = doc.createRange(); |
|
508 rng.setStart(point.offsetNode, point.offset); |
|
509 rng.collapse(true); |
|
510 } else if (doc.caretRangeFromPoint) { |
|
511 rng = doc.caretRangeFromPoint(x, y); |
|
512 } else if (doc.body.createTextRange) { |
|
513 rng = doc.body.createTextRange(); |
|
514 |
|
515 try { |
|
516 rng.moveToPoint(x, y); |
|
517 rng.collapse(true); |
|
518 } catch (ex) { |
|
519 // Append to top or bottom depending on drop location |
|
520 rng.collapse(y < doc.body.clientHeight); |
|
521 } |
|
522 } |
|
523 |
|
524 return rng; |
|
525 }; |
|
526 |
|
527 RangeUtils.getNode = function(container, offset) { |
|
528 if (container.nodeType == 1 && container.hasChildNodes()) { |
|
529 if (offset >= container.childNodes.length) { |
|
530 offset = container.childNodes.length - 1; |
|
531 } |
|
532 |
|
533 container = container.childNodes[offset]; |
|
534 } |
|
535 |
|
536 return container; |
|
537 }; |
|
538 |
|
539 return RangeUtils; |
|
540 }); |