|
1 /** |
|
2 * BookmarkManager.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 handles selection bookmarks. |
|
13 * |
|
14 * @class tinymce.dom.BookmarkManager |
|
15 */ |
|
16 define("tinymce/dom/BookmarkManager", [ |
|
17 "tinymce/Env", |
|
18 "tinymce/util/Tools" |
|
19 ], function(Env, Tools) { |
|
20 /** |
|
21 * Constructs a new BookmarkManager instance for a specific selection instance. |
|
22 * |
|
23 * @constructor |
|
24 * @method BookmarkManager |
|
25 * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. |
|
26 */ |
|
27 function BookmarkManager(selection) { |
|
28 var dom = selection.dom; |
|
29 |
|
30 /** |
|
31 * Returns a bookmark location for the current selection. This bookmark object |
|
32 * can then be used to restore the selection after some content modification to the document. |
|
33 * |
|
34 * @method getBookmark |
|
35 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. |
|
36 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. |
|
37 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. |
|
38 * @example |
|
39 * // Stores a bookmark of the current selection |
|
40 * var bm = tinymce.activeEditor.selection.getBookmark(); |
|
41 * |
|
42 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); |
|
43 * |
|
44 * // Restore the selection bookmark |
|
45 * tinymce.activeEditor.selection.moveToBookmark(bm); |
|
46 */ |
|
47 this.getBookmark = function(type, normalized) { |
|
48 var rng, rng2, id, collapsed, name, element, chr = '', styles; |
|
49 |
|
50 function findIndex(name, element) { |
|
51 var index = 0; |
|
52 |
|
53 Tools.each(dom.select(name), function(node, i) { |
|
54 if (node == element) { |
|
55 index = i; |
|
56 } |
|
57 }); |
|
58 |
|
59 return index; |
|
60 } |
|
61 |
|
62 function normalizeTableCellSelection(rng) { |
|
63 function moveEndPoint(start) { |
|
64 var container, offset, childNodes, prefix = start ? 'start' : 'end'; |
|
65 |
|
66 container = rng[prefix + 'Container']; |
|
67 offset = rng[prefix + 'Offset']; |
|
68 |
|
69 if (container.nodeType == 1 && container.nodeName == "TR") { |
|
70 childNodes = container.childNodes; |
|
71 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; |
|
72 if (container) { |
|
73 offset = start ? 0 : container.childNodes.length; |
|
74 rng['set' + (start ? 'Start' : 'End')](container, offset); |
|
75 } |
|
76 } |
|
77 } |
|
78 |
|
79 moveEndPoint(true); |
|
80 moveEndPoint(); |
|
81 |
|
82 return rng; |
|
83 } |
|
84 |
|
85 function getLocation() { |
|
86 var rng = selection.getRng(true), root = dom.getRoot(), bookmark = {}; |
|
87 |
|
88 function getPoint(rng, start) { |
|
89 var container = rng[start ? 'startContainer' : 'endContainer'], |
|
90 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; |
|
91 |
|
92 if (container.nodeType == 3) { |
|
93 if (normalized) { |
|
94 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { |
|
95 offset += node.nodeValue.length; |
|
96 } |
|
97 } |
|
98 |
|
99 point.push(offset); |
|
100 } else { |
|
101 childNodes = container.childNodes; |
|
102 |
|
103 if (offset >= childNodes.length && childNodes.length) { |
|
104 after = 1; |
|
105 offset = Math.max(0, childNodes.length - 1); |
|
106 } |
|
107 |
|
108 point.push(dom.nodeIndex(childNodes[offset], normalized) + after); |
|
109 } |
|
110 |
|
111 for (; container && container != root; container = container.parentNode) { |
|
112 point.push(dom.nodeIndex(container, normalized)); |
|
113 } |
|
114 |
|
115 return point; |
|
116 } |
|
117 |
|
118 bookmark.start = getPoint(rng, true); |
|
119 |
|
120 if (!selection.isCollapsed()) { |
|
121 bookmark.end = getPoint(rng); |
|
122 } |
|
123 |
|
124 return bookmark; |
|
125 } |
|
126 |
|
127 if (type == 2) { |
|
128 element = selection.getNode(); |
|
129 name = element ? element.nodeName : null; |
|
130 |
|
131 if (name == 'IMG') { |
|
132 return {name: name, index: findIndex(name, element)}; |
|
133 } |
|
134 |
|
135 if (selection.tridentSel) { |
|
136 return selection.tridentSel.getBookmark(type); |
|
137 } |
|
138 |
|
139 return getLocation(); |
|
140 } |
|
141 |
|
142 // Handle simple range |
|
143 if (type) { |
|
144 return {rng: selection.getRng()}; |
|
145 } |
|
146 |
|
147 rng = selection.getRng(); |
|
148 id = dom.uniqueId(); |
|
149 collapsed = selection.isCollapsed(); |
|
150 styles = 'overflow:hidden;line-height:0px'; |
|
151 |
|
152 // Explorer method |
|
153 if (rng.duplicate || rng.item) { |
|
154 // Text selection |
|
155 if (!rng.item) { |
|
156 rng2 = rng.duplicate(); |
|
157 |
|
158 try { |
|
159 // Insert start marker |
|
160 rng.collapse(); |
|
161 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); |
|
162 |
|
163 // Insert end marker |
|
164 if (!collapsed) { |
|
165 rng2.collapse(false); |
|
166 |
|
167 // Detect the empty space after block elements in IE and move the |
|
168 // end back one character <p></p>] becomes <p>]</p> |
|
169 rng.moveToElementText(rng2.parentElement()); |
|
170 if (rng.compareEndPoints('StartToEnd', rng2) === 0) { |
|
171 rng2.move('character', -1); |
|
172 } |
|
173 |
|
174 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); |
|
175 } |
|
176 } catch (ex) { |
|
177 // IE might throw unspecified error so lets ignore it |
|
178 return null; |
|
179 } |
|
180 } else { |
|
181 // Control selection |
|
182 element = rng.item(0); |
|
183 name = element.nodeName; |
|
184 |
|
185 return {name: name, index: findIndex(name, element)}; |
|
186 } |
|
187 } else { |
|
188 element = selection.getNode(); |
|
189 name = element.nodeName; |
|
190 if (name == 'IMG') { |
|
191 return {name: name, index: findIndex(name, element)}; |
|
192 } |
|
193 |
|
194 // W3C method |
|
195 rng2 = normalizeTableCellSelection(rng.cloneRange()); |
|
196 |
|
197 // Insert end marker |
|
198 if (!collapsed) { |
|
199 rng2.collapse(false); |
|
200 rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); |
|
201 } |
|
202 |
|
203 rng = normalizeTableCellSelection(rng); |
|
204 rng.collapse(true); |
|
205 rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); |
|
206 } |
|
207 |
|
208 selection.moveToBookmark({id: id, keep: 1}); |
|
209 |
|
210 return {id: id}; |
|
211 }; |
|
212 |
|
213 /** |
|
214 * Restores the selection to the specified bookmark. |
|
215 * |
|
216 * @method moveToBookmark |
|
217 * @param {Object} bookmark Bookmark to restore selection from. |
|
218 * @return {Boolean} true/false if it was successful or not. |
|
219 * @example |
|
220 * // Stores a bookmark of the current selection |
|
221 * var bm = tinymce.activeEditor.selection.getBookmark(); |
|
222 * |
|
223 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); |
|
224 * |
|
225 * // Restore the selection bookmark |
|
226 * tinymce.activeEditor.selection.moveToBookmark(bm); |
|
227 */ |
|
228 this.moveToBookmark = function(bookmark) { |
|
229 var rng, root, startContainer, endContainer, startOffset, endOffset; |
|
230 |
|
231 function setEndPoint(start) { |
|
232 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; |
|
233 |
|
234 if (point) { |
|
235 offset = point[0]; |
|
236 |
|
237 // Find container node |
|
238 for (node = root, i = point.length - 1; i >= 1; i--) { |
|
239 children = node.childNodes; |
|
240 |
|
241 if (point[i] > children.length - 1) { |
|
242 return; |
|
243 } |
|
244 |
|
245 node = children[point[i]]; |
|
246 } |
|
247 |
|
248 // Move text offset to best suitable location |
|
249 if (node.nodeType === 3) { |
|
250 offset = Math.min(point[0], node.nodeValue.length); |
|
251 } |
|
252 |
|
253 // Move element offset to best suitable location |
|
254 if (node.nodeType === 1) { |
|
255 offset = Math.min(point[0], node.childNodes.length); |
|
256 } |
|
257 |
|
258 // Set offset within container node |
|
259 if (start) { |
|
260 rng.setStart(node, offset); |
|
261 } else { |
|
262 rng.setEnd(node, offset); |
|
263 } |
|
264 } |
|
265 |
|
266 return true; |
|
267 } |
|
268 |
|
269 function restoreEndPoint(suffix) { |
|
270 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; |
|
271 |
|
272 if (marker) { |
|
273 node = marker.parentNode; |
|
274 |
|
275 if (suffix == 'start') { |
|
276 if (!keep) { |
|
277 idx = dom.nodeIndex(marker); |
|
278 } else { |
|
279 node = marker.firstChild; |
|
280 idx = 1; |
|
281 } |
|
282 |
|
283 startContainer = endContainer = node; |
|
284 startOffset = endOffset = idx; |
|
285 } else { |
|
286 if (!keep) { |
|
287 idx = dom.nodeIndex(marker); |
|
288 } else { |
|
289 node = marker.firstChild; |
|
290 idx = 1; |
|
291 } |
|
292 |
|
293 endContainer = node; |
|
294 endOffset = idx; |
|
295 } |
|
296 |
|
297 if (!keep) { |
|
298 prev = marker.previousSibling; |
|
299 next = marker.nextSibling; |
|
300 |
|
301 // Remove all marker text nodes |
|
302 Tools.each(Tools.grep(marker.childNodes), function(node) { |
|
303 if (node.nodeType == 3) { |
|
304 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); |
|
305 } |
|
306 }); |
|
307 |
|
308 // Remove marker but keep children if for example contents where inserted into the marker |
|
309 // Also remove duplicated instances of the marker for example by a |
|
310 // split operation or by WebKit auto split on paste feature |
|
311 while ((marker = dom.get(bookmark.id + '_' + suffix))) { |
|
312 dom.remove(marker, 1); |
|
313 } |
|
314 |
|
315 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node |
|
316 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market |
|
317 // isn't worth the effort. Sorry, Opera but it's just a fact |
|
318 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { |
|
319 idx = prev.nodeValue.length; |
|
320 prev.appendData(next.nodeValue); |
|
321 dom.remove(next); |
|
322 |
|
323 if (suffix == 'start') { |
|
324 startContainer = endContainer = prev; |
|
325 startOffset = endOffset = idx; |
|
326 } else { |
|
327 endContainer = prev; |
|
328 endOffset = idx; |
|
329 } |
|
330 } |
|
331 } |
|
332 } |
|
333 } |
|
334 |
|
335 function addBogus(node) { |
|
336 // Adds a bogus BR element for empty block elements |
|
337 if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { |
|
338 node.innerHTML = '<br data-mce-bogus="1" />'; |
|
339 } |
|
340 |
|
341 return node; |
|
342 } |
|
343 |
|
344 if (bookmark) { |
|
345 if (bookmark.start) { |
|
346 rng = dom.createRng(); |
|
347 root = dom.getRoot(); |
|
348 |
|
349 if (selection.tridentSel) { |
|
350 return selection.tridentSel.moveToBookmark(bookmark); |
|
351 } |
|
352 |
|
353 if (setEndPoint(true) && setEndPoint()) { |
|
354 selection.setRng(rng); |
|
355 } |
|
356 } else if (bookmark.id) { |
|
357 // Restore start/end points |
|
358 restoreEndPoint('start'); |
|
359 restoreEndPoint('end'); |
|
360 |
|
361 if (startContainer) { |
|
362 rng = dom.createRng(); |
|
363 rng.setStart(addBogus(startContainer), startOffset); |
|
364 rng.setEnd(addBogus(endContainer), endOffset); |
|
365 selection.setRng(rng); |
|
366 } |
|
367 } else if (bookmark.name) { |
|
368 selection.select(dom.select(bookmark.name)[bookmark.index]); |
|
369 } else if (bookmark.rng) { |
|
370 selection.setRng(bookmark.rng); |
|
371 } |
|
372 } |
|
373 }; |
|
374 } |
|
375 |
|
376 /** |
|
377 * Returns true/false if the specified node is a bookmark node or not. |
|
378 * |
|
379 * @static |
|
380 * @method isBookmarkNode |
|
381 * @param {DOMNode} node DOM Node to check if it's a bookmark node or not. |
|
382 * @return {Boolean} true/false if the node is a bookmark node or not. |
|
383 */ |
|
384 BookmarkManager.isBookmarkNode = function(node) { |
|
385 return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark'; |
|
386 }; |
|
387 |
|
388 return BookmarkManager; |
|
389 }); |