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