Skip to content

Commit

Permalink
Issue 331/mention support on @ keypress (#22119)
Browse files Browse the repository at this point in the history
* Add '@' button for inserting mentions on mobile

* Move react-native-azted dependency to external dependencies section

* Handle promise rejection.

* Fix focus issue.

* Add space after mention

* Update selection onFocus.

* Check for site capabilities for mentions support

* Add the mention button inside a toolbar.

* Use HOC for site capabilities.

* Only include space after mention on iOS

On Android we are discarding selections when we think they might get
stripped by Aztec, so adding the space to the ends results in the
selection values getting discarded.

* Use onKeyDown instead of onEnter and onBackspace.

* Intercept @ keypress to trigger mention UI.

* Intercept @ keypress to trigger mention UI.

* Trigger the UI for mentions only when there is a space before the @

* Put mentions behind the DEV flag.

* Only trigger @ keypress on DEV builds.

* Remove DEV flag

* Only make mention keypress available when capabilities and editing menu are on.

* Enable space after mention on Android

* Remove DEV flag for toolbar mention button.

* Bring changes from gb-mobile to the monorepo structure.

* Check triggerKeyCodes on Android

* Add newline to end of file

* Update code to use keycodes.

* Update GB main reference.

* Update dependencies.

Co-authored-by: Matt Chowning <[email protected]>
  • Loading branch information
SergioEstevao and mchowning authored Jun 29, 2020
1 parent 40be8a5 commit 6225038
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 45 deletions.
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.wordpress.mobile.ReactNativeAztec

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

/**
* This event includes all data contained in [com.facebook.react.views.textinput.ReactTextChangedEvent],
* plus some extra info Gutenberg needs from Aztec.
*/
class AztecReactTextChangedEvent(
viewId: Int,
private val mText: String,
private val mEventCount: Int,
private val mMostRecentChar: Char?
) : Event<AztecReactTextChangedEvent>(viewId) {

override fun getEventName(): String = "topAztecChange"

override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(viewTag, eventName, serializeEventData())
}

private fun serializeEventData(): WritableMap =
Arguments.createMap().apply {
putString("text", mText)
putInt("eventCount", mEventCount)
putInt("target", viewTag)
if (mMostRecentChar != null) {
putInt("keyCode", mMostRecentChar.toInt())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
MapBuilder.of(
"bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture")))*/
.put(
"topChange",
"topAztecChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")))
Expand Down Expand Up @@ -623,13 +623,15 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
// the text (minus the Enter char itself).
if (!mEditText.isEnterPressedUnderway()) {
int currentEventCount = mEditText.incrementAndGetEventCounter();
boolean singleCharacterHasBeenAdded = count - before == 1;
// The event that contains the event counter and updates it must be sent first.
// TODO: t7936714 merge these events
mEventDispatcher.dispatchEvent(
new ReactTextChangedEvent(
new AztecReactTextChangedEvent(
mEditText.getId(),
mEditText.toHtml(mEditText.getText(), false),
currentEventCount));
currentEventCount,
singleCharacterHasBeenAdded ? s.charAt(start + before) : null));

mEventDispatcher.dispatchEvent(
new ReactTextInputEvent(
Expand Down Expand Up @@ -657,8 +659,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
}
public void afterTextChanged(Editable s) {}
}

