0
|
1 /* Functionality for finding, storing, and restoring selections
|
|
2 *
|
|
3 * This does not provide a generic API, just the minimal functionality
|
|
4 * required by the CodeMirror system.
|
|
5 */
|
|
6
|
|
7 // Namespace object.
|
|
8 var select = {};
|
|
9
|
|
10 (function() {
|
|
11 select.ie_selection = document.selection && document.selection.createRangeCollection;
|
|
12
|
|
13 // Find the 'top-level' (defined as 'a direct child of the node
|
|
14 // passed as the top argument') node that the given node is
|
|
15 // contained in. Return null if the given node is not inside the top
|
|
16 // node.
|
|
17 function topLevelNodeAt(node, top) {
|
|
18 while (node && node.parentNode != top)
|
|
19 node = node.parentNode;
|
|
20 return node;
|
|
21 }
|
|
22
|
|
23 // Find the top-level node that contains the node before this one.
|
|
24 function topLevelNodeBefore(node, top) {
|
|
25 while (!node.previousSibling && node.parentNode != top)
|
|
26 node = node.parentNode;
|
|
27 return topLevelNodeAt(node.previousSibling, top);
|
|
28 }
|
|
29
|
|
30 var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
|
|
31
|
|
32 select.scrollToNode = function(node, cursor) {
|
|
33 if (!node) return;
|
|
34 var element = node, body = document.body,
|
|
35 html = document.documentElement,
|
|
36 atEnd = !element.nextSibling || !element.nextSibling.nextSibling
|
|
37 || !element.nextSibling.nextSibling.nextSibling;
|
|
38 // In Opera (and recent Webkit versions), BR elements *always*
|
|
39 // have a offsetTop property of zero.
|
|
40 var compensateHack = 0;
|
|
41 while (element && !element.offsetTop) {
|
|
42 compensateHack++;
|
|
43 element = element.previousSibling;
|
|
44 }
|
|
45 // atEnd is another kludge for these browsers -- if the cursor is
|
|
46 // at the end of the document, and the node doesn't have an
|
|
47 // offset, just scroll to the end.
|
|
48 if (compensateHack == 0) atEnd = false;
|
|
49
|
|
50 // WebKit has a bad habit of (sometimes) happily returning bogus
|
|
51 // offsets when the document has just been changed. This seems to
|
|
52 // always be 5/5, so we don't use those.
|
|
53 if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5)
|
|
54 return;
|
|
55
|
|
56 var y = compensateHack * (element ? element.offsetHeight : 0), x = 0,
|
|
57 width = (node ? node.offsetWidth : 0), pos = element;
|
|
58 while (pos && pos.offsetParent) {
|
|
59 y += pos.offsetTop;
|
|
60 // Don't count X offset for <br> nodes
|
|
61 if (!isBR(pos))
|
|
62 x += pos.offsetLeft;
|
|
63 pos = pos.offsetParent;
|
|
64 }
|
|
65
|
|
66 var scroll_x = body.scrollLeft || html.scrollLeft || 0,
|
|
67 scroll_y = body.scrollTop || html.scrollTop || 0,
|
|
68 scroll = false, screen_width = window.innerWidth || html.clientWidth || 0;
|
|
69
|
|
70 if (cursor || width < screen_width) {
|
|
71 if (cursor) {
|
|
72 var off = select.offsetInNode(node), size = nodeText(node).length;
|
|
73 if (size) x += width * (off / size);
|
|
74 }
|
|
75 var screen_x = x - scroll_x;
|
|
76 if (screen_x < 0 || screen_x > screen_width) {
|
|
77 scroll_x = x;
|
|
78 scroll = true;
|
|
79 }
|
|
80 }
|
|
81 var screen_y = y - scroll_y;
|
|
82 if (screen_y < 0 || atEnd || screen_y > (window.innerHeight || html.clientHeight || 0) - 50) {
|
|
83 scroll_y = atEnd ? 1e6 : y;
|
|
84 scroll = true;
|
|
85 }
|
|
86 if (scroll) window.scrollTo(scroll_x, scroll_y);
|
|
87 };
|
|
88
|
|
89 select.scrollToCursor = function(container) {
|
|
90 select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true);
|
|
91 };
|
|
92
|
|
93 // Used to prevent restoring a selection when we do not need to.
|
|
94 var currentSelection = null;
|
|
95
|
|
96 select.snapshotChanged = function() {
|
|
97 if (currentSelection) currentSelection.changed = true;
|
|
98 };
|
|
99
|
|
100 // Find the 'leaf' node (BR or text) after the given one.
|
|
101 function baseNodeAfter(node) {
|
|
102 var next = node.nextSibling;
|
|
103 if (next) {
|
|
104 while (next.firstChild) next = next.firstChild;
|
|
105 if (next.nodeType == 3 || isBR(next)) return next;
|
|
106 else return baseNodeAfter(next);
|
|
107 }
|
|
108 else {
|
|
109 var parent = node.parentNode;
|
|
110 while (parent && !parent.nextSibling) parent = parent.parentNode;
|
|
111 return parent && baseNodeAfter(parent);
|
|
112 }
|
|
113 }
|
|
114
|
|
115 // This is called by the code in editor.js whenever it is replacing
|
|
116 // a text node. The function sees whether the given oldNode is part
|
|
117 // of the current selection, and updates this selection if it is.
|
|
118 // Because nodes are often only partially replaced, the length of
|
|
119 // the part that gets replaced has to be taken into account -- the
|
|
120 // selection might stay in the oldNode if the newNode is smaller
|
|
121 // than the selection's offset. The offset argument is needed in
|
|
122 // case the selection does move to the new object, and the given
|
|
123 // length is not the whole length of the new node (part of it might
|
|
124 // have been used to replace another node).
|
|
125 select.snapshotReplaceNode = function(from, to, length, offset) {
|
|
126 if (!currentSelection) return;
|
|
127
|
|
128 function replace(point) {
|
|
129 if (from == point.node) {
|
|
130 currentSelection.changed = true;
|
|
131 if (length && point.offset > length) {
|
|
132 point.offset -= length;
|
|
133 }
|
|
134 else {
|
|
135 point.node = to;
|
|
136 point.offset += (offset || 0);
|
|
137 }
|
|
138 }
|
|
139 else if (select.ie_selection && point.offset == 0 && point.node == baseNodeAfter(from)) {
|
|
140 currentSelection.changed = true;
|
|
141 }
|
|
142 }
|
|
143 replace(currentSelection.start);
|
|
144 replace(currentSelection.end);
|
|
145 };
|
|
146
|
|
147 select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
|
|
148 if (!currentSelection) return;
|
|
149
|
|
150 function move(point) {
|
|
151 if (from == point.node && (!ifAtStart || point.offset == 0)) {
|
|
152 currentSelection.changed = true;
|
|
153 point.node = to;
|
|
154 if (relative) point.offset = Math.max(0, point.offset + distance);
|
|
155 else point.offset = distance;
|
|
156 }
|
|
157 }
|
|
158 move(currentSelection.start);
|
|
159 move(currentSelection.end);
|
|
160 };
|
|
161
|
|
162 // Most functions are defined in two ways, one for the IE selection
|
|
163 // model, one for the W3C one.
|
|
164 if (select.ie_selection) {
|
|
165 function selRange() {
|
|
166 var sel = document.selection;
|
|
167 if (!sel) return null;
|
|
168 if (sel.createRange) return sel.createRange();
|
|
169 else return sel.createTextRange();
|
|
170 }
|
|
171
|
|
172 function selectionNode(start) {
|
|
173 var range = selRange();
|
|
174 range.collapse(start);
|
|
175
|
|
176 function nodeAfter(node) {
|
|
177 var found = null;
|
|
178 while (!found && node) {
|
|
179 found = node.nextSibling;
|
|
180 node = node.parentNode;
|
|
181 }
|
|
182 return nodeAtStartOf(found);
|
|
183 }
|
|
184
|
|
185 function nodeAtStartOf(node) {
|
|
186 while (node && node.firstChild) node = node.firstChild;
|
|
187 return {node: node, offset: 0};
|
|
188 }
|
|
189
|
|
190 var containing = range.parentElement();
|
|
191 if (!isAncestor(document.body, containing)) return null;
|
|
192 if (!containing.firstChild) return nodeAtStartOf(containing);
|
|
193
|
|
194 var working = range.duplicate();
|
|
195 working.moveToElementText(containing);
|
|
196 working.collapse(true);
|
|
197 for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
|
|
198 if (cur.nodeType == 3) {
|
|
199 var size = cur.nodeValue.length;
|
|
200 working.move("character", size);
|
|
201 }
|
|
202 else {
|
|
203 working.moveToElementText(cur);
|
|
204 working.collapse(false);
|
|
205 }
|
|
206
|
|
207 var dir = range.compareEndPoints("StartToStart", working);
|
|
208 if (dir == 0) return nodeAfter(cur);
|
|
209 if (dir == 1) continue;
|
|
210 if (cur.nodeType != 3) return nodeAtStartOf(cur);
|
|
211
|
|
212 working.setEndPoint("StartToEnd", range);
|
|
213 return {node: cur, offset: size - working.text.length};
|
|
214 }
|
|
215 return nodeAfter(containing);
|
|
216 }
|
|
217
|
|
218 select.markSelection = function() {
|
|
219 currentSelection = null;
|
|
220 var sel = document.selection;
|
|
221 if (!sel) return;
|
|
222 var start = selectionNode(true),
|
|
223 end = selectionNode(false);
|
|
224 if (!start || !end) return;
|
|
225 currentSelection = {start: start, end: end, changed: false};
|
|
226 };
|
|
227
|
|
228 select.selectMarked = function() {
|
|
229 if (!currentSelection || !currentSelection.changed) return;
|
|
230
|
|
231 function makeRange(point) {
|
|
232 var range = document.body.createTextRange(),
|
|
233 node = point.node;
|
|
234 if (!node) {
|
|
235 range.moveToElementText(document.body);
|
|
236 range.collapse(false);
|
|
237 }
|
|
238 else if (node.nodeType == 3) {
|
|
239 range.moveToElementText(node.parentNode);
|
|
240 var offset = point.offset;
|
|
241 while (node.previousSibling) {
|
|
242 node = node.previousSibling;
|
|
243 offset += (node.innerText || "").length;
|
|
244 }
|
|
245 range.move("character", offset);
|
|
246 }
|
|
247 else {
|
|
248 range.moveToElementText(node);
|
|
249 range.collapse(true);
|
|
250 }
|
|
251 return range;
|
|
252 }
|
|
253
|
|
254 var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
|
|
255 start.setEndPoint("StartToEnd", end);
|
|
256 start.select();
|
|
257 };
|
|
258
|
|
259 select.offsetInNode = function(node) {
|
|
260 var range = selRange();
|
|
261 if (!range) return 0;
|
|
262 var range2 = range.duplicate();
|
|
263 try {range2.moveToElementText(node);} catch(e){return 0;}
|
|
264 range.setEndPoint("StartToStart", range2);
|
|
265 return range.text.length;
|
|
266 };
|
|
267
|
|
268 // Get the top-level node that one end of the cursor is inside or
|
|
269 // after. Note that this returns false for 'no cursor', and null
|
|
270 // for 'start of document'.
|
|
271 select.selectionTopNode = function(container, start) {
|
|
272 var range = selRange();
|
|
273 if (!range) return false;
|
|
274 var range2 = range.duplicate();
|
|
275 range.collapse(start);
|
|
276 var around = range.parentElement();
|
|
277 if (around && isAncestor(container, around)) {
|
|
278 // Only use this node if the selection is not at its start.
|
|
279 range2.moveToElementText(around);
|
|
280 if (range.compareEndPoints("StartToStart", range2) == 1)
|
|
281 return topLevelNodeAt(around, container);
|
|
282 }
|
|
283
|
|
284 // Move the start of a range to the start of a node,
|
|
285 // compensating for the fact that you can't call
|
|
286 // moveToElementText with text nodes.
|
|
287 function moveToNodeStart(range, node) {
|
|
288 if (node.nodeType == 3) {
|
|
289 var count = 0, cur = node.previousSibling;
|
|
290 while (cur && cur.nodeType == 3) {
|
|
291 count += cur.nodeValue.length;
|
|
292 cur = cur.previousSibling;
|
|
293 }
|
|
294 if (cur) {
|
|
295 try{range.moveToElementText(cur);}
|
|
296 catch(e){return false;}
|
|
297 range.collapse(false);
|
|
298 }
|
|
299 else range.moveToElementText(node.parentNode);
|
|
300 if (count) range.move("character", count);
|
|
301 }
|
|
302 else {
|
|
303 try{range.moveToElementText(node);}
|
|
304 catch(e){return false;}
|
|
305 }
|
|
306 return true;
|
|
307 }
|
|
308
|
|
309 // Do a binary search through the container object, comparing
|
|
310 // the start of each node to the selection
|
|
311 var start = 0, end = container.childNodes.length - 1;
|
|
312 while (start < end) {
|
|
313 var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle];
|
|
314 if (!node) return false; // Don't ask. IE6 manages this sometimes.
|
|
315 if (!moveToNodeStart(range2, node)) return false;
|
|
316 if (range.compareEndPoints("StartToStart", range2) == 1)
|
|
317 start = middle;
|
|
318 else
|
|
319 end = middle - 1;
|
|
320 }
|
|
321
|
|
322 if (start == 0) {
|
|
323 var test1 = selRange(), test2 = test1.duplicate();
|
|
324 try {
|
|
325 test2.moveToElementText(container);
|
|
326 } catch(exception) {
|
|
327 return null;
|
|
328 }
|
|
329 if (test1.compareEndPoints("StartToStart", test2) == 0)
|
|
330 return null;
|
|
331 }
|
|
332 return container.childNodes[start] || null;
|
|
333 };
|
|
334
|
|
335 // Place the cursor after this.start. This is only useful when
|
|
336 // manually moving the cursor instead of restoring it to its old
|
|
337 // position.
|
|
338 select.focusAfterNode = function(node, container) {
|
|
339 var range = document.body.createTextRange();
|
|
340 range.moveToElementText(node || container);
|
|
341 range.collapse(!node);
|
|
342 range.select();
|
|
343 };
|
|
344
|
|
345 select.somethingSelected = function() {
|
|
346 var range = selRange();
|
|
347 return range && (range.text != "");
|
|
348 };
|
|
349
|
|
350 function insertAtCursor(html) {
|
|
351 var range = selRange();
|
|
352 if (range) {
|
|
353 range.pasteHTML(html);
|
|
354 range.collapse(false);
|
|
355 range.select();
|
|
356 }
|
|
357 }
|
|
358
|
|
359 // Used to normalize the effect of the enter key, since browsers
|
|
360 // do widely different things when pressing enter in designMode.
|
|
361 select.insertNewlineAtCursor = function() {
|
|
362 insertAtCursor("<br>");
|
|
363 };
|
|
364
|
|
365 select.insertTabAtCursor = function() {
|
|
366 insertAtCursor(fourSpaces);
|
|
367 };
|
|
368
|
|
369 // Get the BR node at the start of the line on which the cursor
|
|
370 // currently is, and the offset into the line. Returns null as
|
|
371 // node if cursor is on first line.
|
|
372 select.cursorPos = function(container, start) {
|
|
373 var range = selRange();
|
|
374 if (!range) return null;
|
|
375
|
|
376 var topNode = select.selectionTopNode(container, start);
|
|
377 while (topNode && !isBR(topNode))
|
|
378 topNode = topNode.previousSibling;
|
|
379
|
|
380 var range2 = range.duplicate();
|
|
381 range.collapse(start);
|
|
382 if (topNode) {
|
|
383 range2.moveToElementText(topNode);
|
|
384 range2.collapse(false);
|
|
385 }
|
|
386 else {
|
|
387 // When nothing is selected, we can get all kinds of funky errors here.
|
|
388 try { range2.moveToElementText(container); }
|
|
389 catch (e) { return null; }
|
|
390 range2.collapse(true);
|
|
391 }
|
|
392 range.setEndPoint("StartToStart", range2);
|
|
393
|
|
394 return {node: topNode, offset: range.text.length};
|
|
395 };
|
|
396
|
|
397 select.setCursorPos = function(container, from, to) {
|
|
398 function rangeAt(pos) {
|
|
399 var range = document.body.createTextRange();
|
|
400 if (!pos.node) {
|
|
401 range.moveToElementText(container);
|
|
402 range.collapse(true);
|
|
403 }
|
|
404 else {
|
|
405 range.moveToElementText(pos.node);
|
|
406 range.collapse(false);
|
|
407 }
|
|
408 range.move("character", pos.offset);
|
|
409 return range;
|
|
410 }
|
|
411
|
|
412 var range = rangeAt(from);
|
|
413 if (to && to != from)
|
|
414 range.setEndPoint("EndToEnd", rangeAt(to));
|
|
415 range.select();
|
|
416 }
|
|
417
|
|
418 // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
|
|
419 select.getBookmark = function (container) {
|
|
420 var from = select.cursorPos(container, true), to = select.cursorPos(container, false);
|
|
421 if (from && to) return {from: from, to: to};
|
|
422 };
|
|
423
|
|
424 // Restore a stored selection.
|
|
425 select.setBookmark = function(container, mark) {
|
|
426 if (!mark) return;
|
|
427 select.setCursorPos(container, mark.from, mark.to);
|
|
428 };
|
|
429 }
|
|
430 // W3C model
|
|
431 else {
|
|
432 // Find the node right at the cursor, not one of its
|
|
433 // ancestors with a suitable offset. This goes down the DOM tree
|
|
434 // until a 'leaf' is reached (or is it *up* the DOM tree?).
|
|
435 function innerNode(node, offset) {
|
|
436 while (node.nodeType != 3 && !isBR(node)) {
|
|
437 var newNode = node.childNodes[offset] || node.nextSibling;
|
|
438 offset = 0;
|
|
439 while (!newNode && node.parentNode) {
|
|
440 node = node.parentNode;
|
|
441 newNode = node.nextSibling;
|
|
442 }
|
|
443 node = newNode;
|
|
444 if (!newNode) break;
|
|
445 }
|
|
446 return {node: node, offset: offset};
|
|
447 }
|
|
448
|
|
449 // Store start and end nodes, and offsets within these, and refer
|
|
450 // back to the selection object from those nodes, so that this
|
|
451 // object can be updated when the nodes are replaced before the
|
|
452 // selection is restored.
|
|
453 select.markSelection = function () {
|
|
454 var selection = window.getSelection();
|
|
455 if (!selection || selection.rangeCount == 0)
|
|
456 return (currentSelection = null);
|
|
457 var range = selection.getRangeAt(0);
|
|
458
|
|
459 currentSelection = {
|
|
460 start: innerNode(range.startContainer, range.startOffset),
|
|
461 end: innerNode(range.endContainer, range.endOffset),
|
|
462 changed: false
|
|
463 };
|
|
464 };
|
|
465
|
|
466 select.selectMarked = function () {
|
|
467 var cs = currentSelection;
|
|
468 // on webkit-based browsers, it is apparently possible that the
|
|
469 // selection gets reset even when a node that is not one of the
|
|
470 // endpoints get messed with. the most common situation where
|
|
471 // this occurs is when a selection is deleted or overwitten. we
|
|
472 // check for that here.
|
|
473 function focusIssue() {
|
|
474 if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) {
|
|
475 var selection = window.getSelection();
|
|
476 if (!selection || selection.rangeCount == 0) return true;
|
|
477 var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset);
|
|
478 return cs.start.node != point.node || cs.start.offset != point.offset;
|
|
479 }
|
|
480 }
|
|
481 if (!cs || !(cs.changed || (webkit && focusIssue()))) return;
|
|
482 var range = document.createRange();
|
|
483
|
|
484 function setPoint(point, which) {
|
|
485 if (point.node) {
|
|
486 // Some magic to generalize the setting of the start and end
|
|
487 // of a range.
|
|
488 if (point.offset == 0)
|
|
489 range["set" + which + "Before"](point.node);
|
|
490 else
|
|
491 range["set" + which](point.node, point.offset);
|
|
492 }
|
|
493 else {
|
|
494 range.setStartAfter(document.body.lastChild || document.body);
|
|
495 }
|
|
496 }
|
|
497
|
|
498 setPoint(cs.end, "End");
|
|
499 setPoint(cs.start, "Start");
|
|
500 selectRange(range);
|
|
501 };
|
|
502
|
|
503 // Helper for selecting a range object.
|
|
504 function selectRange(range) {
|
|
505 var selection = window.getSelection();
|
|
506 if (!selection) return;
|
|
507 selection.removeAllRanges();
|
|
508 selection.addRange(range);
|
|
509 }
|
|
510 function selectionRange() {
|
|
511 var selection = window.getSelection();
|
|
512 if (!selection || selection.rangeCount == 0)
|
|
513 return false;
|
|
514 else
|
|
515 return selection.getRangeAt(0);
|
|
516 }
|
|
517
|
|
518 // Finding the top-level node at the cursor in the W3C is, as you
|
|
519 // can see, quite an involved process.
|
|
520 select.selectionTopNode = function(container, start) {
|
|
521 var range = selectionRange();
|
|
522 if (!range) return false;
|
|
523
|
|
524 var node = start ? range.startContainer : range.endContainer;
|
|
525 var offset = start ? range.startOffset : range.endOffset;
|
|
526 // Work around (yet another) bug in Opera's selection model.
|
|
527 if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
|
|
528 container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset]))
|
|
529 offset--;
|
|
530
|
|
531 // For text nodes, we look at the node itself if the cursor is
|
|
532 // inside, or at the node before it if the cursor is at the
|
|
533 // start.
|
|
534 if (node.nodeType == 3){
|
|
535 if (offset > 0)
|
|
536 return topLevelNodeAt(node, container);
|
|
537 else
|
|
538 return topLevelNodeBefore(node, container);
|
|
539 }
|
|
540 // Occasionally, browsers will return the HTML node as
|
|
541 // selection. If the offset is 0, we take the start of the frame
|
|
542 // ('after null'), otherwise, we take the last node.
|
|
543 else if (node.nodeName.toUpperCase() == "HTML") {
|
|
544 return (offset == 1 ? null : container.lastChild);
|
|
545 }
|
|
546 // If the given node is our 'container', we just look up the
|
|
547 // correct node by using the offset.
|
|
548 else if (node == container) {
|
|
549 return (offset == 0) ? null : node.childNodes[offset - 1];
|
|
550 }
|
|
551 // In any other case, we have a regular node. If the cursor is
|
|
552 // at the end of the node, we use the node itself, if it is at
|
|
553 // the start, we use the node before it, and in any other
|
|
554 // case, we look up the child before the cursor and use that.
|
|
555 else {
|
|
556 if (offset == node.childNodes.length)
|
|
557 return topLevelNodeAt(node, container);
|
|
558 else if (offset == 0)
|
|
559 return topLevelNodeBefore(node, container);
|
|
560 else
|
|
561 return topLevelNodeAt(node.childNodes[offset - 1], container);
|
|
562 }
|
|
563 };
|
|
564
|
|
565 select.focusAfterNode = function(node, container) {
|
|
566 var range = document.createRange();
|
|
567 range.setStartBefore(container.firstChild || container);
|
|
568 // In Opera, setting the end of a range at the end of a line
|
|
569 // (before a BR) will cause the cursor to appear on the next
|
|
570 // line, so we set the end inside of the start node when
|
|
571 // possible.
|
|
572 if (node && !node.firstChild)
|
|
573 range.setEndAfter(node);
|
|
574 else if (node)
|
|
575 range.setEnd(node, node.childNodes.length);
|
|
576 else
|
|
577 range.setEndBefore(container.firstChild || container);
|
|
578 range.collapse(false);
|
|
579 selectRange(range);
|
|
580 };
|
|
581
|
|
582 select.somethingSelected = function() {
|
|
583 var range = selectionRange();
|
|
584 return range && !range.collapsed;
|
|
585 };
|
|
586
|
|
587 select.offsetInNode = function(node) {
|
|
588 var range = selectionRange();
|
|
589 if (!range) return 0;
|
|
590 range = range.cloneRange();
|
|
591 range.setStartBefore(node);
|
|
592 return range.toString().length;
|
|
593 };
|
|
594
|
|
595 select.insertNodeAtCursor = function(node) {
|
|
596 var range = selectionRange();
|
|
597 if (!range) return;
|
|
598
|
|
599 range.deleteContents();
|
|
600 range.insertNode(node);
|
|
601 webkitLastLineHack(document.body);
|
|
602
|
|
603 // work around weirdness where Opera will magically insert a new
|
|
604 // BR node when a BR node inside a span is moved around. makes
|
|
605 // sure the BR ends up outside of spans.
|
|
606 if (window.opera && isBR(node) && isSpan(node.parentNode)) {
|
|
607 var next = node.nextSibling, p = node.parentNode, outer = p.parentNode;
|
|
608 outer.insertBefore(node, p.nextSibling);
|
|
609 var textAfter = "";
|
|
610 for (; next && next.nodeType == 3; next = next.nextSibling) {
|
|
611 textAfter += next.nodeValue;
|
|
612 removeElement(next);
|
|
613 }
|
|
614 outer.insertBefore(makePartSpan(textAfter, document), node.nextSibling);
|
|
615 }
|
|
616 range = document.createRange();
|
|
617 range.selectNode(node);
|
|
618 range.collapse(false);
|
|
619 selectRange(range);
|
|
620 }
|
|
621
|
|
622 select.insertNewlineAtCursor = function() {
|
|
623 select.insertNodeAtCursor(document.createElement("BR"));
|
|
624 };
|
|
625
|
|
626 select.insertTabAtCursor = function() {
|
|
627 select.insertNodeAtCursor(document.createTextNode(fourSpaces));
|
|
628 };
|
|
629
|
|
630 select.cursorPos = function(container, start) {
|
|
631 var range = selectionRange();
|
|
632 if (!range) return;
|
|
633
|
|
634 var topNode = select.selectionTopNode(container, start);
|
|
635 while (topNode && !isBR(topNode))
|
|
636 topNode = topNode.previousSibling;
|
|
637
|
|
638 range = range.cloneRange();
|
|
639 range.collapse(start);
|
|
640 if (topNode)
|
|
641 range.setStartAfter(topNode);
|
|
642 else
|
|
643 range.setStartBefore(container);
|
|
644
|
|
645 var text = range.toString();
|
|
646 return {node: topNode, offset: text.length};
|
|
647 };
|
|
648
|
|
649 select.setCursorPos = function(container, from, to) {
|
|
650 var range = document.createRange();
|
|
651
|
|
652 function setPoint(node, offset, side) {
|
|
653 if (offset == 0 && node && !node.nextSibling) {
|
|
654 range["set" + side + "After"](node);
|
|
655 return true;
|
|
656 }
|
|
657
|
|
658 if (!node)
|
|
659 node = container.firstChild;
|
|
660 else
|
|
661 node = node.nextSibling;
|
|
662
|
|
663 if (!node) return;
|
|
664
|
|
665 if (offset == 0) {
|
|
666 range["set" + side + "Before"](node);
|
|
667 return true;
|
|
668 }
|
|
669
|
|
670 var backlog = []
|
|
671 function decompose(node) {
|
|
672 if (node.nodeType == 3)
|
|
673 backlog.push(node);
|
|
674 else
|
|
675 forEach(node.childNodes, decompose);
|
|
676 }
|
|
677 while (true) {
|
|
678 while (node && !backlog.length) {
|
|
679 decompose(node);
|
|
680 node = node.nextSibling;
|
|
681 }
|
|
682 var cur = backlog.shift();
|
|
683 if (!cur) return false;
|
|
684
|
|
685 var length = cur.nodeValue.length;
|
|
686 if (length >= offset) {
|
|
687 range["set" + side](cur, offset);
|
|
688 return true;
|
|
689 }
|
|
690 offset -= length;
|
|
691 }
|
|
692 }
|
|
693
|
|
694 to = to || from;
|
|
695 if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
|
|
696 selectRange(range);
|
|
697 };
|
|
698 }
|
|
699 })();
|