Add select event plugin

Polyfill 'onSelect' behavior for React.

Use non-standard 'selectionchange' event rather than 'select' event.
This allows us to fire the event when user moves the cursor via arrow
keys, and not just when they select multiple chars.

Add methods to ReactDOMSelection to make getting current selection
easier, so we can do a fast check for change without having to
calculate char offsets for selection start and end.
This commit is contained in:
Josh Duck
2013-10-07 15:02:57 -07:00
committed by Paul O’Shannessy
parent ed9c0ca87c
commit 2b7a7599bb
8 changed files with 285 additions and 44 deletions
+2 -26
View File
@@ -18,6 +18,8 @@
"use strict";
var shallowEqual = require('shallowEqual');
/**
* If your React component's render function is "pure", e.g. it will render the
* same result given the same props and state, provide this Mixin for a
@@ -49,30 +51,4 @@ var ReactComponentWithPureRenderMixin = {
}
};
/**
* Performs equality by iterating through keys on an object and returning
* false when any key has values which are not strictly equal between
* objA and objB. Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
var key;
// Test for A's keys different from B.
for (key in objA) {
if (objA.hasOwnProperty(key) &&
(!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {
return false;
}
}
// Test for B'a keys missing from A.
for (key in objB) {
if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {
return false;
}
}
return true;
}
module.exports = ReactComponentWithPureRenderMixin;
+12 -10
View File
@@ -33,8 +33,9 @@ var getTextContentAccessor = require('getTextContentAccessor');
* differ between the two APIs.
*
* @param {DOMElement} node
* @return {object}
*/
function getIESelection(node) {
function getIEOffsets(node) {
var selection = document.selection;
var selectedRange = selection.createRange();
var selectedLength = selectedRange.text.length;
@@ -55,8 +56,9 @@ function getIESelection(node) {
/**
* @param {DOMElement} node
* @return {object}
*/
function getModernSelection(node) {
function getModernOffsets(node) {
var selection = window.getSelection();
var anchorNode = selection.anchorNode;
var anchorOffset = selection.anchorOffset;
@@ -90,7 +92,7 @@ function getModernSelection(node) {
* @param {DOMElement|DOMTextNode} node
* @param {object} offsets
*/
function setIESelection(node, offsets) {
function setIEOffsets(node, offsets) {
var range = document.selection.createRange().duplicate();
var start, end;
@@ -124,7 +126,7 @@ function setIESelection(node, offsets) {
* @param {DOMElement|DOMTextNode} node
* @param {object} offsets
*/
function setModernSelection(node, offsets) {
function setModernOffsets(node, offsets) {
var selection = window.getSelection();
var length = node[getTextContentAccessor()].length;
@@ -149,18 +151,18 @@ var ReactDOMSelection = {
/**
* @param {DOMElement} node
*/
get: function(node) {
var getSelection = document.selection ? getIESelection : getModernSelection;
return getSelection(node);
getOffsets: function(node) {
var getOffsets = document.selection ? getIEOffsets : getModernOffsets;
return getOffsets(node);
},
/**
* @param {DOMElement|DOMTextNode} node
* @param {object} offsets
*/
set: function(node, offsets) {
var setSelection = document.selection ? setIESelection : setModernSelection;
setSelection(node, offsets);
setOffsets: function(node, offsets) {
var setOffsets = document.selection ? setIEOffsets : setModernOffsets;
setOffsets(node, offsets);
}
};
+7 -5
View File
@@ -32,14 +32,15 @@ var ReactPerf = require('ReactPerf');
var DefaultDOMPropertyConfig = require('DefaultDOMPropertyConfig');
var DOMProperty = require('DOMProperty');
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
var ChangeEventPlugin = require('ChangeEventPlugin');
var CompositionEventPlugin = require('CompositionEventPlugin');
var DefaultEventPluginOrder = require('DefaultEventPluginOrder');
var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
var EventPluginHub = require('EventPluginHub');
var ReactInstanceHandles = require('ReactInstanceHandles');
var SimpleEventPlugin = require('SimpleEventPlugin');
var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin');
var ReactInstanceHandles = require('ReactInstanceHandles');
var SelectEventPlugin = require('SelectEventPlugin');
var SimpleEventPlugin = require('SimpleEventPlugin');
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactUpdates = require('ReactUpdates');
@@ -61,7 +62,8 @@ function inject() {
'EnterLeaveEventPlugin': EnterLeaveEventPlugin,
'ChangeEventPlugin': ChangeEventPlugin,
'CompositionEventPlugin': CompositionEventPlugin,
'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin
'MobileSafariClickEventPlugin': MobileSafariClickEventPlugin,
'SelectEventPlugin': SelectEventPlugin
});
ReactDOM.injection.injectComponentClasses({
+2 -2
View File
@@ -111,7 +111,7 @@ var ReactInputSelection = {
}
} else {
// Content editable or old IE textarea.
selection = ReactDOMSelection.get(input);
selection = ReactDOMSelection.getOffsets(input);
}
return selection || {start: 0, end: 0};
@@ -140,7 +140,7 @@ var ReactInputSelection = {
range.moveEnd('character', end - start);
range.select();
} else {
ReactDOMSelection.set(input, offsets);
ReactDOMSelection.setOffsets(input, offsets);
}
}
};
-1
View File
@@ -46,7 +46,6 @@ var activeElementID = null;
var activeElementValue = null;
var activeElementValueProp = null;
/**
* SECTION: handle `change` event
*/
@@ -35,6 +35,7 @@ var DefaultEventPluginOrder = [
keyOf({TapEventPlugin: null}),
keyOf({EnterLeaveEventPlugin: null}),
keyOf({ChangeEventPlugin: null}),
keyOf({SelectEventPlugin: null}),
keyOf({CompositionEventPlugin: null}),
keyOf({AnalyticsEventPlugin: null}),
keyOf({MobileSafariClickEventPlugin: null})
+212
View File
@@ -0,0 +1,212 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule SelectEventPlugin
*/
"use strict";
var EventConstants = require('EventConstants');
var EventPluginHub = require('EventPluginHub');
var EventPropagators = require('EventPropagators');
var ExecutionEnvironment = require('ExecutionEnvironment');
var SyntheticEvent = require('SyntheticEvent');
var getActiveElement = require('getActiveElement');
var isEventSupported = require('isEventSupported');
var keyOf = require('keyOf');
var shallowEqual = require('shallowEqual');
var topLevelTypes = EventConstants.topLevelTypes;
var eventTypes = {
select: {
phasedRegistrationNames: {
bubbled: keyOf({onSelect: null}),
captured: keyOf({onSelectCapture: null})
}
}
};
var useSelectionChange = false;
var useSelect = false;
if (ExecutionEnvironment.canUseDOM) {
useSelectionChange = 'onselectionchange' in document;
useSelect = isEventSupported('select');
}
var activeElement = null;
var activeElementID = null;
var activeNativeEvent = null;
var lastSelection = null;
var mouseDown = false;
/**
* Get an object which is a unique representation of the current selection.
*
* The return value will not be consistent across nodes or browsers, but
* two identical selections on the same node will return identical objects.
*
* @param {DOMElement} node
* @param {object}
*/
function getSelection(node) {
if ('selectionStart' in node) {
return {
start: node.selectionStart,
end: node.selectionEnd
};
} else if (document.selection) {
var range = document.selection.createRange();
return {
parentElement: range.parentElement(),
text: range.text,
top: range.boundingTop,
left: range.boundingLeft
};
} else {
var selection = window.getSelection();
return {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset
};
}
}
/**
* Poll selection to see whether it's changed.
*
* @param {object} nativeEvent
* @return {?SyntheticEvent}
*/
function constructSelectEvent(nativeEvent) {
// Ensure we have the right element, and that the user is not dragging a
// selection (this matches native `select` event behavior).
if (mouseDown || activeElement != getActiveElement()) {
return;
}
// Only fire when selection has actually changed.
var currentSelection = getSelection(activeElement);
if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
lastSelection = currentSelection;
return SyntheticEvent.getPooled(
eventTypes.select,
activeElementID,
nativeEvent
);
}
}
/**
* Handle deferred event. And manually dispatch synthetic events.
*/
function handleDeferredEvent() {
if (!activeNativeEvent) {
return;
}
var event = constructSelectEvent(activeNativeEvent);
activeNativeEvent = null;
if (event) {
EventPropagators.accumulateTwoPhaseDispatches(event);
// Enqueue and process the abstract event manually.
EventPluginHub.enqueueEvents(event);
EventPluginHub.processEventQueue();
}
}
/**
* This plugin creates an `onSelect` event that normalizes select events
* across form elements.
*
* Supported elements are:
* - input (see `supportedInputTypes`)
* - TEXTAREA
* - contentEditable
*
* This differs from native browser implementations in the following ways:
* - Fires on contentEditable fields as well as inputs.
* - Fires for collapsed selection.
* - Fires after user input.
*/
var SelectEventPlugin = {
eventTypes: eventTypes,
/**
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
* @see {EventPluginHub.extractEvents}
*/
extractEvents: function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
switch (topLevelType) {
case topLevelTypes.topFocus:
// Track the input node that has focus.
activeElement = topLevelTarget;
activeElementID = topLevelTargetID;
lastSelection = null;
mouseDown = false;
break;
case topLevelTypes.topBlur:
activeElement = null;
activeElementID = null;
lastSelection = null;
mouseDown = false;
break;
case topLevelTypes.topSelectionChange:
// Chrome and IE fire non-standard event whenever selection is
// changed (and sometimes when it hasn't).
var event = constructSelectEvent(nativeEvent);
if (event) {
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
}
break;
case topLevelTypes.topMouseDown:
// Don't fire the event while the user is dragging.
mouseDown = true;
break;
case topLevelTypes.topMouseUp:
mouseDown = false;
activeNativeEvent = nativeEvent;
handleDeferredEvent.defer();
break;
case topLevelTypes.topKeyDown:
// For Firefox we check seleciton after each key.
if (!useSelectionChange) {
activeNativeEvent = nativeEvent;
handleDeferredEvent.defer();
}
break;
}
}
};
module.exports = SelectEventPlugin;
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule shallowEqual
*/
"use strict";
/**
* Performs equality by iterating through keys on an object and returning
* false when any key has values which are not strictly equal between
* objA and objB. Returns true when the values of all keys are strictly equal.
*
* @return {boolean}
*/
function shallowEqual(objA, objB) {
if (objA === objB) {
return true;
}
var key;
// Test for A's keys different from B.
for (key in objA) {
if (objA.hasOwnProperty(key) &&
(!objB.hasOwnProperty(key) || objA[key] !== objB[key])) {
return false;
}
}
// Test for B'a keys missing from A.
for (key in objB) {
if (objB.hasOwnProperty(key) && !objA.hasOwnProperty(key)) {
return false;
}
}
return true;
}
module.exports = shallowEqual;