5292224ef98bcc500f07c8d22d951b479a6ea222
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / DOM / BookMark.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13 /***************************************************
14 * HTMLArea.DOM.BookMark: BookMark object
15 ***************************************************/
16 define('TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/BookMark',
17 ['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM'],
20 function (UserAgent, Util, Dom) {
21
22 /**
23 * Constructor method
24 *
25 * @param object config: an object with property "editor" giving reference to the parent object
26 *
27 * @return void
28 */
29 var BookMark = function (config) {
30
31 /**
32 * Reference to the editor MUST be set in config
33 */
34 this.editor = null;
35
36 Util.apply(this, config);
37
38 /**
39 * Reference to the editor document
40 */
41 this.document = this.editor.document;
42
43 /**
44 * Reference to the editor selection object
45 */
46 this.selection = this.editor.getSelection();
47 };
48
49 /**
50 * Get a bookMark
51 *
52 * @param object range: the range to bookMark
53 * @param boolean nonIntrusive: if true, a non-intrusive bookmark is requested
54 *
55 * @return object the bookMark
56 */
57 BookMark.prototype.get = function (range, nonIntrusive) {
58 var bookMark;
59 if (UserAgent.isIEBeforeIE9) {
60 // Bookmarking will not work on control ranges
61 try {
62 bookMark = range.getBookmark();
63 } catch (e) {
64 bookMark = null;
65 }
66 } else {
67 if (nonIntrusive) {
68 bookMark = this.getNonIntrusiveBookMark(range, true);
69 } else {
70 bookMark = this.getIntrusiveBookMark(range);
71 }
72 }
73 return bookMark;
74 };
75
76 /**
77 * Get an intrusive bookMark
78 * Adapted from FCKeditor
79 * This is an "intrusive" way to create a bookMark. It includes <span> tags
80 * in the range boundaries. The advantage of it is that it is possible to
81 * handle DOM mutations when moving back to the bookMark.
82 *
83 * @param object range: the range to bookMark
84 *
85 * @return object the bookMark
86 */
87 BookMark.prototype.getIntrusiveBookMark = function (range) {
88 // Create the bookmark info (random IDs).
89 var bookMark = {
90 nonIntrusive: false,
91 startId: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'S',
92 endId: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'E'
93 };
94 var startSpan;
95 var endSpan;
96 var rangeClone = range.cloneRange();
97 // For collapsed ranges, add just the start marker
98 if (!range.collapsed ) {
99 endSpan = this.document.createElement('span');
100 endSpan.style.display = 'none';
101 endSpan.id = bookMark.endId;
102 endSpan.setAttribute('data-htmlarea-bookmark', true);
103 endSpan.innerHTML = '&nbsp;';
104 rangeClone.collapse(false);
105 rangeClone.insertNode(endSpan);
106 }
107 startSpan = this.document.createElement('span');
108 startSpan.style.display = 'none';
109 startSpan.id = bookMark.startId;
110 startSpan.setAttribute('data-htmlarea-bookmark', true);
111 startSpan.innerHTML = '&nbsp;';
112 var rangeClone = range.cloneRange();
113 rangeClone.collapse(true);
114 rangeClone.insertNode(startSpan);
115 bookMark.startNode = startSpan;
116 bookMark.endNode = endSpan;
117 // Update the range position.
118 if (endSpan) {
119 range.setEndBefore(endSpan);
120 range.setStartAfter(startSpan);
121 } else {
122 range.setEndAfter(startSpan);
123 range.collapse(false);
124 }
125 return bookMark;
126 };
127
128 /**
129 * Get a non-intrusive bookMark
130 * Adapted from FCKeditor
131 *
132 * @param object range: the range to bookMark
133 * @param boolean normalized: if true, normalized enpoints are calculated
134 *
135 * @return object the bookMark
136 */
137 BookMark.prototype.getNonIntrusiveBookMark = function (range, normalized) {
138 var startContainer = range.startContainer,
139 endContainer = range.endContainer,
140 startOffset = range.startOffset,
141 endOffset = range.endOffset,
142 collapsed = range.collapsed,
143 child,
144 previous,
145 bookMark = {};
146 if (!startContainer || !endContainer) {
147 bookMark = {
148 nonIntrusive: true,
149 start: 0,
150 end: 0
151 };
152 } else {
153 if (normalized) {
154 // Find out if the start is pointing to a text node that might be normalized
155 if (startContainer.nodeType == Dom.NODE_ELEMENT) {
156 child = startContainer.childNodes[startOffset];
157 // In this case, move the start to that text node
158 if (
159 child
160 && child.nodeType == Dom.NODE_TEXT
161 && startOffset > 0
162 && child.previousSibling.nodeType == Dom.NODE_TEXT
163 ) {
164 startContainer = child;
165 startOffset = 0;
166 }
167 // Get the normalized offset
168 if (child && child.nodeType == Dom.NODE_ELEMENT) {
169 startOffset = Dom.getPositionWithinParent(child, true);
170 }
171 }
172 // Normalize the start
173 while (
174 startContainer.nodeType == Dom.NODE_TEXT
175 && (previous = startContainer.previousSibling)
176 && previous.nodeType == Dom.NODE_TEXT
177 ) {
178 startContainer = previous;
179 startOffset += previous.nodeValue.length;
180 }
181 // Process the end only if not collapsed
182 if (!collapsed) {
183 // Find out if the start is pointing to a text node that will be normalized
184 if (endContainer.nodeType == Dom.NODE_ELEMENT) {
185 child = endContainer.childNodes[endOffset];
186 // In this case, move the end to that text node
187 if (
188 child
189 && child.nodeType == Dom.NODE_TEXT
190 && endOffset > 0
191 && child.previousSibling.nodeType == Dom.NODE_TEXT
192 ) {
193 endContainer = child;
194 endOffset = 0;
195 }
196 // Get the normalized offset
197 if (child && child.nodeType == Dom.NODE_ELEMENT) {
198 endOffset = Dom.getPositionWithinParent(child, true);
199 }
200 }
201 // Normalize the end
202 while (
203 endContainer.nodeType == Dom.NODE_TEXT
204 && (previous = endContainer.previousSibling)
205 && previous.nodeType == Dom.NODE_TEXT
206 ) {
207 endContainer = previous;
208 endOffset += previous.nodeValue.length;
209 }
210 }
211 }
212 bookMark = {
213 start: this.editor.getDomNode().getPositionWithinTree(startContainer, normalized),
214 end: collapsed ? null : this.editor.getDomNode().getPositionWithinTree(endContainer, normalized),
215 startOffset: startOffset,
216 endOffset: endOffset,
217 normalized: normalized,
218 collapsed: collapsed,
219 nonIntrusive: true
220 };
221 }
222 return bookMark;
223 };
224
225 /**
226 * Get the end point of the bookMark
227 * Adapted from FCKeditor
228 *
229 * @param object bookMark: the bookMark
230 * @param boolean endPoint: true, for startPoint, false for endPoint
231 *
232 * @return object the endPoint node
233 */
234 BookMark.prototype.getEndPoint = function (bookMark, endPoint) {
235 if (endPoint) {
236 return this.document.getElementById(bookMark.startId);
237 } else {
238 return this.document.getElementById(bookMark.endId);
239 }
240 };
241
242 /**
243 * Get a range and move it to the bookMark
244 *
245 * @param object bookMark: the bookmark to move to
246 *
247 * @return object the range that was bookmarked
248 */
249 BookMark.prototype.moveTo = function (bookMark) {
250 var range = this.selection.createRange();
251 if (UserAgent.isIEBeforeIE9) {
252 if (bookMark) {
253 range.moveToBookmark(bookMark);
254 }
255 } else {
256 if (bookMark.nonIntrusive) {
257 range = this.moveToNonIntrusiveBookMark(range, bookMark);
258 } else {
259 range = this.moveToIntrusiveBookMark(range, bookMark);
260 }
261 }
262 return range;
263 };
264
265 /**
266 * Move the range to the intrusive bookMark
267 * Adapted from FCKeditor
268 *
269 * @param object range: the range to be moved
270 * @param object bookMark: the bookmark to move to
271 *
272 * @return object the range that was bookmarked
273 */
274 BookMark.prototype.moveToIntrusiveBookMark = function (range, bookMark) {
275 var startSpan = this.getEndPoint(bookMark, true),
276 endSpan = this.getEndPoint(bookMark, false),
277 parent;
278 if (startSpan) {
279 // If the previous sibling is a text node, let the anchorNode have it as parent
280 if (startSpan.previousSibling && startSpan.previousSibling.nodeType === Dom.TEXT_NODE) {
281 range.setStart(startSpan.previousSibling, startSpan.previousSibling.data.length);
282 } else {
283 range.setStartBefore(startSpan);
284 }
285 Dom.removeFromParent(startSpan);
286 } else {
287 // For some reason, the startSpan was removed or its id attribute was removed so that it cannot be retrieved
288 range.setStart(this.document.body, 0);
289 }
290 // If the bookmarked range was collapsed, the end span will not be available
291 if (endSpan) {
292 // If the next sibling is a text node, let the focusNode have it as parent
293 if (endSpan.nextSibling && endSpan.nextSibling.nodeType === Dom.TEXT_NODE) {
294 range.setEnd(endSpan.nextSibling, 0);
295 } else {
296 range.setEndBefore(endSpan);
297 }
298 Dom.removeFromParent(endSpan);
299 } else {
300 range.collapse(true);
301 }
302 return range;
303 };
304
305 /**
306 * Move the range to the non-intrusive bookMark
307 * Adapted from FCKeditor
308 *
309 * @param object range: the range to be moved
310 * @param object bookMark: the bookMark to move to
311 *
312 * @return object the range that was bookmarked
313 */
314 BookMark.prototype.moveToNonIntrusiveBookMark = function (range, bookMark) {
315 if (bookMark.start) {
316 // Get the start information
317 var startContainer = this.editor.getDomNode().getNodeByPosition(bookMark.start, bookMark.normalized),
318 startOffset = bookMark.startOffset;
319 // Set the start boundary
320 range.setStart(startContainer, startOffset);
321 // Get the end information
322 var endContainer = bookMark.end && this.editor.getDomNode().getNodeByPosition(bookMark.end, bookMark.normalized),
323 endOffset = bookMark.endOffset;
324 // Set the end boundary. If not available, collapse the range
325 if (endContainer) {
326 range.setEnd(endContainer, endOffset);
327 } else {
328 range.collapse(true);
329 }
330 }
331 return range;
332 };
333
334 return BookMark;
335
336 });