68369c27627c899852f5e75f9e8dda99ff668d40
[Packages/TYPO3.CMS.git] / typo3 / sysext / t3editor / jslib / select.js
1 /* Functionality for finding, storing, and re-storing 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 var 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 if (!node){
26 return null;
27 }
28 while (!node.previousSibling && node.parentNode != top){
29 node = node.parentNode;
30 }
31
32 return topLevelNodeAt(node.previousSibling, top);
33 }
34
35 // Most functions are defined in two ways, one for the IE selection
36 // model, one for the W3C one.
37 if (ie_selection) {
38 // Store the current selection in such a way that it can be
39 // restored after we manipulated the DOM tree. For IE, we store
40 // pixel coordinates.
41 select.markSelection = function (win) {
42 var selection = win.document.selection;
43 var start = selection.createRange(), end = start.duplicate();
44 start.collapse(true);
45 end.collapse(false);
46
47 var body = win.document.body;
48 // And we better hope no fool gave this window a padding or a
49 // margin, or all these computations will be in vain.
50 return {start: {x: start.boundingLeft + body.scrollLeft - 1,
51 y: start.boundingTop + body.scrollTop},
52 end: {x: end.boundingLeft + body.scrollLeft - 1,
53 y: end.boundingTop + body.scrollTop},
54 window: win};
55 };
56
57 // Restore a stored selection.
58 select.selectMarked = function(sel) {
59 if (!sel)
60 return;
61 var range1 = sel.window.document.body.createTextRange(), range2 = range1.duplicate();
62 range1.moveToPoint(sel.start.x, sel.start.y);
63 range2.moveToPoint(sel.end.x, sel.end.y);
64 range1.setEndPoint("EndToStart", range2);
65 range1.select();
66 };
67
68 // Not needed in IE model -- see W3C model.
69 select.replaceSelection = function(){};
70
71 // A Cursor object represents a top-level node that the cursor is
72 // currently in or after. It is not possible to reliably get more
73 // detailed information, but just this node is enough for most
74 // purposes.
75 select.Cursor = function(container) {
76 this.container = container;
77 this.doc = container.ownerDocument;
78 var selection = this.doc.selection;
79 this.valid = !!selection;
80 if (this.valid) {
81 var range = selection.createRange();
82 range.collapse(false);
83 var around = range.parentElement();
84 if (around && isAncestor(container, around)) {
85 this.start = topLevelNodeAt(around, container);
86 }
87 else {
88 range.pasteHTML("<span id='// temp //'></span>");
89 var temp = this.doc.getElementById("// temp //");
90 this.start = topLevelNodeBefore(temp, container);
91 if (temp)
92 removeElement(temp);
93 }
94 }
95 };
96
97 // Place the cursor after this.start. This is only useful when
98 // manually moving the cursor instead of restoring it to its old
99 // position.
100 select.Cursor.prototype.focus = function () {
101 var range = this.doc.body.createTextRange();
102 range.moveToElementText(this.start || this.container);
103 range.collapse(!this.start);
104 range.select();
105 };
106
107
108 // Used to normalize the effect of the enter key, since browsers
109 // do widely different things when pressing enter in designMode.
110 select.insertNewlineAtCursor = function(window) {
111 var selection = window.document.selection;
112 if (selection) {
113 var range = selection.createRange();
114 range.pasteHTML("<br/>");
115 range.collapse(false);
116 range.select();
117 }
118 };
119
120 // Insert a custom string at current cursor position (added for t3editor)
121 select.insertTextAtCursor = function(window,text) {
122 var selection = window.document.selection;
123 if (selection) {
124 var range = selection.createRange();
125 range.pasteHTML(text);
126 range.collapse(false);
127 range.select();
128 }
129 };
130
131 }
132 // W3C model
133 else {
134 // Well, Opera isn't even supported at the moment, but it almost
135 // is, and this is used to fix an issue with getting the scroll
136 // position.
137 var opera_scroll = !window.scrollX && !window.scrollY;
138
139 // Store start and end nodes, and offsets within these, and refer
140 // back to the selection object from those nodes, so that this
141 // object can be updated when the nodes are replaced before the
142 // selection is restored.
143 select.markSelection = function (win) {
144 var selection = win.getSelection();
145 if (!selection || selection.rangeCount == 0)
146 return null;
147 var range = selection.getRangeAt(0);
148
149 var result = {start: {node: range.startContainer, offset: range.startOffset},
150 end: {node: range.endContainer, offset: range.endOffset},
151 window: win,
152 scrollX: opera_scroll && win.document.body.scrollLeft,
153 scrollY: opera_scroll && win.document.body.scrollTop};
154
155 // We want the nodes right at the cursor, not one of their
156 // ancestors with a suitable offset. This goes down the DOM tree
157 // until a 'leaf' is reached (or is it *up* the DOM tree?).
158 function normalize(point){
159 while (point.node.nodeType != 3 && point.node.nodeName != "BR") {
160 var newNode = point.node.childNodes[point.offset] || point.node.nextSibling;
161 point.offset = 0;
162 while (!newNode && point.node.parentNode) {
163 point.node = point.node.parentNode;
164 newNode = point.node.nextSibling;
165 }
166 point.node = newNode;
167 if (!newNode)
168 break;
169 }
170 }
171
172 normalize(result.start);
173 normalize(result.end);
174 // Make the links back to the selection object (see
175 // replaceSelection).
176 if (result.start.node)
177 result.start.node.selectStart = result.start;
178 if (result.end.node)
179 result.end.node.selectEnd = result.end;
180
181 return result;
182 };
183
184 // Helper for selecting a range object.
185 function selectRange(range, window) {
186 var selection = window.getSelection();
187 selection.removeAllRanges();
188 selection.addRange(range);
189 };
190
191 select.selectMarked = function (sel) {
192 if (!sel)
193 return;
194 var win = sel.window;
195 var range = win.document.createRange();
196
197 function setPoint(point, which) {
198 if (point.node) {
199 // Remove the link back to the selection.
200 delete point.node["select" + which];
201 // Some magic to generalize the setting of the start and end
202 // of a range.
203 if (point.offset == 0)
204 range["set" + which + "Before"](point.node);
205 else
206 range["set" + which](point.node, point.offset);
207 }
208 else {
209 range.setStartAfter(win.document.body.lastChild || win.document.body);
210 }
211 }
212
213 // Have to restore the scroll position of the frame in Opera.
214 if (opera_scroll){
215 sel.window.document.body.scrollLeft = sel.scrollX;
216 sel.window.document.body.scrollTop = sel.scrollY;
217 }
218 try {
219 setPoint(sel.start, "Start");
220 setPoint(sel.end, "End");
221 selectRange(range, win);
222 } catch(e) {}
223 };
224
225 // This is called by the code in codemirror.js whenever it is
226 // replacing a part of the DOM tree. The function sees whether the
227 // given oldNode is part of the current selection, and updates
228 // this selection if it is. Because nodes are often only partially
229 // replaced, the length of the part that gets replaced has to be
230 // taken into account -- the selection might stay in the oldNode
231 // if the newNode is smaller than the selection's offset. The
232 // offset argument is needed in case the selection does move to
233 // the new object, and the given length is not the whole length of
234 // the new node (part of it might have been used to replace
235 // another node).
236 select.replaceSelection = function(oldNode, newNode, length, offset) {
237 function replace(which) {
238 var selObj = oldNode["select" + which];
239 if (selObj) {
240 if (selObj.offset > length) {
241 selObj.offset -= length;
242 }
243 else {
244 newNode["select" + which] = selObj;
245 delete oldNode["select" + which];
246 selObj.node = newNode;
247 selObj.offset += (offset || 0);
248 }
249 }
250 }
251 replace("Start");
252 replace("End");
253 };
254
255 // Finding the top-level node at the cursor in the W3C is, as you
256 // can see, quite an involved process. [Some of this can probably
257 // be simplified, but I'm afraid to touch it now that it finally
258 // works.]
259 select.Cursor = function(container) {
260 this.container = container;
261 this.win = container.ownerDocument.defaultView;
262 var selection = this.win.getSelection();
263 this.valid = selection && selection.rangeCount > 0;
264 if (this.valid) {
265 var range = selection.getRangeAt(0);
266 var end = range.endContainer;
267 // For text nodes, we look at the node itself if the cursor is
268 // inside, or at the node before it if the cursor is at the
269 // start.
270
271 if (end.nodeType == 3){
272 if (range.endOffset > 0)
273 this.start = topLevelNodeAt(end, this.container);
274 else
275 this.start = topLevelNodeBefore(end, this.container);
276 }
277 // Occasionally, browsers will return the HTML node as
278 // selection (Opera does this all the time, which is the
279 // reason this editor does not work on that browser). If the
280 // offset is 0, we take the start of the frame ('after null'),
281 // otherwise, we take the last node.
282 else if (end.nodeName == "HTML") {
283 this.start = (range.endOffset == 1 ? null : container.lastChild);
284 }
285 // If the given node is our 'container', we just look up the
286 // correct node by using the offset.
287 else if (end == container) {
288 if (range.endOffset == 0)
289 this.start = null;
290 else
291 this.start = end.childNodes[range.endOffset - 1];
292 }
293 // In any other case, we have a regular node. If the cursor is
294 // at the end of the node, we use the node itself, if it is at
295 // the start, we use the node before it, and in any other
296 // case, we look up the child before the cursor and use that.
297 else {
298 if (range.endOffset == end.childNodes.length)
299 this.start = topLevelNodeAt(end, this.container);
300 else if (range.endOffset == 0)
301 this.start = topLevelNodeBefore(end, this.container);
302 else
303 this.start = topLevelNodeAt(end.childNodes[range.endOffset - 1], this.container);
304 }
305 }
306 };
307
308 select.Cursor.prototype.focus = function() {
309 var sel = this.win.getSelection();
310 var range = this.win.document.createRange();
311 range.setStartBefore(this.container.firstChild || this.container);
312 if (this.start)
313 range.setEndAfter(this.start);
314 else
315 range.setEndBefore(this.container.firstChild || this.container);
316
317 range.collapse(false);
318 selectRange(range, this.win);
319 };
320
321
322
323 select.insertNewlineAtCursor = function(window) {
324 var selection = window.getSelection();
325 if (selection && selection.rangeCount > 0) {
326 var range = selection.getRangeAt(0);
327 var br = withDocument(window.document, BR);
328 range.insertNode(br);
329 range.setEndAfter(br);
330 range.collapse(false);
331 selectRange(range, window);
332 }
333 };
334
335 // added for t3editor
336 select.insertTextAtCursor = function(window,text) {
337 var selection = window.getSelection();
338 if (selection && selection.rangeCount > 0) {
339 var range = selection.getRangeAt(0);
340 // var br = withDocument(window.document, BR);
341 textnode = window.document.createTextNode(text);
342 range.insertNode(textnode);
343 range.setEndAfter(textnode);
344 range.collapse(false);
345 selectRange(range, window);
346 }
347 };
348 }
349
350 // Search backwards through the top-level nodes until the next BR or
351 // the start of the frame.
352 select.Cursor.prototype.startOfLine = function() {
353 var start = this.start || this.container.firstChild;
354 while (start && start.nodeName != "BR")
355 start = start.previousSibling;
356 return start;
357 };
358 }());