Skip to content

Commit

Permalink
Add fix for android keyboards
Browse files Browse the repository at this point in the history
  • Loading branch information
fraziermork committed Jul 16, 2019
1 parent 82ff3ed commit acb05db
Show file tree
Hide file tree
Showing 10 changed files with 637 additions and 67 deletions.
1 change: 1 addition & 0 deletions meta/bundle-size-stats/Draft.js.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions meta/bundle-size-stats/Draft.min.js.json

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions src/component/handlers/composition/DOMObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
* @emails oncall+draft_js
*/

'use strict';

const UserAgent = require('UserAgent');

const findAncestorOffsetKey = require('findAncestorOffsetKey');
const Immutable = require('immutable');
const invariant = require('invariant');
const nullthrows = require('nullthrows');

const {Map} = Immutable;

type MutationRecordT =
| MutationRecord
| {|type: 'characterData', target: Node, removedNodes?: void|};

// Heavily based on Prosemirror's DOMObserver https://github.com/ProseMirror/prosemirror-view/blob/master/src/domobserver.js

const DOM_OBSERVER_OPTIONS = {
subtree: true,
characterData: true,
childList: true,
characterDataOldValue: false,
attributes: false,
};
// IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified
const USE_CHAR_DATA = UserAgent.isBrowser('IE <= 11');

class DOMObserver {
observer: ?MutationObserver;
container: HTMLElement;
mutations: Map<string, string>;
onCharData: ?({target: EventTarget, type: string}) => void;

constructor(container: HTMLElement) {
this.container = container;
this.mutations = Map();
if (window.MutationObserver && !USE_CHAR_DATA) {
this.observer = new window.MutationObserver(mutations =>
this.registerMutations(mutations),
);
} else {
this.onCharData = e => {
invariant(
e.target instanceof Node,
'Expected target to be an instance of Node',
);
this.registerMutation({
type: 'characterData',
target: e.target,
});
};
}
}

start(): void {
if (this.observer) {
this.observer.observe(this.container, DOM_OBSERVER_OPTIONS);
} else {
/* $FlowFixMe(>=0.68.0 site=www,mobile) This event type is not defined
* by Flow's standard library */
this.container.addEventListener(
'DOMCharacterDataModified',
this.onCharData,
);
}
}

stopAndFlushMutations(): Map<string, string> {
const {observer} = this;
if (observer) {
this.registerMutations(observer.takeRecords());
observer.disconnect();
} else {
/* $FlowFixMe(>=0.68.0 site=www,mobile) This event type is not defined
* by Flow's standard library */
this.container.removeEventListener(
'DOMCharacterDataModified',
this.onCharData,
);
}
const mutations = this.mutations;
this.mutations = Map();
return mutations;
}

registerMutations(mutations: Array<MutationRecord>): void {
for (let i = 0; i < mutations.length; i++) {
this.registerMutation(mutations[i]);
}
}

getMutationTextContent(mutation: MutationRecordT): ?string {
const {type, target, removedNodes} = mutation;
if (type === 'characterData') {
// When `textContent` is '', there is a race condition that makes
// getting the offsetKey from the target not possible.
// These events are also followed by a `childList`, which is the one
// we are able to retrieve the offsetKey and apply the '' text.
if (target.textContent !== '') {
return target.textContent;
}
} else if (type === 'childList') {
// `characterData` events won't happen or are ignored when
// removing the last character of a leaf node, what happens
// instead is a `childList` event with a `removedNodes` array.
// For this case the textContent should be '' and
// `DraftModifier.replaceText` will make sure the content is
// updated properly.
if (removedNodes && removedNodes.length) {
return '';
}
}
return null;
}

registerMutation(mutation: MutationRecordT): void {
const textContent = this.getMutationTextContent(mutation);
if (textContent != null) {
const offsetKey = nullthrows(findAncestorOffsetKey(mutation.target));
this.mutations = this.mutations.set(offsetKey, textContent);
}
}
}

module.exports = DOMObserver;
145 changes: 98 additions & 47 deletions src/component/handlers/composition/DraftEditorCompositionHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@

import type DraftEditor from 'DraftEditor.react';

const DOMObserver = require('DOMObserver');
const DraftModifier = require('DraftModifier');
const DraftOffsetKey = require('DraftOffsetKey');
const EditorState = require('EditorState');
const Keys = require('Keys');

const editOnSelect = require('editOnSelect');
const getContentEditableContainer = require('getContentEditableContainer');
const getDraftEditorSelection = require('getDraftEditorSelection');
const getEntityKeyForSelection = require('getEntityKeyForSelection');
const isSelectionAtLeafStart = require('isSelectionAtLeafStart');
const nullthrows = require('nullthrows');

