FocusManager.js 7.7 KB

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