private class AztecContentSizeWatcher implements com.facebook.react.views.textinput.ContentSizeWatcher {
Expand Down
56 changes: 49 additions & 7 deletions packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UIKit
class RCTAztecView: Aztec.TextView {
@objc var onBackspace: RCTBubblingEventBlock? = nil
@objc var onChange: RCTBubblingEventBlock? = nil
@objc var onKeyDown: RCTBubblingEventBlock? = nil
@objc var onEnter: RCTBubblingEventBlock? = nil
@objc var onFocus: RCTBubblingEventBlock? = nil
@objc var onBlur: RCTBubblingEventBlock? = nil
Expand All @@ -14,6 +15,7 @@ class RCTAztecView: Aztec.TextView {
@objc var onSelectionChange: RCTBubblingEventBlock? = nil
@objc var minWidth: CGFloat = 0
@objc var maxWidth: CGFloat = 0
@objc var triggerKeyCodes: NSArray?

@objc var activeFormats: NSSet? = nil {
didSet {
Expand Down Expand Up @@ -304,7 +306,7 @@ class RCTAztecView: Aztec.TextView {
// MARK: - Edits

open override func insertText(_ text: String) {
guard !interceptEnter(text) else {
guard !interceptEnter(text), !interceptTriggersKeyCodes(text) else {
return
}

Expand Down Expand Up @@ -342,12 +344,13 @@ class RCTAztecView: Aztec.TextView {
}

guard text == "\n",
let onEnter = onEnter else {
let onKeyDown = onKeyDown else {
return false
}

let caretData = packCaretDataForRN()
onEnter(caretData)
var eventData = packCaretDataForRN()
eventData = add(keyTrigger: "\r", to: eventData)
onKeyDown(eventData)
return true
}

Expand All @@ -356,19 +359,45 @@ class RCTAztecView: Aztec.TextView {
|| (selectedRange.location == 0 && selectedRange.length == 0)
|| text.count == 1 // send backspace event when cleaning all characters
|| selectedRange == NSRange(location: 0, length: textStorage.length), // send backspace event when deleting all the text
let onBackspace = onBackspace else {
let onKeyDown = onKeyDown else {
return false
}
var range = selectedRange
if text.count == 1 {
range = NSRange(location: 0, length: textStorage.length)
}
let caretData = packCaretDataForRN(overrideRange: range)
var caretData = packCaretDataForRN(overrideRange: range)
onSelectionChange?(caretData)
onBackspace(caretData)
let backSpaceKeyCode:UInt8 = 8
caretData = add(keyCode: backSpaceKeyCode, to: caretData)
onKeyDown(caretData)
return true
}

private func interceptTriggersKeyCodes(_ text: String) -> Bool {
guard let keyCodes = triggerKeyCodes,
keyCodes.count > 0,
let onKeyDown = onKeyDown,
text.count == 1
else {
return false
}
for value in keyCodes {
guard let keyString = value as? String,
let keyCode = keyString.first?.asciiValue,
text.contains(keyString)
else {
continue
}

var eventData = [AnyHashable:Any]()
eventData = add(keyCode: keyCode, to: eventData)
onKeyDown(eventData)
return true
}
return false;
}

private func isNewLineBeforeSelectionAndNotEndOfContent() -> Bool {
guard let currentLocation = text.indexFromLocation(selectedRange.location) else {
return false
Expand Down Expand Up @@ -429,6 +458,19 @@ class RCTAztecView: Aztec.TextView {
return result
}

func add(keyTrigger: String, to pack:[AnyHashable: Any]) -> [AnyHashable: Any] {
guard let keyCode = keyTrigger.first?.asciiValue else {
return pack
}
return add(keyCode: keyCode, to: pack)
}

func add(keyCode: UInt8, to pack:[AnyHashable: Any]) -> [AnyHashable: Any] {
var result = pack
result["keyCode"] = keyCode
return result
}

// MARK: - RN Properties

@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ @interface RCT_EXTERN_MODULE(RCTAztecViewManager, NSObject)
RCT_EXPORT_VIEW_PROPERTY(onBackspace, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onEnter, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(triggerKeyCodes, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTBubblingEventBlock)
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-aztec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"dependencies": {
"@wordpress/keycodes": "file:../keycodes"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
Expand Down
66 changes: 58 additions & 8 deletions packages/react-native-aztec/src/AztecView.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import ReactNative, {
Platform,
} from 'react-native';
import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';
/**
* WordPress dependencies
*/
import { ENTER, BACKSPACE } from '@wordpress/keycodes';

const AztecManager = UIManager.getViewManagerConfig( 'RCTAztecView' );

Expand All @@ -18,6 +22,8 @@ class AztecView extends React.Component {
this._onContentSizeChange = this._onContentSizeChange.bind( this );
this._onEnter = this._onEnter.bind( this );
this._onBackspace = this._onBackspace.bind( this );
this._onKeyDown = this._onKeyDown.bind( this );
this._onChange = this._onChange.bind( this );
this._onHTMLContentWithCursor = this._onHTMLContentWithCursor.bind(
this
);
Expand Down Expand Up @@ -57,21 +63,43 @@ class AztecView extends React.Component {
return;
}

if ( ! this.props.onEnter ) {
if ( ! this.props.onKeyDown ) {
return;
}

const { onEnter } = this.props;
onEnter( event );
const { onKeyDown } = this.props;

const newEvent = { ...event, keyCode: ENTER };
onKeyDown( newEvent );
}

_onBackspace( event ) {
if ( ! this.props.onBackspace ) {
if ( ! this.props.onKeyDown ) {
return;
}

const { onKeyDown } = this.props;

const newEvent = {
...event,
keyCode: BACKSPACE,
preventDefault: () => {},
};
onKeyDown( newEvent );
}

_onKeyDown( event ) {
if ( ! this.props.onKeyDown ) {
return;
}

const { onBackspace } = this.props;
onBackspace( event );
const { onKeyDown } = this.props;
const newEvent = {
...event,
keyCode: event.nativeEvent.keyCode,
preventDefault: () => {},
};
onKeyDown( newEvent );
}

_onHTMLContentWithCursor( event ) {
Expand Down Expand Up @@ -107,6 +135,26 @@ class AztecView extends React.Component {
onBlur( event );
}

_onChange( event ) {
// iOS uses the the onKeyDown prop directly from native only when one of the triggerKeyCodes is entered, but
// Android includes the information needed for onKeyDown in the event passed to onChange.
if ( Platform.OS === 'android' ) {
const triggersIncludeEventKeyCode =
this.props.triggerKeyCodes &&
this.props.triggerKeyCodes
.map( ( char ) => char.charCodeAt( 0 ) )
.includes( event.nativeEvent.keyCode );
if ( triggersIncludeEventKeyCode ) {
this._onKeyDown( event );
}
}

const { onChange } = this.props;
if ( onChange ) {
onChange( event );
}
}

_onSelectionChange( event ) {
if ( this.props.onSelectionChange ) {
const { selectionStart, selectionEnd, text } = event.nativeEvent;
Expand Down Expand Up @@ -182,15 +230,17 @@ class AztecView extends React.Component {
style={ style }
onContentSizeChange={ this._onContentSizeChange }
onHTMLContentWithCursor={ this._onHTMLContentWithCursor }
onChange={ this._onChange }
onSelectionChange={ this._onSelectionChange }
onEnter={ this.props.onEnter && this._onEnter }
onEnter={ this.props.onKeyDown && this._onEnter }
onBackspace={ this.props.onKeyDown && this._onBackspace }
onKeyDown={ this.props.onKeyDown && this._onKeyDown }
deleteEnter={ this.props.deleteEnter }
// IMPORTANT: the onFocus events are thrown away as these are handled by onPress() in the upper level.
// It's necessary to do this otherwise onFocus may be set by `{...otherProps}` and thus the onPress + onFocus
// combination generate an infinite loop as described in https://github.com/wordpress-mobile/gutenberg-mobile/issues/302
onFocus={ this._onAztecFocus }
onBlur={ this._onBlur }
onBackspace={ this._onBackspace }
/>
</TouchableWithoutFeedback>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native-editor/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ PODS:
- DoubleConversion
- glog
- glog (0.3.5)
- Gutenberg (8.4.0-rc.1):
- Gutenberg (8.4.0):
- React (= 0.61.5)
- React-CoreModules (= 0.61.5)
- React-RCTImage (= 0.61.5)
Expand Down Expand Up @@ -372,7 +372,7 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
Gutenberg: f7055103da8a673d813ecec75875d973e3d25445
Gutenberg: 42a3ed491af07194744d45aa7fc44b8202ea1a5b
RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1
RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320
React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78
Expand Down
Loading

0 comments on commit 6225038

Please sign in to comment.