/**
* Millisecond delay to allow `compositionstart` to fire again upon
Expand All @@ -41,19 +46,23 @@ const RESOLVE_DELAY = 20;
*/
let resolved = false;
let stillComposing = false;
let textInputData = '';
let domObserver = null;

var DraftEditorCompositionHandler = {
onBeforeInput: function(editor: DraftEditor, e: SyntheticInputEvent): void {
textInputData = (textInputData || '') + e.data;
},
function startDOMObserver(editor: DraftEditor) {
if (!domObserver) {
domObserver = new DOMObserver(getContentEditableContainer(editor));
domObserver.start();
}
}

const DraftEditorCompositionHandler = {
/**
* A `compositionstart` event has fired while we're still in composition
* mode. Continue the current composition session to prevent a re-render.
*/
onCompositionStart: function(editor: DraftEditor): void {
stillComposing = true;
startDOMObserver(editor);
},

/**
Expand All @@ -80,6 +89,8 @@ var DraftEditorCompositionHandler = {
}, RESOLVE_DELAY);
},

onSelect: editOnSelect,

/**
* In Safari, keydown events may fire when committing compositions. If
* the arrow keys are used to commit, prevent default so that the cursor
Expand Down Expand Up @@ -132,61 +143,101 @@ var DraftEditorCompositionHandler = {
return;
}

const mutations = nullthrows(domObserver).stopAndFlushMutations();
domObserver = null;
resolved = true;
const composedChars = textInputData;
textInputData = '';

const editorState = EditorState.set(editor._latestEditorState, {
let editorState = EditorState.set(editor._latestEditorState, {
inCompositionMode: false,
});

const currentStyle = editorState.getCurrentInlineStyle();
const entityKey = getEntityKeyForSelection(
editorState.getCurrentContent(),
editorState.getSelection(),
);

const mustReset = (
!composedChars ||
isSelectionAtLeafStart(editorState) ||
currentStyle.size > 0 ||
entityKey !== null
);
editor.exitCurrentMode();

if (mustReset) {
editor.restoreEditorDOM();
if (!mutations.size) {
editor.update(editorState);
return;
}

editor.exitCurrentMode();
// TODO, check if Facebook still needs this flag or if it could be removed.
// Since there can be multiple mutations providing a `composedChars` doesn't
// apply well on this new model.
// if (
// gkx('draft_handlebeforeinput_composed_text') &&
// editor.props.handleBeforeInput &&
// isEventHandled(
// editor.props.handleBeforeInput(
// composedChars,
// editorState,
// event.timeStamp,
// ),
// )
// ) {
// return;
// }

let contentState = editorState.getCurrentContent();
mutations.forEach((composedChars, offsetKey) => {
const {blockKey, decoratorKey, leafKey} = DraftOffsetKey.decode(
offsetKey,
);

if (composedChars) {
// If characters have been composed, re-rendering with the update
// is sufficient to reset the editor.
const contentState = DraftModifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
const {start, end} = editorState
.getBlockTree(blockKey)
.getIn([decoratorKey, 'leaves', leafKey]);

const replacementRange = editorState.getSelection().merge({
anchorKey: blockKey,
focusKey: blockKey,
anchorOffset: start,
focusOffset: end,
isBackward: false,
});

const entityKey = getEntityKeyForSelection(
contentState,
replacementRange,
);
const currentStyle = contentState
.getBlockForKey(blockKey)
.getInlineStyleAt(start);

contentState = DraftModifier.replaceText(
contentState,
replacementRange,
composedChars,
currentStyle,
entityKey,
);
editor.update(
EditorState.push(
editorState,
contentState,
'insert-characters',
),
);
return;
}
// We need to update the editorState so the leaf node ranges are properly
// updated and multiple mutations are correctly applied.
editorState = EditorState.set(editorState, {
currentContent: contentState,
});
});

if (mustReset) {
editor.update(
EditorState.set(editorState, {
nativelyRenderedContent: null,
forceSelection: true,
}),
);
}
// When we apply the text changes to the ContentState, the selection always
// goes to the end of the field, but it should just stay where it is
// after compositionEnd.
const documentSelection = getDraftEditorSelection(
editorState,
getContentEditableContainer(editor),
);
const compositionEndSelectionState = documentSelection.selectionState;

editor.restoreEditorDOM();

const editorStateWithUpdatedSelection = EditorState.acceptSelection(
editorState,
compositionEndSelectionState,
);

editor.update(
EditorState.push(
editorStateWithUpdatedSelection,
contentState,
'insert-characters',
),
);
},
};

Expand Down
Loading

0 comments on commit acb05db

Please sign in to comment.