Skip to content

Commit 89cb284

Browse files
committed
Make React selection handling be multi-window aware.
React generally handles being rendered into another window context correctly (we have been doing this for a while in native Mac popovers). The main place where there are global window/document accesses are in places where we deal with the DOM selection (window.getSelection() and document.activeElement). There has been some discussion about this in the public React GitHub repo: facebook/fbjs#188 facebook#7866 facebook#7936 facebook#9184 While this was a good starting point, those proposed changes did not go far enough, since they assumed that React was executing in the top-most window, and the focus was in a child frame (in the same origin). Thus for them it was possible to check document.activeElement in the top window, find which iframe had focus and then recurse into it. In our case, the controller and view frames are siblings, and the top window is in another origin, so we can't use that code path. The main reason why we can't get the current window/document is that ReactInputSelection runs as a transaction wrapper, which doesn't have access to components or DOM nodes (and may run across multiple nodes). To work around this I added a ReactLastActiveThing which keeps track of the last DOM node that we mounted a component into (for the initial render) or the last component that we updated (for re-renders). It's kind of gross, but I couldn't think of any better alternatives. All of the modifications are no-ops when not running inside a frame, so this should have no impact for non-elements uses. I did not update any of the IE8 selection API code paths, we don't support it. (cherry picked from commit 94b759b in the 0.14-stable. Appears to work mostly as is, needed to be updated to take 5c5d2ec into account)
1 parent c5fb46f commit 89cb284

File tree

9 files changed

+210
-10
lines changed

9 files changed

+210
-10
lines changed

