-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(editable): trigger beforeinput event in FireFox
- Loading branch information
1 parent
dfcafbf
commit 52af03a
Showing
2 changed files
with
210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
/** | ||
* Create an `onBeforeInput` event to match | ||
* http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents. | ||
* | ||
* This event plugin is based on the native `textInput` event | ||
* available in Chrome, Safari, Opera, and IE. This event fires after | ||
* `onKeyPress` and `onCompositionEnd`, but before `onInput`. | ||
* | ||
* `beforeInput` is spec'd but not implemented in any browsers, and | ||
* the `input` event does not provide any useful information about what has | ||
* actually been added, contrary to the spec. Thus, `textInput` is the best | ||
* available event to identify the characters that have actually been inserted | ||
* into the target node. | ||
* | ||
* Inspired by react-dom but less complex: https://github.com/facebook/react/blob/480626a9e920d5e04194c793a828318102ea4ff4/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js | ||
*/ | ||
const canUseDOM: boolean = | ||
typeof window !== 'undefined' && | ||
typeof window.document !== 'undefined' && | ||
typeof window.document.createElement !== 'undefined' | ||
|
||
const SPACEBAR_CODE = 32; | ||
const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE); | ||
|
||
// Track the current IME composition status, if any. | ||
let isComposing = false; | ||
|
||
/** | ||
* Return whether a native keypress event is assumed to be a command. | ||
* This is required because Firefox fires `keypress` events for key commands | ||
* (cut, copy, select-all, etc.) even though no character is inserted. | ||
*/ | ||
function isKeypressCommand(nativeEvent: any) { | ||
return ( | ||
(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) && | ||
// ctrlKey && altKey is equivalent to AltGr, and is not a command. | ||
!(nativeEvent.ctrlKey && nativeEvent.altKey) | ||
); | ||
} | ||
|
||
function getDataFromCustomEvent(nativeEvent: any) { | ||
const detail = nativeEvent.detail; | ||
if (typeof detail === 'object' && 'data' in detail) { | ||
return detail.data; | ||
} | ||
return null; | ||
} | ||
|
||
const getNativeBeforeInputChars = ( | ||
domEventName: string, | ||
nativeEvent: any, | ||
): string | null => { | ||
let hasSpaceKeypress = false | ||
|
||
switch (domEventName) { | ||
case 'compositionend': | ||
return getDataFromCustomEvent(nativeEvent); | ||
case 'keypress': | ||
/** | ||
* If native `textInput` events are available, our goal is to make | ||
* use of them. However, there is a special case: the spacebar key. | ||
* In Webkit, preventing default on a spacebar `textInput` event | ||
* cancels character insertion, but it *also* causes the browser | ||
* to fall back to its default spacebar behavior of scrolling the | ||
* page. | ||
* | ||
* Tracking at: | ||
* https://code.google.com/p/chromium/issues/detail?id=355103 | ||
* | ||
* To avoid this issue, use the keypress event as if no `textInput` | ||
* event is available. | ||
*/ | ||
const which = nativeEvent.which; | ||
if (which !== SPACEBAR_CODE) { | ||
return null; | ||
} | ||
|
||
hasSpaceKeypress = true; | ||
return SPACEBAR_CHAR; | ||
|
||
case 'textInput': | ||
// Record the characters to be added to the DOM. | ||
const chars = nativeEvent.data; | ||
|
||
// If it's a spacebar character, assume that we have already handled | ||
// it at the keypress level and bail immediately. Android Chrome | ||
// doesn't give us keycodes, so we need to ignore it. | ||
if (chars === SPACEBAR_CHAR && hasSpaceKeypress) { | ||
return null; | ||
} | ||
|
||
return chars; | ||
|
||
default: | ||
// For other native event types, do nothing. | ||
return null; | ||
} | ||
} | ||
|
||
function getFallbackBeforeInputChars( | ||
domEventName: string, | ||
nativeEvent: any, | ||
): string | null { | ||
// If we are currently composing (IME) and using a fallback to do so, | ||
// try to extract the composed characters from the fallback object. | ||
// If composition event is available, we extract a string only at | ||
// compositionevent, otherwise extract it at fallback events. | ||
if (isComposing) { | ||
if ( | ||
domEventName === 'compositionend' | ||
) { | ||
return nativeEvent.data | ||
} | ||
return null; | ||
} | ||
|
||
switch (domEventName) { | ||
case 'paste': | ||
// If a paste event occurs after a keypress, throw out the input | ||
// chars. Paste events should not lead to BeforeInput events. | ||
return null; | ||
case 'keypress': | ||
/** | ||
* As of v27, Firefox may fire keypress events even when no character | ||
* will be inserted. A few possibilities: | ||
* | ||
* - `which` is `0`. Arrow keys, Esc key, etc. | ||
* | ||
* - `which` is the pressed key code, but no char is available. | ||
* Ex: 'AltGr + d` in Polish. There is no modified character for | ||
* this key combination and no character is inserted into the | ||
* document, but FF fires the keypress for char code `100` anyway. | ||
* No `input` event will occur. | ||
* | ||
* - `which` is the pressed key code, but a command combination is | ||
* being used. Ex: `Cmd+C`. No character is inserted, and no | ||
* `input` event will occur. | ||
*/ | ||
if (!isKeypressCommand(nativeEvent)) { | ||
// IE fires the `keypress` event when a user types an emoji via | ||
// Touch keyboard of Windows. In such a case, the `char` property | ||
// holds an emoji character like `\uD83D\uDE0A`. Because its length | ||
// is 2, the property `which` does not represent an emoji correctly. | ||
// In such a case, we directly return the `char` property instead of | ||
// using `which`. | ||
if (nativeEvent.char && nativeEvent.char.length > 1) { | ||
return nativeEvent.char; | ||
} else if (nativeEvent.which) { | ||
return String.fromCharCode(nativeEvent.which); | ||
} | ||
} | ||
return null; | ||
case 'compositionend': | ||
// remove some lower browser | ||
return nativeEvent.data; | ||
default: | ||
return null; | ||
} | ||
} | ||
|
||
export const extractBeforeInputEvent = (domEventName: string, nativeEvent: Event) => { | ||
// Webkit offers a very useful `textInput` event that can be used to | ||
// directly represent `beforeInput`. The IE `textinput` event is not as | ||
// useful, so we don't use it. | ||
const canUseTextInputEvent = canUseDOM && 'TextEvent' in window | ||
let chars; | ||
|
||
if (canUseTextInputEvent) { | ||
chars = getNativeBeforeInputChars(domEventName, nativeEvent); | ||
} else { | ||
chars = getFallbackBeforeInputChars(domEventName, nativeEvent); | ||
} | ||
// dispatch a new event | ||
const event = new CustomEvent('beforeinput', { | ||
detail: chars | ||
}) | ||
if(nativeEvent.target) { | ||
nativeEvent.target.dispatchEvent(event) | ||
} | ||
} | ||
|
||
|
||
export const addOnBeforeInput = (el: any, isPrevent?: boolean) => { | ||
['compositionend', 'textInput', 'keypress'].forEach(eventName => { | ||
el.addEventListener(eventName, function(e: Event) { | ||
extractBeforeInputEvent(eventName, e) | ||
if(eventName === 'compositionend') isComposing = false | ||
// determine whether prevent there | ||
if(isPrevent) { | ||
e.preventDefault() | ||
} | ||
}) | ||
}) | ||
el.addEventListener('compositionstart', () => isComposing = true) | ||
} |