1 /** |
|
2 * FocusManager.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 manages the focus/blur state of the editor. This class is needed since some |
|
13 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. |
|
14 * |
|
15 * This class will fire two events focus and blur on the editor instances that got affected. |
|
16 * It will also handle the restore of selection when the focus is lost and returned. |
|
17 * |
|
18 * @class tinymce.FocusManager |
|
19 */ |
|
20 define("tinymce/FocusManager", [ |
|
21 "tinymce/dom/DOMUtils", |
|
22 "tinymce/Env" |
|
23 ], function(DOMUtils, Env) { |
|
24 var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; |
|
25 |
|
26 /** |
|
27 * Constructs a new focus manager instance. |
|
28 * |
|
29 * @constructor FocusManager |
|
30 * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for. |
|
31 */ |
|
32 function FocusManager(editorManager) { |
|
33 function getActiveElement() { |
|
34 try { |
|
35 return document.activeElement; |
|
36 } catch (ex) { |
|
37 // IE sometimes fails to get the activeElement when resizing table |
|
38 // TODO: Investigate this |
|
39 return document.body; |
|
40 } |
|
41 } |
|
42 |
|
43 // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object |
|
44 // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well. |
|
45 function createBookmark(dom, rng) { |
|
46 if (rng && rng.startContainer) { |
|
47 // Verify that the range is within the root of the editor |
|
48 if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) { |
|
49 return; |
|
50 } |
|
51 |
|
52 return { |
|
53 startContainer: rng.startContainer, |
|
54 startOffset: rng.startOffset, |
|
55 endContainer: rng.endContainer, |
|
56 endOffset: rng.endOffset |
|
57 }; |
|
58 } |
|
59 |
|
60 return rng; |
|
61 } |
|
62 |
|
63 function bookmarkToRng(editor, bookmark) { |
|
64 var rng; |
|
65 |
|
66 if (bookmark.startContainer) { |
|
67 rng = editor.getDoc().createRange(); |
|
68 rng.setStart(bookmark.startContainer, bookmark.startOffset); |
|
69 rng.setEnd(bookmark.endContainer, bookmark.endOffset); |
|
70 } else { |
|
71 rng = bookmark; |
|
72 } |
|
73 |
|
74 return rng; |
|
75 } |
|
76 |
|
77 function isUIElement(elm) { |
|
78 return !!DOM.getParent(elm, FocusManager.isEditorUIElement); |
|
79 } |
|
80 |
|
81 function registerEvents(e) { |
|
82 var editor = e.editor; |
|
83 |
|
84 editor.on('init', function() { |
|
85 // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab |
|
86 if (editor.inline || Env.ie) { |
|
87 // Use the onbeforedeactivate event when available since it works better see #7023 |
|
88 if ("onbeforedeactivate" in document && Env.ie < 9) { |
|
89 editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) { |
|
90 if (e.target != editor.getBody()) { |
|
91 return; |
|
92 } |
|
93 |
|
94 try { |
|
95 editor.lastRng = editor.selection.getRng(); |
|
96 } catch (ex) { |
|
97 // IE throws "Unexcpected call to method or property access" some times so lets ignore it |
|
98 } |
|
99 }); |
|
100 } else { |
|
101 // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes |
|
102 editor.on('nodechange mouseup keyup', function(e) { |
|
103 var node = getActiveElement(); |
|
104 |
|
105 // Only act on manual nodechanges |
|
106 if (e.type == 'nodechange' && e.selectionChange) { |
|
107 return; |
|
108 } |
|
109 |
|
110 // IE 11 reports active element as iframe not body of iframe |
|
111 if (node && node.id == editor.id + '_ifr') { |
|
112 node = editor.getBody(); |
|
113 } |
|
114 |
|
115 if (editor.dom.isChildOf(node, editor.getBody())) { |
|
116 editor.lastRng = editor.selection.getRng(); |
|
117 } |
|
118 }); |
|
119 } |
|
120 |
|
121 // Handles the issue with WebKit not retaining selection within inline document |
|
122 // If the user releases the mouse out side the body since a mouse up event wont occur on the body |
|
123 if (Env.webkit && !selectionChangeHandler) { |
|
124 selectionChangeHandler = function() { |
|
125 var activeEditor = editorManager.activeEditor; |
|
126 |
|
127 if (activeEditor && activeEditor.selection) { |
|
128 var rng = activeEditor.selection.getRng(); |
|
129 |
|
130 // Store when it's non collapsed |
|
131 if (rng && !rng.collapsed) { |
|
132 editor.lastRng = rng; |
|
133 } |
|
134 } |
|
135 }; |
|
136 |
|
137 DOM.bind(document, 'selectionchange', selectionChangeHandler); |
|
138 } |
|
139 } |
|
140 }); |
|
141 |
|
142 editor.on('setcontent', function() { |
|
143 editor.lastRng = null; |
|
144 }); |
|
145 |
|
146 // Remove last selection bookmark on mousedown see #6305 |
|
147 editor.on('mousedown', function() { |
|
148 editor.selection.lastFocusBookmark = null; |
|
149 }); |
|
150 |
|
151 editor.on('focusin', function() { |
|
152 var focusedEditor = editorManager.focusedEditor; |
|
153 |
|
154 if (editor.selection.lastFocusBookmark) { |
|
155 editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark)); |
|
156 editor.selection.lastFocusBookmark = null; |
|
157 } |
|
158 |
|
159 if (focusedEditor != editor) { |
|
160 if (focusedEditor) { |
|
161 focusedEditor.fire('blur', {focusedEditor: editor}); |
|
162 } |
|
163 |
|
164 editorManager.setActive(editor); |
|
165 editorManager.focusedEditor = editor; |
|
166 editor.fire('focus', {blurredEditor: focusedEditor}); |
|
167 editor.focus(true); |
|
168 } |
|
169 |
|
170 editor.lastRng = null; |
|
171 }); |
|
172 |
|
173 editor.on('focusout', function() { |
|
174 window.setTimeout(function() { |
|
175 var focusedEditor = editorManager.focusedEditor; |
|
176 |
|
177 // Still the same editor the the blur was outside any editor UI |
|
178 if (!isUIElement(getActiveElement()) && focusedEditor == editor) { |
|
179 editor.fire('blur', {focusedEditor: null}); |
|
180 editorManager.focusedEditor = null; |
|
181 |
|
182 // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs |
|
183 if (editor.selection) { |
|
184 editor.selection.lastFocusBookmark = null; |
|
185 } |
|
186 } |
|
187 }, 0); |
|
188 }); |
|
189 |
|
190 // Check if focus is moved to an element outside the active editor by checking if the target node |
|
191 // isn't within the body of the activeEditor nor a UI element such as a dialog child control |
|
192 if (!documentFocusInHandler) { |
|
193 documentFocusInHandler = function(e) { |
|
194 var activeEditor = editorManager.activeEditor; |
|
195 |
|
196 if (activeEditor && e.target.ownerDocument == document) { |
|
197 // Check to make sure we have a valid selection don't update the bookmark if it's |
|
198 // a focusin to the body of the editor see #7025 |
|
199 if (activeEditor.selection && e.target != activeEditor.getBody()) { |
|
200 activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng); |
|
201 } |
|
202 |
|
203 // Fire a blur event if the element isn't a UI element |
|
204 if (e.target != document.body && !isUIElement(e.target) && editorManager.focusedEditor == activeEditor) { |
|
205 activeEditor.fire('blur', {focusedEditor: null}); |
|
206 editorManager.focusedEditor = null; |
|
207 } |
|
208 } |
|
209 }; |
|
210 |
|
211 DOM.bind(document, 'focusin', documentFocusInHandler); |
|
212 } |
|
213 |
|
214 // Handle edge case when user starts the selection inside the editor and releases |
|
215 // the mouse outside the editor producing a new selection. This weird workaround is needed since |
|
216 // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843 |
|
217 if (editor.inline && !documentMouseUpHandler) { |
|
218 documentMouseUpHandler = function(e) { |
|
219 var activeEditor = editorManager.activeEditor; |
|
220 |
|
221 if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) { |
|
222 var rng = activeEditor.selection.getRng(); |
|
223 |
|
224 if (!rng.collapsed) { |
|
225 activeEditor.lastRng = rng; |
|
226 } |
|
227 } |
|
228 }; |
|
229 |
|
230 DOM.bind(document, 'mouseup', documentMouseUpHandler); |
|
231 } |
|
232 } |
|
233 |
|
234 function unregisterDocumentEvents(e) { |
|
235 if (editorManager.focusedEditor == e.editor) { |
|
236 editorManager.focusedEditor = null; |
|
237 } |
|
238 |
|
239 if (!editorManager.activeEditor) { |
|
240 DOM.unbind(document, 'selectionchange', selectionChangeHandler); |
|
241 DOM.unbind(document, 'focusin', documentFocusInHandler); |
|
242 DOM.unbind(document, 'mouseup', documentMouseUpHandler); |
|
243 selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; |
|
244 } |
|
245 } |
|
246 |
|
247 editorManager.on('AddEditor', registerEvents); |
|
248 editorManager.on('RemoveEditor', unregisterDocumentEvents); |
|
249 } |
|
250 |
|
251 /** |
|
252 * Returns true if the specified element is part of the UI for example an button or text input. |
|
253 * |
|
254 * @method isEditorUIElement |
|
255 * @param {Element} elm Element to check if it's part of the UI or not. |
|
256 * @return {Boolean} True/false state if the element is part of the UI or not. |
|
257 */ |
|
258 FocusManager.isEditorUIElement = function(elm) { |
|
259 // Needs to be converted to string since svg can have focus: #6776 |
|
260 return elm.className.toString().indexOf('mce-') !== -1; |
|
261 }; |
|
262 |
|
263 return FocusManager; |
|
264 }); |
|