src/renderers/dom/client/ReactBrowserEventEmitter.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, {
279279
mountAt
280280
);
281281
} else {
282+
// This an IE8 only code path, we don't care about accesing the
283+
// global window.
282284
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
283285
topLevelTypes.topScroll,
284286
'scroll',
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Copyright 2017 Quip
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ReactCurrentWindow
10+
*/
11+
12+
'use strict';
13+
14+
var ReactDOMComponentTree = require('ReactDOMComponentTree');
15+
var ReactInstanceMap = require('ReactInstanceMap');
16+
var ReactLastActiveThing = require('ReactLastActiveThing');
17+
18+
var getHostComponentFromComposite = require('getHostComponentFromComposite');
19+
20+
var lastCurrentWindow;
21+
22+
function warn(message) {
23+
if (__DEV__) {
24+
console.warn(message);
25+
}
26+
}
27+
28+
function windowFromNode(node) {
29+
if (node.ownerDocument) {
30+
return node.ownerDocument.defaultView ||
31+
node.ownerDocument.parentWindow;
32+
}
33+
return null;
34+
}
35+
36+
function extractCurrentWindow() {
37+
var thing = ReactLastActiveThing.thing;
38+
if (!thing) {
39+
warn('No active thing.');
40+
return null;
41+
}
42+
43+
// We can't use instanceof checks since the object may be from a different
44+
// window and thus have a different constructor (from a different JS
45+
// context).
46+
if (thing.window === thing) {
47+
// Already a window
48+
return thing;
49+
}
50+
51+
if (typeof thing.nodeType !== 'undefined') {
52+
// DOM node
53+
var nodeParentWindow = windowFromNode(thing);
54+
if (nodeParentWindow) {
55+
return nodeParentWindow;
56+
} else {
57+
warn('Could not determine node parent window.');
58+
return null;
59+
}
60+
}
61+
62+
if (thing.getPublicInstance) {
63+
// Component
64+
var component = thing.getPublicInstance();
65+
if (!component) {
66+
warn('Could not get component public instance.');
67+
return null;
68+
}
69+
var inst = ReactInstanceMap.get(component);
70+
if (!inst) {
71+
warn('Component is not in the instance map.');
72+
return null;
73+
}
74+
inst = getHostComponentFromComposite(inst);
75+
if (!inst) {
76+
warn('Cannot get host component.');
77+
return null;
78+
}
79+
var componentNode = ReactDOMComponentTree.getNodeFromInstance(inst);
80+
if (!componentNode) {
81+
warn('Could not get node from component.');
82+
return null;
83+
}
84+
var componentParentWindow = windowFromNode(componentNode);
85+
if (componentParentWindow) {
86+
return componentParentWindow;
87+
}
88+
warn('Could not determine component node parent window.');
89+
return null;
90+
}
91+
92+
warn('Fallthrough, unexpected active thing type');
93+
return null;
94+
}
95+
96+
var ReactCurrentWindow = {
97+
currentWindow: function() {
98+
if (window.top === window) {
99+
// Fast path for non-frame cases.
100+
return window;
101+
}
102+
103+
var currentWindow = extractCurrentWindow();
104+
if (currentWindow) {
105+
lastCurrentWindow = ReactLastActiveThing.thing = currentWindow;
106+
return currentWindow;
107+
}
108+
if (lastCurrentWindow) {
109+
warn('Could not determine current window, using the last value');
110+
return lastCurrentWindow;
111+
}
112+
warn('Could not determine the current window, using the global value');
113+
return window;
114+
},
115+
};
116+
117+
module.exports = ReactCurrentWindow;

src/renderers/dom/client/ReactDOMSelection.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
4040
* @return {object}
4141
*/
4242
function getIEOffsets(node) {
43+
// This an IE8 only code path, we don't care about accesing the global
44+
// window.
4345
var selection = document.selection;
4446
var selectedRange = selection.createRange();
4547
var selectedLength = selectedRange.text.length;
@@ -63,7 +65,8 @@ function getIEOffsets(node) {
6365
* @return {?object}
6466
*/
6567
function getModernOffsets(node) {
66-
var selection = window.getSelection && window.getSelection();
68+
var currentWindow = node.ownerDocument.defaultView;
69+
var selection = currentWindow.getSelection && currentWindow.getSelection();
6770

6871
if (!selection || selection.rangeCount === 0) {
6972
return null;
@@ -119,7 +122,7 @@ function getModernOffsets(node) {
119122
var end = start + rangeLength;
120123

121124
// Detect whether the selection is backward.
122-
var detectionRange = document.createRange();
125+
var detectionRange = node.ownerDocument.createRange();
123126
detectionRange.setStart(anchorNode, anchorOffset);
124127
detectionRange.setEnd(focusNode, focusOffset);
125128
var isBackward = detectionRange.collapsed;
@@ -135,6 +138,8 @@ function getModernOffsets(node) {
135138
* @param {object} offsets
136139
*/
137140
function setIEOffsets(node, offsets) {
141+
// This an IE8 only code path, we don't care about accesing the global
142+
// window.
138143
var range = document.selection.createRange().duplicate();
139144
var start, end;
140145

@@ -169,11 +174,12 @@ function setIEOffsets(node, offsets) {
169174
* @param {object} offsets
170175
*/
171176
function setModernOffsets(node, offsets) {
172-
if (!window.getSelection) {
177+
var currentWindow = node.ownerDocument.defaultView;
178+
if (!currentWindow.getSelection) {
173179
return;
174180
}
175181

176-
var selection = window.getSelection();
182+
var selection = currentWindow.getSelection();
177183
var length = node[getTextContentAccessor()].length;
178184
var start = Math.min(offsets.start, length);
179185
var end = offsets.end === undefined ?
@@ -191,7 +197,7 @@ function setModernOffsets(node, offsets) {
191197
var endMarker = getNodeForCharacterOffset(node, end);
192198

193199
if (startMarker && endMarker) {
194-
var range = document.createRange();
200+
var range = node.ownerDocument.createRange();
195201
range.setStart(startMarker.node, startMarker.offset);
196202
selection.removeAllRanges();
197203

src/renderers/dom/client/ReactInputSelection.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ var ReactDOMSelection = require('ReactDOMSelection');
1515

1616
var containsNode = require('containsNode');
1717
var focusNode = require('focusNode');
18-
var getActiveElement = require('getActiveElement');
18+
var getActiveElement = require('getActiveElementForCurrentWindow');
1919

2020
function isInDocument(node) {
21-
return containsNode(document.documentElement, node);
21+
return node.ownerDocument && containsNode(node.ownerDocument.documentElement, node);
2222
}
2323

2424
/**

src/renderers/dom/client/ReactMount.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var ReactElement = require('ReactElement');
2222
var ReactFeatureFlags = require('ReactFeatureFlags');
2323
var ReactInstanceMap = require('ReactInstanceMap');
2424
var ReactInstrumentation = require('ReactInstrumentation');
25+
var ReactLastActiveThing = require('ReactLastActiveThing');
2526
var ReactMarkupChecksum = require('ReactMarkupChecksum');
2627
var ReactReconciler = require('ReactReconciler');
2728
var ReactUpdateQueue = require('ReactUpdateQueue');
@@ -422,6 +423,7 @@ var ReactMount = {
422423

423424
_renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) {
424425
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
426+
ReactLastActiveThing.thing = container;
425427
invariant(
426428
ReactElement.isValidElement(nextElement),
427429
'ReactDOM.render(): Invalid component element.%s',

src/renderers/dom/client/eventPlugins/SelectEventPlugin.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var ReactDOMComponentTree = require('ReactDOMComponentTree');
1818
var ReactInputSelection = require('ReactInputSelection');
1919
var SyntheticEvent = require('SyntheticEvent');
2020

21-
var getActiveElement = require('getActiveElement');
21+
var getActiveElement = require('getActiveElementForCurrentWindow');
2222
var isTextInputElement = require('isTextInputElement');
2323
var keyOf = require('keyOf');
2424
var shallowEqual = require('shallowEqual');
@@ -70,21 +70,24 @@ var ON_SELECT_KEY = keyOf({onSelect: null});
7070
* @return {object}
7171
*/
7272
function getSelection(node) {
73+
var currentWindow = node.ownerDocument.defaultView;
7374
if ('selectionStart' in node &&
7475
ReactInputSelection.hasSelectionCapabilities(node)) {
7576
return {
7677
start: node.selectionStart,
7778
end: node.selectionEnd,
7879
};
79-
} else if (window.getSelection) {
80-
var selection = window.getSelection();
80+
} else if (currentWindow.getSelection) {
81+
var selection = currentWindow.getSelection();
8182
return {
8283
anchorNode: selection.anchorNode,
8384
anchorOffset: selection.anchorOffset,
8485
focusNode: selection.focusNode,
8586
focusOffset: selection.focusOffset,
8687
};
8788
} else if (document.selection) {
89+
// This an IE8 only code path, we don't care about accesing the global
90+
// window.
8891
var range = document.selection.createRange();
8992
return {
9093
parentElement: range.parentElement(),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright Quip 2017
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule getActiveElementForCurrentWindow
10+
* @typechecks
11+
*/
12+
13+
14+
/**
15+
* Re-implementation of getActiveElement from fbjs that uses ReactCurrentWindow
16+
* to get the active element in the window that the currently executing component
17+
* is rendered into.
18+
*/
19+
'use strict';
20+
21+
var ReactCurrentWindow = require('ReactCurrentWindow');
22+
23+
function getActiveElement() /*?DOMElement*/{
24+
var currentWindow = ReactCurrentWindow.currentWindow();
25+
var document = currentWindow.document;
26+
if (typeof document === 'undefined') {
27+
return null;
28+
}
29+
try {
30+
return document.activeElement || document.body;
31+
} catch (e) {
32+
return document.body;
33+
}
34+
}
35+
36+
module.exports = getActiveElement;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright 2017 Quip
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ReactLastActiveComponent
10+
*/
11+
12+
'use strict';
13+
14+
/**
15+
* Stores a reference to the most recently component DOM container (for the
16+
* initial render) or updated component (for updates). Meant to be used by
17+
* {@code ReactCurrentWindow} to determine the window that components are
18+
* currently being rendered into.
19+
*/
20+
var ReactLastActiveThing = {
21+
/**
22+
* @type {Window|DOMElement|ReactComponent|null}
23+
*/
24+
thing: null,
25+
26+
};
27+
28+
module.exports = ReactLastActiveThing;

src/renderers/shared/stack/reconciler/ReactUpdates.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
var CallbackQueue = require('CallbackQueue');
1515
var PooledClass = require('PooledClass');
1616
var ReactFeatureFlags = require('ReactFeatureFlags');
17+
var ReactLastActiveThing = require('ReactLastActiveThing');
1718
var ReactReconciler = require('ReactReconciler');
1819
var Transaction = require('Transaction');
1920

@@ -150,6 +151,8 @@ function runBatchedUpdates(transaction) {
150151
// that performUpdateIfNecessary is a noop.
151152
var component = dirtyComponents[i];
152153

154+
ReactLastActiveThing.thing = component;
155+
153156
// If performUpdateIfNecessary happens to enqueue any new updates, we
154157
// shouldn't execute the callbacks until the next render happens, so
155158
// stash the callbacks first
@@ -198,6 +201,7 @@ var flushBatchedUpdates = function() {
198201
// updates enqueued by setState callbacks and asap calls.
199202
while (dirtyComponents.length || asapEnqueued) {
200203
if (dirtyComponents.length) {
204+
ReactLastActiveThing.thing = dirtyComponents[0];
201205
var transaction = ReactUpdatesFlushTransaction.getPooled();
202206
transaction.perform(runBatchedUpdates, null, transaction);
203207
ReactUpdatesFlushTransaction.release(transaction);
@@ -218,6 +222,8 @@ var flushBatchedUpdates = function() {
218222
* list of functions which will be executed once the rerender occurs.
219223
*/
220224
function enqueueUpdate(component) {
225+
ReactLastActiveThing.thing = component;
226+
221227
ensureInjected();
222228

223229
// Various parts of our code (such as ReactCompositeComponent's

0 commit comments

Comments
 (0)