From 6940e33b6977d0a6839d4322d98bb9709dcfdf73 Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Wed, 17 Jan 2018 06:34:35 -0800 Subject: [PATCH 01/24] =?UTF-8?q?Prefer=20node=E2=80=99s=20window=20and=20?= =?UTF-8?q?document=20over=20globals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react-dom/src/client/ReactDOMSelection.js | 14 ++++-- .../react-dom/src/events/SelectEventPlugin.js | 48 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMSelection.js b/packages/react-dom/src/client/ReactDOMSelection.js index 2fe966df8216a..767894a43697e 100644 --- a/packages/react-dom/src/client/ReactDOMSelection.js +++ b/packages/react-dom/src/client/ReactDOMSelection.js @@ -14,7 +14,11 @@ import {TEXT_NODE} from '../shared/HTMLNodeType'; * @return {?object} */ export function getOffsets(outerNode) { - const selection = window.getSelection && window.getSelection(); + let win = window; + if (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) { + win = outerNode.ownerDocument.defaultView; + } + const selection = win.getSelection && win.getSelection(); if (!selection || selection.rangeCount === 0) { return null; @@ -150,11 +154,13 @@ export function getModernOffsetsFromPoints( * @param {object} offsets */ export function setOffsets(node, offsets) { - if (!window.getSelection) { + const doc = node.ownerDocument || document; + + if (!doc.defaultView.getSelection) { return; } - const selection = window.getSelection(); + const selection = doc.defaultView.getSelection(); const length = node[getTextContentAccessor()].length; let start = Math.min(offsets.start, length); let end = offsets.end === undefined ? start : Math.min(offsets.end, length); @@ -180,7 +186,7 @@ export function setOffsets(node, offsets) { ) { return; } - const range = document.createRange(); + const range = doc.createRange(); range.setStart(startMarker.node, startMarker.offset); selection.removeAllRanges(); diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index e6193c7973a8c..3f24870e471af 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -64,21 +64,42 @@ function getSelection(node) { start: node.selectionStart, end: node.selectionEnd, }; - } else if (window.getSelection) { - const selection = window.getSelection(); - return { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, - }; + } else { + let win = window; + if (node.ownerDocument && node.ownerDocument.defaultView) { + win = node.ownerDocument.defaultView; + } + if (win.getSelection) { + const selection = win.getSelection(); + return { + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, + }; + } } } +/** + * Get document associated with the event target. + * + * @param {object} nativeEventTarget + * @return {Document} + */ +function getEventTargetDocument(eventTarget) { + return eventTarget.window === eventTarget + ? eventTarget.document + : eventTarget.nodeType === DOCUMENT_NODE + ? eventTarget + : eventTarget.ownerDocument; +} + /** * Poll selection to see whether it's changed. * * @param {object} nativeEvent + * @param {object} nativeEventTarget * @return {?SyntheticEvent} */ function constructSelectEvent(nativeEvent, nativeEventTarget) { @@ -86,10 +107,12 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { // selection (this matches native `select` event behavior). In HTML5, select // fires only on input and textarea thus if there's no focused element we // won't dispatch. + const doc = getEventTargetDocument(nativeEventTarget); + if ( mouseDown || activeElement == null || - activeElement !== getActiveElement() + activeElement !== getActiveElement(doc) ) { return null; } @@ -140,12 +163,7 @@ const SelectEventPlugin = { nativeEvent, nativeEventTarget, ) { - const doc = - nativeEventTarget.window === nativeEventTarget - ? nativeEventTarget.document - : nativeEventTarget.nodeType === DOCUMENT_NODE - ? nativeEventTarget - : nativeEventTarget.ownerDocument; + const doc = getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. if (!doc || !isListeningToAllDependencies('onSelect', doc)) { From 3f3c1d7f01442108f37db3b9080460c7c921eaec Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Wed, 17 Jan 2018 08:14:00 -0800 Subject: [PATCH 02/24] Support active elements in nested browsing contexts --- .../src/client/ReactInputSelection.js | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js index 506153a118053..51376ca02e9b2 100644 --- a/packages/react-dom/src/client/ReactInputSelection.js +++ b/packages/react-dom/src/client/ReactInputSelection.js @@ -12,7 +12,25 @@ import * as ReactDOMSelection from './ReactDOMSelection'; import {ELEMENT_NODE} from '../shared/HTMLNodeType'; function isInDocument(node) { - return containsNode(document.documentElement, node); + return ( + node && + node.ownerDocument && + containsNode(node.ownerDocument.documentElement, node) + ); +} + +function getActiveElementDeep() { + let win = window; + let element = getActiveElement(); + while (element instanceof win.HTMLIFrameElement) { + try { + win = element.contentDocument.defaultView; + } catch (e) { + return element; + } + element = getActiveElement(win.document); + } + return element; } /** @@ -33,7 +51,7 @@ export function hasSelectionCapabilities(elem) { } export function getSelectionInformation() { - const focusedElem = getActiveElement(); + const focusedElem = getActiveElementDeep(); return { focusedElem: focusedElem, selectionRange: hasSelectionCapabilities(focusedElem) @@ -48,7 +66,7 @@ export function getSelectionInformation() { * nodes and place them back in, resulting in focus being lost. */ export function restoreSelection(priorSelectionInformation) { - const curFocusedElem = getActiveElement(); + const curFocusedElem = getActiveElementDeep(); const priorFocusedElem = priorSelectionInformation.focusedElem; const priorSelectionRange = priorSelectionInformation.selectionRange; if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) { From 51be426d3d778558282abf6364e04f3f0a591e64 Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Tue, 23 Jan 2018 07:11:54 -0800 Subject: [PATCH 03/24] Avoid invoking defaultView getter unnecessarily --- packages/react-dom/src/client/ReactDOMSelection.js | 12 +++++------- packages/react-dom/src/events/SelectEventPlugin.js | 6 ++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMSelection.js b/packages/react-dom/src/client/ReactDOMSelection.js index 767894a43697e..078316652f0a7 100644 --- a/packages/react-dom/src/client/ReactDOMSelection.js +++ b/packages/react-dom/src/client/ReactDOMSelection.js @@ -14,10 +14,8 @@ import {TEXT_NODE} from '../shared/HTMLNodeType'; * @return {?object} */ export function getOffsets(outerNode) { - let win = window; - if (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) { - win = outerNode.ownerDocument.defaultView; - } + const win = + (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) || window; const selection = win.getSelection && win.getSelection(); if (!selection || selection.rangeCount === 0) { @@ -155,12 +153,12 @@ export function getModernOffsetsFromPoints( */ export function setOffsets(node, offsets) { const doc = node.ownerDocument || document; - - if (!doc.defaultView.getSelection) { + const win = doc ? doc.defaultView : window; + if (!win.getSelection) { return; } - const selection = doc.defaultView.getSelection(); + const selection = win.getSelection(); const length = node[getTextContentAccessor()].length; let start = Math.min(offsets.start, length); let end = offsets.end === undefined ? start : Math.min(offsets.end, length); diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index 3f24870e471af..f04a4522ce5c7 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -65,10 +65,8 @@ function getSelection(node) { end: node.selectionEnd, }; } else { - let win = window; - if (node.ownerDocument && node.ownerDocument.defaultView) { - win = node.ownerDocument.defaultView; - } + const win = + (node.ownerDocument && node.ownerDocument.defaultView) || window; if (win.getSelection) { const selection = win.getSelection(); return { From 37140671193c79e3cfd59b3a355b625185dc8d55 Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Wed, 17 Jan 2018 06:34:35 -0800 Subject: [PATCH 04/24] =?UTF-8?q?Prefer=20node=E2=80=99s=20window=20and=20?= =?UTF-8?q?document=20over=20globals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react-dom/src/client/ReactDOMSelection.js | 14 ++++-- .../react-dom/src/events/SelectEventPlugin.js | 48 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMSelection.js b/packages/react-dom/src/client/ReactDOMSelection.js index 2fe966df8216a..767894a43697e 100644 --- a/packages/react-dom/src/client/ReactDOMSelection.js +++ b/packages/react-dom/src/client/ReactDOMSelection.js @@ -14,7 +14,11 @@ import {TEXT_NODE} from '../shared/HTMLNodeType'; * @return {?object} */ export function getOffsets(outerNode) { - const selection = window.getSelection && window.getSelection(); + let win = window; + if (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) { + win = outerNode.ownerDocument.defaultView; + } + const selection = win.getSelection && win.getSelection(); if (!selection || selection.rangeCount === 0) { return null; @@ -150,11 +154,13 @@ export function getModernOffsetsFromPoints( * @param {object} offsets */ export function setOffsets(node, offsets) { - if (!window.getSelection) { + const doc = node.ownerDocument || document; + + if (!doc.defaultView.getSelection) { return; } - const selection = window.getSelection(); + const selection = doc.defaultView.getSelection(); const length = node[getTextContentAccessor()].length; let start = Math.min(offsets.start, length); let end = offsets.end === undefined ? start : Math.min(offsets.end, length); @@ -180,7 +186,7 @@ export function setOffsets(node, offsets) { ) { return; } - const range = document.createRange(); + const range = doc.createRange(); range.setStart(startMarker.node, startMarker.offset); selection.removeAllRanges(); diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index e6193c7973a8c..3f24870e471af 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -64,21 +64,42 @@ function getSelection(node) { start: node.selectionStart, end: node.selectionEnd, }; - } else if (window.getSelection) { - const selection = window.getSelection(); - return { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, - }; + } else { + let win = window; + if (node.ownerDocument && node.ownerDocument.defaultView) { + win = node.ownerDocument.defaultView; + } + if (win.getSelection) { + const selection = win.getSelection(); + return { + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, + }; + } } } +/** + * Get document associated with the event target. + * + * @param {object} nativeEventTarget + * @return {Document} + */ +function getEventTargetDocument(eventTarget) { + return eventTarget.window === eventTarget + ? eventTarget.document + : eventTarget.nodeType === DOCUMENT_NODE + ? eventTarget + : eventTarget.ownerDocument; +} + /** * Poll selection to see whether it's changed. * * @param {object} nativeEvent + * @param {object} nativeEventTarget * @return {?SyntheticEvent} */ function constructSelectEvent(nativeEvent, nativeEventTarget) { @@ -86,10 +107,12 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { // selection (this matches native `select` event behavior). In HTML5, select // fires only on input and textarea thus if there's no focused element we // won't dispatch. + const doc = getEventTargetDocument(nativeEventTarget); + if ( mouseDown || activeElement == null || - activeElement !== getActiveElement() + activeElement !== getActiveElement(doc) ) { return null; } @@ -140,12 +163,7 @@ const SelectEventPlugin = { nativeEvent, nativeEventTarget, ) { - const doc = - nativeEventTarget.window === nativeEventTarget - ? nativeEventTarget.document - : nativeEventTarget.nodeType === DOCUMENT_NODE - ? nativeEventTarget - : nativeEventTarget.ownerDocument; + const doc = getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. if (!doc || !isListeningToAllDependencies('onSelect', doc)) { From 66b87667ccf820b8201486c0e902511d9dd823dd Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Wed, 17 Jan 2018 08:14:00 -0800 Subject: [PATCH 05/24] Support active elements in nested browsing contexts --- .../src/client/ReactInputSelection.js | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js index 506153a118053..51376ca02e9b2 100644 --- a/packages/react-dom/src/client/ReactInputSelection.js +++ b/packages/react-dom/src/client/ReactInputSelection.js @@ -12,7 +12,25 @@ import * as ReactDOMSelection from './ReactDOMSelection'; import {ELEMENT_NODE} from '../shared/HTMLNodeType'; function isInDocument(node) { - return containsNode(document.documentElement, node); + return ( + node && + node.ownerDocument && + containsNode(node.ownerDocument.documentElement, node) + ); +} + +function getActiveElementDeep() { + let win = window; + let element = getActiveElement(); + while (element instanceof win.HTMLIFrameElement) { + try { + win = element.contentDocument.defaultView; + } catch (e) { + return element; + } + element = getActiveElement(win.document); + } + return element; } /** @@ -33,7 +51,7 @@ export function hasSelectionCapabilities(elem) { } export function getSelectionInformation() { - const focusedElem = getActiveElement(); + const focusedElem = getActiveElementDeep(); return { focusedElem: focusedElem, selectionRange: hasSelectionCapabilities(focusedElem) @@ -48,7 +66,7 @@ export function getSelectionInformation() { * nodes and place them back in, resulting in focus being lost. */ export function restoreSelection(priorSelectionInformation) { - const curFocusedElem = getActiveElement(); + const curFocusedElem = getActiveElementDeep(); const priorFocusedElem = priorSelectionInformation.focusedElem; const priorSelectionRange = priorSelectionInformation.selectionRange; if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) { From 75bc65eef9cd175e5999ea2e628fc52ce7e7e6dd Mon Sep 17 00:00:00 2001 From: Andrew Patton Date: Tue, 23 Jan 2018 07:11:54 -0800 Subject: [PATCH 06/24] Avoid invoking defaultView getter unnecessarily --- packages/react-dom/src/client/ReactDOMSelection.js | 12 +++++------- packages/react-dom/src/events/SelectEventPlugin.js | 6 ++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/react-dom/src/client/ReactDOMSelection.js b/packages/react-dom/src/client/ReactDOMSelection.js index 767894a43697e..078316652f0a7 100644 --- a/packages/react-dom/src/client/ReactDOMSelection.js +++ b/packages/react-dom/src/client/ReactDOMSelection.js @@ -14,10 +14,8 @@ import {TEXT_NODE} from '../shared/HTMLNodeType'; * @return {?object} */ export function getOffsets(outerNode) { - let win = window; - if (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) { - win = outerNode.ownerDocument.defaultView; - } + const win = + (outerNode.ownerDocument && outerNode.ownerDocument.defaultView) || window; const selection = win.getSelection && win.getSelection(); if (!selection || selection.rangeCount === 0) { @@ -155,12 +153,12 @@ export function getModernOffsetsFromPoints( */ export function setOffsets(node, offsets) { const doc = node.ownerDocument || document; - - if (!doc.defaultView.getSelection) { + const win = doc ? doc.defaultView : window; + if (!win.getSelection) { return; } - const selection = doc.defaultView.getSelection(); + const selection = win.getSelection(); const length = node[getTextContentAccessor()].length; let start = Math.min(offsets.start, length); let end = offsets.end === undefined ? start : Math.min(offsets.end, length); diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index 3f24870e471af..f04a4522ce5c7 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -65,10 +65,8 @@ function getSelection(node) { end: node.selectionEnd, }; } else { - let win = window; - if (node.ownerDocument && node.ownerDocument.defaultView) { - win = node.ownerDocument.defaultView; - } + const win = + (node.ownerDocument && node.ownerDocument.defaultView) || window; if (win.getSelection) { const selection = win.getSelection(); return { From 5a61b664be14eb6f99752fb7e8d6de96912c2f25 Mon Sep 17 00:00:00 2001 From: Brandon Dail Date: Sun, 28 Jan 2018 18:49:13 -0800 Subject: [PATCH 07/24] Implement selection event fixtures --- fixtures/dom/package.json | 1 + fixtures/dom/public/index.html | 1 + fixtures/dom/src/components/Header.js | 1 + fixtures/dom/src/components/Iframe.js | 53 +++++++++++++++++++ fixtures/dom/src/components/fixtures/index.js | 3 ++ .../selection-events/DraftJsEditorTestCase.js | 36 +++++++++++++ .../ReorderedInputsTestCase.js | 49 +++++++++++++++++ .../fixtures/selection-events/index.js | 24 +++++++++ fixtures/dom/src/react-loader.js | 8 +++ fixtures/dom/yarn.lock | 14 ++++- 10 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 fixtures/dom/src/components/Iframe.js create mode 100644 fixtures/dom/src/components/fixtures/selection-events/DraftJsEditorTestCase.js create mode 100644 fixtures/dom/src/components/fixtures/selection-events/ReorderedInputsTestCase.js create mode 100644 fixtures/dom/src/components/fixtures/selection-events/index.js diff --git a/fixtures/dom/package.json b/fixtures/dom/package.json index 333c566c7ded9..b8de05fc70fbd 100644 --- a/fixtures/dom/package.json +++ b/fixtures/dom/package.json @@ -8,6 +8,7 @@ "dependencies": { "classnames": "^2.2.5", "core-js": "^2.4.1", + "draft-js": "^0.10.5", "prop-types": "^15.6.0", "query-string": "^4.2.3", "react": "^15.4.1", diff --git a/fixtures/dom/public/index.html b/fixtures/dom/public/index.html index f6bb303c9a975..b370c3c8eaefb 100644 --- a/fixtures/dom/public/index.html +++ b/fixtures/dom/public/index.html @@ -16,6 +16,7 @@ React App +
diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index fbb2e6e505b39..19fd16c15f46f 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -64,6 +64,7 @@ class Header extends React.Component { +