Skip to content

Commit

Permalink
fix(editable): trigger beforeinput event in FireFox
Browse files Browse the repository at this point in the history
  • Loading branch information
marsprince committed Nov 11, 2020
1 parent dfcafbf commit 52af03a
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/slate-vue/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Transforms, Range,Editor, Element, Node} from 'slate';
import {DOMStaticRange} from '../utils/dom';
import { IS_FIREFOX, IS_SAFARI, IS_EDGE_LEGACY } from '../utils/environment'
import Hotkeys from '../utils/hotkeys'
import { addOnBeforeInput } from '../utils/beforeInput';

interface IEvent extends Event {
data: string | null
Expand Down Expand Up @@ -329,6 +330,14 @@ export const Editable = tsx.component({
},
_onBeforeInput(event: IEvent) {
const editor = (this as any).$editor;
// in FireFox, we use a dispatchEvent and only support insertData
if(IS_FIREFOX) {
event.preventDefault()
event.stopPropagation()
const text = (event as any).detail as string
Editor.insertText(editor, text)
return
}
if (
!this.readOnly &&
hasEditableTarget(editor, event.target) &&
Expand Down Expand Up @@ -983,6 +992,12 @@ export const Editable = tsx.component({
// The autoFocus TextareaHTMLAttribute doesn't do anything on a div, so it
// needs to be manually focused.
updateAutoFocus();
// patch beforeinput in FireFox
if(IS_FIREFOX) {
useEffect(() => {
addOnBeforeInput(ref.current, true)
}, [])
}
},
render() {
const editor = (this as any).$editor;
Expand Down
195 changes: 195 additions & 0 deletions packages/slate-vue/utils/beforeInput.ts
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)
}

0 comments on commit 52af03a

Please sign in to comment.