Skip to content

Commit

Permalink
Implemented 'focusIn,focusOut' events emulation (close #1044) (#1067)
Browse files Browse the repository at this point in the history
  • Loading branch information
miherlosev authored and AndreyBelym committed Mar 27, 2017
1 parent c603244 commit db3ac48
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 140 deletions.
212 changes: 124 additions & 88 deletions src/client/sandbox/event/focus-blur.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ import * as browserUtils from '../../utils/browser';
import * as domUtils from '../../utils/dom';
import * as styleUtils from '../../utils/style';

const INTERNAL_FOCUS_FLAG = 'hammerhead|internal-focus';
const INTERNAL_BLUR_FLAG = 'hammerhead|internal-blur';
const INTERNAL_FOCUS_BLUR_FLAG_PREFIX = 'hammerhead|event|internal-';

const eventsMap = {
bubbles: {
'focus': 'focusin',
'blur': 'focusout'
},
nonBubbles: {
'focusin': 'focus',
'focusout': 'blur'
}
};

export default class FocusBlurSandbox extends SandboxBase {
constructor (listeners, eventSimulator, messageSandbox, shadowUI, timersSandbox, elementEditingWatcher) {
Expand All @@ -17,6 +27,7 @@ export default class FocusBlurSandbox extends SandboxBase {
this.shouldDisableOuterFocusHandlers = false;
this.topWindow = null;
this.lastFocusedElement = null;
this.scrollState = {};

this.eventSimulator = eventSimulator;
this.activeWindowTracker = new ActiveWindowTracker(messageSandbox);
Expand All @@ -38,7 +49,7 @@ export default class FocusBlurSandbox extends SandboxBase {
}

static _restoreElementScroll (el, scroll) {
var newScroll = styleUtils.getElementScroll(el);
const newScroll = styleUtils.getElementScroll(el);

if (newScroll.left !== scroll.left)
styleUtils.setScrollLeft(el, scroll.left);
Expand All @@ -64,97 +75,125 @@ export default class FocusBlurSandbox extends SandboxBase {
this.lastFocusedElement = null;
}

_isEventRaisedOnShadowUIRootInIE9 (el) {
// NOTE: T239149 - TD15.1? - An error occurs during assertion creation on
// http://knockoutjs.com/examples/helloWorld.html in IE9.
return browserUtils.isIE9 && this.shadowUI.getRoot() === el;
}

_shouldUseLabelHtmlForElement (el, type) {
return type === 'focus' && domUtils.isLabelElement(el) && el.htmlFor;
}

_getElementNonScrollableParentsScrollState (el) {
const scrollState = [];
const elementParents = domUtils.getParents(el);

for (const elementParent of elementParents) {
if (styleUtils.get(elementParent, 'overflow') === 'hidden') {
scrollState.push({
element: elementParent,
state: styleUtils.getElementScroll(elementParent)
});
}
}

return scrollState;
}

_restoreElementNonScrollableParentsScrollState (scrollState) {
for (const scrollStateEntry of scrollState)
FocusBlurSandbox._restoreElementScroll(scrollStateEntry.element, scrollStateEntry.state);
}

_saveScrollStateIfNecessary (el, preventScrolling) {
if (preventScrolling)
this.scrollState.windowScroll = styleUtils.getElementScroll(this.window);

if (browserUtils.isIE)
this.scrollState.elementNonScrollableParentsScrollState = this._getElementNonScrollableParentsScrollState(el);
}

_restoreScrollStateIfNecessary (preventScrolling) {
if (preventScrolling)
FocusBlurSandbox._restoreElementScroll(this.window, this.scrollState.windowScroll);

if (browserUtils.isIE)
this._restoreElementNonScrollableParentsScrollState(this.scrollState.elementNonScrollableParentsScrollState);
}

_raiseEvent (el, type, callback, withoutHandlers, isAsync, forMouseEvent, preventScrolling) {
// NOTE: We cannot use Promise because 'resolve' will be called async, but we need to resolve
// immediately in IE9 and IE10.

// NOTE: The focus and blur events should be raised after activeElement is changed (B237489)
// in MSEdge, the focus/blur events are executed synchronously.
var simulateEvent = () => {
const simulateEvent = () => {
// NOTE: The focus and blur events should be raised after activeElement is changed (B237489)
// in MSEdge, the focus/blur events are executed synchronously.
if (browserUtils.isIE && browserUtils.version < 12) {
this.window.setTimeout(() => {
this.window.setTimeout(() => {
if (el[FocusBlurSandbox.getInternalEventFlag(type)])
delete el[FocusBlurSandbox.getInternalEventFlag(type)];
delete el[FocusBlurSandbox.getInternalEventFlag(type)];
}, 0);
}, 0);
}
else if (el[FocusBlurSandbox.getInternalEventFlag(type)])
else
delete el[FocusBlurSandbox.getInternalEventFlag(type)];

const bubblesEventType = eventsMap.bubbles[type];

if (!withoutHandlers) {
if (isAsync)
if (isAsync) {
// NOTE: focusin, focusout events are synchronously
this.eventSimulator[bubblesEventType](el);
this.timersSandbox.deferFunction(() => this.eventSimulator[type](el));
else
}
else if (browserUtils.isMSEdge && type === 'blur') {
this.eventSimulator[bubblesEventType](el);
this.eventSimulator[type](el);
}
else {
this.eventSimulator[type](el);
this.eventSimulator[bubblesEventType](el);
}
}

callback();
};

// NOTE: T239149 - TD15.1? - An error occurs during assertion creation on
// http://knockoutjs.com/examples/helloWorld.html in IE9.
if (browserUtils.isIE9 && this.shadowUI.getRoot() === el && (type === 'focus' || type === 'blur'))
if (this._isEventRaisedOnShadowUIRootInIE9(el))
callback();

if (el[type]) {
// NOTE: We should guarantee that activeElement will be changed, therefore we need to call the native
// focus/blur event. To guarantee that all focus/blur events are raised, we need to raise them manually.
var windowScroll = null;
var elementParents = [];
var nonScrollableParents = [];
var nonScrollableParentsScrollValues = [];

if (preventScrolling)
windowScroll = styleUtils.getElementScroll(this.window);
// NOTE: To guarantee that all focus/blur events are raised, we need to raise them manually.
this._saveScrollStateIfNecessary(el, preventScrolling);

if (browserUtils.isIE) {
elementParents = domUtils.getParents(el);
if (this._shouldUseLabelHtmlForElement(el, type)) {
const htmlForElement = nativeMethods.getElementById.call(domUtils.findDocument(el), el.htmlFor);

for (var i = 0; i < elementParents.length; i++) {
if (styleUtils.get(elementParents[i], 'overflow') === 'hidden') {
nonScrollableParents.push(elementParents[i]);
nonScrollableParentsScrollValues.push(styleUtils.getElementScroll(elementParents[i]));
}
}
}

var tempElement = null;

if (type === 'focus' && domUtils.isLabelElement(el) && el.htmlFor) {
tempElement = nativeMethods.getElementById.call(domUtils.findDocument(el), el.htmlFor);
if (tempElement)
el = tempElement;
if (htmlForElement)
el = htmlForElement;
else {
callback();
return;
}
}

el[FocusBlurSandbox.getInternalEventFlag(type)] = true;

// NOTE: We should guarantee that activeElement will be changed, therefore we need to call the native
// focus/blur event.
FocusBlurSandbox._getNativeMeth(el, type).call(el);
this._restoreScrollStateIfNecessary(preventScrolling);

if (preventScrolling)
FocusBlurSandbox._restoreElementScroll(this.window, windowScroll);

if (browserUtils.isIE && nonScrollableParents.length) {
for (var j = 0, len = nonScrollableParents.length; j < len; j++)
FocusBlurSandbox._restoreElementScroll(nonScrollableParents[j], nonScrollableParentsScrollValues[j]);
}

var curDocument = domUtils.findDocument(el);
var activeElement = domUtils.getActiveElement(curDocument);
const curDocument = domUtils.findDocument(el);
const activeElement = domUtils.getActiveElement(curDocument);

// NOTE: If the element was not focused and has a parent with tabindex, we focus this parent.
var parent = el.parentNode;
var parentWithTabIndex = parent === document ? null : domUtils.closest(parent, '[tabindex]');
const parent = el.parentNode;
const parentWithTabIndex = parent === document ? null : domUtils.closest(parent, '[tabindex]');

if (type === 'focus' && activeElement !== el && parentWithTabIndex && forMouseEvent) {
// NOTE: In WebKit, Safari and MSEdge, calling the native focus event for a parent element
// raises page scrolling. We can't prevent it. Therefore, we need to restore a page scrolling value.
var needPreventScrolling = browserUtils.isWebKit || browserUtils.isSafari || browserUtils.isIE;
const needPreventScrolling = browserUtils.isWebKit || browserUtils.isSafari || browserUtils.isIE;

this._raiseEvent(parentWithTabIndex, 'focus', simulateEvent, false, false, forMouseEvent, needPreventScrolling);
}
Expand All @@ -172,7 +211,11 @@ export default class FocusBlurSandbox extends SandboxBase {
}

static getInternalEventFlag (type) {
return type === 'focus' ? INTERNAL_FOCUS_FLAG : INTERNAL_BLUR_FLAG;
return INTERNAL_FOCUS_BLUR_FLAG_PREFIX + type;
}

static getNonBubblesEventType (bubblesEventType) {
return eventsMap.nonBubbles[bubblesEventType];
}

attach (window) {
Expand All @@ -182,16 +225,16 @@ export default class FocusBlurSandbox extends SandboxBase {
this.topWindow = domUtils.isCrossDomainWindows(window, window.top) ? window : window.top;

this.listeners.addInternalEventListener(window, ['focus', 'blur'], () => {
var activeElement = domUtils.getActiveElement(this.document);
const activeElement = domUtils.getActiveElement(this.document);

this._onChangeActiveElement(activeElement);
});
}

_callFocusCallback (callback, el) {
_raiseSelectionChange (callback, el) {
// NOTE: In MSEdge, the 'selectionchange' event doesn't occur immediately (it occurs with a some delay)
// so we should raise it right after the 'focus' event is raised.
if (browserUtils.isIE && browserUtils.version > 11 && el && domUtils.isTextEditableElement(el))
if (browserUtils.isMSEdge && el && domUtils.isTextEditableElement(el))
this.eventSimulator.selectionchange(el);

if (typeof callback === 'function')
Expand All @@ -204,26 +247,26 @@ export default class FocusBlurSandbox extends SandboxBase {

// NOTE: el.focus() does not raise the event if the element is invisible. If the element is located
// within an invisible iframe, all browsers except Chrome do not raise the event (GH-442)
var raiseEventInIframe = !isNativeFocus || browserUtils.isWebKit || !styleUtils.isElementInInvisibleIframe(el);
var elDocument = (el[INTERNAL_PROPS.processedContext] || this.window).document;
const raiseEventInIframe = !isNativeFocus || browserUtils.isWebKit ||
!styleUtils.isElementInInvisibleIframe(el);
const elDocument = (el[INTERNAL_PROPS.processedContext] || this.window).document;

if (!raiseEventInIframe || isNativeFocus && !styleUtils.isElementVisible(el, elDocument))
return null;

var isElementInIframe = domUtils.isElementInIframe(el);
var iframeElement = isElementInIframe ? domUtils.getIframeByElement(el) : null;
var curDocument = domUtils.findDocument(el);
var isBodyElement = domUtils.isBodyElement(el);
const isElementInIframe = domUtils.isElementInIframe(el);
const iframeElement = isElementInIframe ? domUtils.getIframeByElement(el) : null;
const curDocument = domUtils.findDocument(el);
const isBodyElement = domUtils.isBodyElement(el);
const activeElement = domUtils.getActiveElement();
const activeElementDocument = domUtils.findDocument(activeElement);

var activeElement = domUtils.getActiveElement();
var activeElementDocument = domUtils.findDocument(activeElement);
let withoutHandlers = false;
let needBlur = false;
let needBlurIframe = false;

var withoutHandlers = false;
var needBlur = false;
var needBlurIframe = false;

var isContentEditable = domUtils.isContentEditableElement(el);
var isCurrentWindowActive = this.activeWindowTracker.isCurrentWindowActive();
const isContentEditable = domUtils.isContentEditableElement(el);
const isCurrentWindowActive = this.activeWindowTracker.isCurrentWindowActive();

if (activeElement === el)
withoutHandlers = !(isBodyElement && isContentEditable && !isCurrentWindowActive);
Expand All @@ -232,8 +275,8 @@ export default class FocusBlurSandbox extends SandboxBase {

// NOTE: In IE, if you call focus() or blur() methods from script, an active element is changed immediately,
// but events are raised asynchronously after some timeout.
var isAsync = false;
var raiseFocusEvent = () => {
let isAsync = false;
const raiseFocusEvent = () => {
if (!isCurrentWindowActive && !domUtils.isShadowUIElement(el))
this.activeWindowTracker.makeCurrentWindowActive();

Expand All @@ -243,10 +286,11 @@ export default class FocusBlurSandbox extends SandboxBase {

// NOTE: If we call focus for an unfocusable element (like 'div' or 'image') in iframe, we should
// specify document.active for this iframe manually, so we call focus without handlers.
if (isElementInIframe && iframeElement && domUtils.getActiveElement(this.topWindow.document) !== iframeElement)
this._raiseEvent(iframeElement, 'focus', () => this._callFocusCallback(callback, el), true, isAsync);
if (isElementInIframe && iframeElement &&
domUtils.getActiveElement(this.topWindow.document) !== iframeElement)
this._raiseEvent(iframeElement, 'focus', () => this._raiseSelectionChange(callback, el), true, isAsync);
else
this._callFocusCallback(callback, el);
this._raiseSelectionChange(callback, el);

}, withoutHandlers || silent, isAsync, forMouseEvent, preventScrolling);
};
Expand All @@ -257,7 +301,7 @@ export default class FocusBlurSandbox extends SandboxBase {
if ((this.eventSimulator.isSavedWindowsEventsExists() || browserUtils.version > 10) &&
this.window.event &&
this.window.event.type === 'focus' && this.window.event.srcElement === el) {
this._callFocusCallback(callback);
this._raiseSelectionChange(callback, el);

return null;
}
Expand Down Expand Up @@ -315,20 +359,12 @@ export default class FocusBlurSandbox extends SandboxBase {
return null;
}

disableOuterFocusHandlers () {
this.shouldDisableOuterFocusHandlers = true;
}

enableOuterFocusHandlers () {
this.shouldDisableOuterFocusHandlers = false;
}

blur (el, callback, withoutHandlers, isNativeBlur) {
var activeElement = domUtils.getActiveElement(domUtils.findDocument(el));
const activeElement = domUtils.getActiveElement(domUtils.findDocument(el));
// NOTE: In IE, if you call the focus() or blur() method from script, an active element is changed
// immediately but events are raised asynchronously after some timeout (in MSEdgethe focus/blur methods
// are executed synchronously).
var isAsync = isNativeBlur && browserUtils.isIE && browserUtils.version < 12;
const isAsync = isNativeBlur && browserUtils.isIE && browserUtils.version < 12;

if (activeElement !== el)
withoutHandlers = true;
Expand Down
9 changes: 7 additions & 2 deletions src/client/sandbox/event/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,12 @@ export default class EventSandbox extends SandboxBase {
this.cancelInternalEvents = function (e, dispatched, preventEvent, cancelHandlers, stopPropagation) {
// NOTE: We should cancel events raised by calling the native function (focus, blur) only if the
// element has a flag. If an event is dispatched, we shouldn't cancel it.
var internalEventFlag = FocusBlurSandbox.getInternalEventFlag(e.type);
// After calling a native function two events were raised
// .focus() -> focus, focusin
// .blur() -> blur, focusout
// So we should prevent both events
var eventType = FocusBlurSandbox.getNonBubblesEventType(e.type) || e.type;
var internalEventFlag = FocusBlurSandbox.getInternalEventFlag(eventType);

if (e.target[internalEventFlag] && !e[eventSimulator.DISPATCHED_EVENT_FLAG])
stopPropagation();
Expand Down Expand Up @@ -206,7 +211,7 @@ export default class EventSandbox extends SandboxBase {
this.listeners.initElementListening(window, DOM_EVENTS.concat(['beforeunload', 'pagehide', 'unload', 'message']));

this.listeners.addInternalEventListener(window, ['focus'], this.onFocus);
this.listeners.addInternalEventListener(window, ['focus', 'blur', 'change'], this.cancelInternalEvents);
this.listeners.addInternalEventListener(window, ['focus', 'blur', 'change', 'focusin', 'focusout'], this.cancelInternalEvents);

this.unload.attach(window);
this.message.attach(window);
Expand Down
Loading

0 comments on commit db3ac48

Please sign in to comment.