2014-05-07 13:06:24 +08:00
|
|
|
/**
|
|
|
|
* FocusManager.js
|
|
|
|
*
|
|
|
|
* Copyright, Moxiecode Systems AB
|
|
|
|
* Released under LGPL License.
|
|
|
|
*
|
|
|
|
* License: http://www.tinymce.com/license
|
|
|
|
* Contributing: http://www.tinymce.com/contributing
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This class manages the focus/blur state of the editor. This class is needed since some
|
|
|
|
* browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
|
|
|
|
*
|
|
|
|
* This class will fire two events focus and blur on the editor instances that got affected.
|
|
|
|
* It will also handle the restore of selection when the focus is lost and returned.
|
|
|
|
*
|
|
|
|
* @class tinymce.FocusManager
|
|
|
|
*/
|
|
|
|
define("tinymce/FocusManager", [
|
|
|
|
"tinymce/dom/DOMUtils",
|
|
|
|
"tinymce/Env"
|
|
|
|
], function(DOMUtils, Env) {
|
2015-06-15 18:01:48 +08:00
|
|
|
var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
|
|
|
|
|
2014-05-07 13:06:24 +08:00
|
|
|
/**
|
|
|
|
* Constructs a new focus manager instance.
|
|
|
|
*
|
|
|
|
* @constructor FocusManager
|
|
|
|
* @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
|
|
|
|
*/
|
|
|
|
function FocusManager(editorManager) {
|
|
|
|
function getActiveElement() {
|
|
|
|
try {
|
|
|
|
return document.activeElement;
|
|
|
|
} catch (ex) {
|
|
|
|
// IE sometimes fails to get the activeElement when resizing table
|
|
|
|
// TODO: Investigate this
|
|
|
|
return document.body;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
|
|
|
|
// TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
|
2015-06-15 18:01:48 +08:00
|
|
|
function createBookmark(dom, rng) {
|
2014-05-07 13:06:24 +08:00
|
|
|
if (rng && rng.startContainer) {
|
2015-06-15 18:01:48 +08:00
|
|
|
// Verify that the range is within the root of the editor
|
|
|
|
if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-05-07 13:06:24 +08:00
|
|
|
return {
|
|
|
|
startContainer: rng.startContainer,
|
|
|
|
startOffset: rng.startOffset,
|
|
|
|
endContainer: rng.endContainer,
|
|
|
|
endOffset: rng.endOffset
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return rng;
|
|
|
|
}
|
|
|
|
|
|
|
|
function bookmarkToRng(editor, bookmark) {
|
|
|
|
var rng;
|
|
|
|
|
|
|
|
if (bookmark.startContainer) {
|
|
|
|
rng = editor.getDoc().createRange();
|
|
|
|
rng.setStart(bookmark.startContainer, bookmark.startOffset);
|
|
|
|
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
|
|
|
|
} else {
|
|
|
|
rng = bookmark;
|
|
|
|
}
|
|
|
|
|
|
|
|
return rng;
|
|
|
|
}
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
function isUIElement(elm) {
|
|
|
|
return !!DOM.getParent(elm, FocusManager.isEditorUIElement);
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
function registerEvents(e) {
|
|
|
|
var editor = e.editor;
|
2014-05-07 13:06:24 +08:00
|
|
|
|
|
|
|
editor.on('init', function() {
|
2015-06-15 18:01:48 +08:00
|
|
|
// Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
|
|
|
|
if (editor.inline || Env.ie) {
|
|
|
|
// Use the onbeforedeactivate event when available since it works better see #7023
|
|
|
|
if ("onbeforedeactivate" in document && Env.ie < 9) {
|
|
|
|
editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) {
|
|
|
|
if (e.target != editor.getBody()) {
|
|
|
|
return;
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
try {
|
|
|
|
editor.lastRng = editor.selection.getRng();
|
|
|
|
} catch (ex) {
|
|
|
|
// IE throws "Unexcpected call to method or property access" some times so lets ignore it
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
|
|
|
|
editor.on('nodechange mouseup keyup', function(e) {
|
|
|
|
var node = getActiveElement();
|
|
|
|
|
|
|
|
// Only act on manual nodechanges
|
|
|
|
if (e.type == 'nodechange' && e.selectionChange) {
|
|
|
|
return;
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
// IE 11 reports active element as iframe not body of iframe
|
|
|
|
if (node && node.id == editor.id + '_ifr') {
|
|
|
|
node = editor.getBody();
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
if (editor.dom.isChildOf(node, editor.getBody())) {
|
|
|
|
editor.lastRng = editor.selection.getRng();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
|
|
|
|
// Handles the issue with WebKit not retaining selection within inline document
|
2015-06-15 18:01:48 +08:00
|
|
|
// If the user releases the mouse out side the body since a mouse up event wont occur on the body
|
|
|
|
if (Env.webkit && !selectionChangeHandler) {
|
2014-05-07 13:06:24 +08:00
|
|
|
selectionChangeHandler = function() {
|
2015-06-15 18:01:48 +08:00
|
|
|
var activeEditor = editorManager.activeEditor;
|
|
|
|
|
|
|
|
if (activeEditor && activeEditor.selection) {
|
|
|
|
var rng = activeEditor.selection.getRng();
|
2014-05-07 13:06:24 +08:00
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
// Store when it's non collapsed
|
|
|
|
if (rng && !rng.collapsed) {
|
|
|
|
editor.lastRng = rng;
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
DOM.bind(document, 'selectionchange', selectionChangeHandler);
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
editor.on('setcontent', function() {
|
2015-06-15 18:01:48 +08:00
|
|
|
editor.lastRng = null;
|
2014-05-07 13:06:24 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
// Remove last selection bookmark on mousedown see #6305
|
|
|
|
editor.on('mousedown', function() {
|
|
|
|
editor.selection.lastFocusBookmark = null;
|
|
|
|
});
|
|
|
|
|
|
|
|
editor.on('focusin', function() {
|
|
|
|
var focusedEditor = editorManager.focusedEditor;
|
|
|
|
|
|
|
|
if (editor.selection.lastFocusBookmark) {
|
|
|
|
editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark));
|
|
|
|
editor.selection.lastFocusBookmark = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (focusedEditor != editor) {
|
|
|
|
if (focusedEditor) {
|
|
|
|
focusedEditor.fire('blur', {focusedEditor: editor});
|
|
|
|
}
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
editorManager.setActive(editor);
|
2014-05-07 13:06:24 +08:00
|
|
|
editorManager.focusedEditor = editor;
|
2015-06-15 18:01:48 +08:00
|
|
|
editor.fire('focus', {blurredEditor: focusedEditor});
|
|
|
|
editor.focus(true);
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
editor.lastRng = null;
|
2014-05-07 13:06:24 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
editor.on('focusout', function() {
|
|
|
|
window.setTimeout(function() {
|
|
|
|
var focusedEditor = editorManager.focusedEditor;
|
|
|
|
|
|
|
|
// Still the same editor the the blur was outside any editor UI
|
|
|
|
if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
|
|
|
|
editor.fire('blur', {focusedEditor: null});
|
|
|
|
editorManager.focusedEditor = null;
|
2015-06-15 18:01:48 +08:00
|
|
|
|
|
|
|
// Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
|
|
|
|
if (editor.selection) {
|
|
|
|
editor.selection.lastFocusBookmark = null;
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
}, 0);
|
|
|
|
});
|
2015-06-15 18:01:48 +08:00
|
|
|
|
|
|
|
// Check if focus is moved to an element outside the active editor by checking if the target node
|
|
|
|
// isn't within the body of the activeEditor nor a UI element such as a dialog child control
|
|
|
|
if (!documentFocusInHandler) {
|
|
|
|
documentFocusInHandler = function(e) {
|
|
|
|
var activeEditor = editorManager.activeEditor;
|
|
|
|
|
|
|
|
if (activeEditor && e.target.ownerDocument == document) {
|
|
|
|
// Check to make sure we have a valid selection don't update the bookmark if it's
|
|
|
|
// a focusin to the body of the editor see #7025
|
|
|
|
if (activeEditor.selection && e.target != activeEditor.getBody()) {
|
|
|
|
activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fire a blur event if the element isn't a UI element
|
|
|
|
if (e.target != document.body && !isUIElement(e.target) && editorManager.focusedEditor == activeEditor) {
|
|
|
|
activeEditor.fire('blur', {focusedEditor: null});
|
|
|
|
editorManager.focusedEditor = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
DOM.bind(document, 'focusin', documentFocusInHandler);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle edge case when user starts the selection inside the editor and releases
|
|
|
|
// the mouse outside the editor producing a new selection. This weird workaround is needed since
|
|
|
|
// Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
|
|
|
|
if (editor.inline && !documentMouseUpHandler) {
|
|
|
|
documentMouseUpHandler = function(e) {
|
|
|
|
var activeEditor = editorManager.activeEditor;
|
|
|
|
|
|
|
|
if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) {
|
|
|
|
var rng = activeEditor.selection.getRng();
|
|
|
|
|
|
|
|
if (!rng.collapsed) {
|
|
|
|
activeEditor.lastRng = rng;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
DOM.bind(document, 'mouseup', documentMouseUpHandler);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function unregisterDocumentEvents(e) {
|
|
|
|
if (editorManager.focusedEditor == e.editor) {
|
|
|
|
editorManager.focusedEditor = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!editorManager.activeEditor) {
|
|
|
|
DOM.unbind(document, 'selectionchange', selectionChangeHandler);
|
|
|
|
DOM.unbind(document, 'focusin', documentFocusInHandler);
|
|
|
|
DOM.unbind(document, 'mouseup', documentMouseUpHandler);
|
|
|
|
selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
|
|
|
|
}
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
editorManager.on('AddEditor', registerEvents);
|
2015-06-15 18:01:48 +08:00
|
|
|
editorManager.on('RemoveEditor', unregisterDocumentEvents);
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the specified element is part of the UI for example an button or text input.
|
|
|
|
*
|
|
|
|
* @method isEditorUIElement
|
|
|
|
* @param {Element} elm Element to check if it's part of the UI or not.
|
|
|
|
* @return {Boolean} True/false state if the element is part of the UI or not.
|
|
|
|
*/
|
|
|
|
FocusManager.isEditorUIElement = function(elm) {
|
2015-06-15 18:01:48 +08:00
|
|
|
// Needs to be converted to string since svg can have focus: #6776
|
|
|
|
return elm.className.toString().indexOf('mce-') !== -1;
|
2014-05-07 13:06:24 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
return FocusManager;
|
2015-06-15 18:01:48 +08:00
|
|
|
});
|