diff --git a/RNTAztecView.podspec b/RNTAztecView.podspec index 926e71e..7d079bf 100644 --- a/RNTAztecView.podspec +++ b/RNTAztecView.podspec @@ -9,11 +9,11 @@ Pod::Spec.new do |s| s.license = package['license'] s.homepage = 'https://github.com/wordpress-mobile/react-native-aztec' s.authors = 'Automattic' - s.source = { :git => 'https://github.com/wordpress-mobile/react-native-aztec.git' } + s.source = { :git => 'https://github.com/ewindso/react-native-aztec' } s.source_files = 'ios/RNTAztecView/*.{h,m,swift}' s.public_header_files = 'ios/RNTAztecView/*.h' s.requires_arc = true - s.platforms = { :ios => "10.0" } + s.platforms = { :ios => "11.0" } s.xcconfig = {'OTHER_LDFLAGS' => '-lxml2', 'HEADER_SEARCH_PATHS' => '/usr/include/libxml2'} s.dependency 'React' diff --git a/android/build.gradle b/android/build.gradle index aae3b77..2cc6d52 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -12,7 +12,7 @@ buildscript { wordpressUtilsVersion = '1.22' espressoVersion = '3.0.1' - aztecVersion = 'v1.3.14' + aztecVersion = 'v1.3.45' } repositories { diff --git a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/BetterLinkMovementMethod.java b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/BetterLinkMovementMethod.java new file mode 100644 index 0000000..8c7b3ef --- /dev/null +++ b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/BetterLinkMovementMethod.java @@ -0,0 +1,455 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import android.app.Activity; +import android.graphics.RectF; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.BackgroundColorSpan; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.TextView; + +/** + * Handles URL clicks on TextViews. Unlike the default implementation, this: + *

+ *

+ */ +public class BetterLinkMovementMethod extends LinkMovementMethod { + + private static BetterLinkMovementMethod singleInstance; + private static final int LINKIFY_NONE = -2; + + private OnLinkClickListener onLinkClickListener; + private OnLinkLongClickListener onLinkLongClickListener; + private final RectF touchedLineBounds = new RectF(); + private boolean isUrlHighlighted; + private ClickableSpan clickableSpanUnderTouchOnActionDown; + private int activeTextViewHashcode; + private LongPressTimer ongoingLongPressTimer; + private boolean wasLongPressRegistered; + + public interface OnLinkClickListener { + /** + * @param textView The TextView on which a click was registered. + * @param url The clicked URL. + * @return True if this click was handled. False to let Android handle the URL. + */ + boolean onClick(TextView textView, String url); + } + + public interface OnLinkLongClickListener { + /** + * @param textView The TextView on which a long-click was registered. + * @param url The long-clicked URL. + * @return True if this long-click was handled. False to let Android handle the URL (as a short-click). + */ + boolean onLongClick(TextView textView, String url); + } + + /** + * Return a new instance of BetterLinkMovementMethod. + */ + public static BetterLinkMovementMethod newInstance() { + return new BetterLinkMovementMethod(); + } + + /** + * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, + * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. + * @param textViews The TextViews on which a {@link BetterLinkMovementMethod} should be registered. + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + public static BetterLinkMovementMethod linkify(int linkifyMask, TextView... textViews) { + BetterLinkMovementMethod movementMethod = newInstance(); + for (TextView textView : textViews) { + addLinks(linkifyMask, movementMethod, textView); + } + return movementMethod; + } + + /** + * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. + * + * @param textViews The TextViews on which a {@link BetterLinkMovementMethod} should be registered. + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + public static BetterLinkMovementMethod linkifyHtml(TextView... textViews) { + return linkify(LINKIFY_NONE, textViews); + } + + /** + * Recursively register a {@link BetterLinkMovementMethod} on every TextView inside a layout. + * + * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, + * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + public static BetterLinkMovementMethod linkify(int linkifyMask, ViewGroup viewGroup) { + BetterLinkMovementMethod movementMethod = newInstance(); + rAddLinks(linkifyMask, viewGroup, movementMethod); + return movementMethod; + } + + /** + * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. + * + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + @SuppressWarnings("unused") + public static BetterLinkMovementMethod linkifyHtml(ViewGroup viewGroup) { + return linkify(LINKIFY_NONE, viewGroup); + } + + /** + * Recursively register a {@link BetterLinkMovementMethod} on every TextView inside a layout. + * + * @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES}, + * {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}. + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + public static BetterLinkMovementMethod linkify(int linkifyMask, Activity activity) { + // Find the layout passed to setContentView(). + ViewGroup activityLayout = ((ViewGroup) ((ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT)).getChildAt(0)); + + BetterLinkMovementMethod movementMethod = newInstance(); + rAddLinks(linkifyMask, activityLayout, movementMethod); + return movementMethod; + } + + /** + * Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links. + * + * @return The registered {@link BetterLinkMovementMethod} on the TextViews. + */ + @SuppressWarnings("unused") + public static BetterLinkMovementMethod linkifyHtml(Activity activity) { + return linkify(LINKIFY_NONE, activity); + } + + /** + * Get a static instance of BetterLinkMovementMethod. Do note that registering a click listener on the returned + * instance is not supported because it will potentially be shared on multiple TextViews. + */ + @SuppressWarnings("unused") + public static BetterLinkMovementMethod getInstance() { + if (singleInstance == null) { + singleInstance = new BetterLinkMovementMethod(); + } + return singleInstance; + } + + protected BetterLinkMovementMethod() { + } + + /** + * Set a listener that will get called whenever any link is clicked on the TextView. + */ + public BetterLinkMovementMethod setOnLinkClickListener(OnLinkClickListener clickListener) { + if (this == singleInstance) { + throw new UnsupportedOperationException("Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " + + "leaks. Please use newInstance() or any of the linkify() methods instead."); + } + + this.onLinkClickListener = clickListener; + return this; + } + + /** + * Set a listener that will get called whenever any link is clicked on the TextView. + */ + public BetterLinkMovementMethod setOnLinkLongClickListener(OnLinkLongClickListener longClickListener) { + if (this == singleInstance) { + throw new UnsupportedOperationException("Setting a long-click listener on the instance returned by getInstance() is not supported to avoid " + + "memory leaks. Please use newInstance() or any of the linkify() methods instead."); + } + + this.onLinkLongClickListener = longClickListener; + return this; + } + +// ======== PUBLIC APIs END ======== // + + private static void rAddLinks(int linkifyMask, ViewGroup viewGroup, BetterLinkMovementMethod movementMethod) { + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View child = viewGroup.getChildAt(i); + + if (child instanceof ViewGroup) { + // Recursively find child TextViews. + rAddLinks(linkifyMask, ((ViewGroup) child), movementMethod); + } else if (child instanceof TextView) { + TextView textView = (TextView) child; + addLinks(linkifyMask, movementMethod, textView); + } + } + } + + private static void addLinks(int linkifyMask, BetterLinkMovementMethod movementMethod, TextView textView) { + textView.setMovementMethod(movementMethod); + if (linkifyMask != LINKIFY_NONE) { + Linkify.addLinks(textView, linkifyMask); + } + } + + @Override + public boolean onTouchEvent(final TextView textView, Spannable text, MotionEvent event) { + if (activeTextViewHashcode != textView.hashCode()) { + // Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted. + // A hacky solution is to reset any "autoLink" property set in XML. But we also want + // to do this once per TextView. + activeTextViewHashcode = textView.hashCode(); + textView.setAutoLinkMask(0); + } + + final ClickableSpan clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch; + } + final boolean touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (clickableSpanUnderTouch != null) { + highlightUrl(textView, clickableSpanUnderTouch, text); + } + + if (touchStartedOverAClickableSpan && onLinkLongClickListener != null) { + LongPressTimer.OnTimerReachedListener longClickListener = new LongPressTimer.OnTimerReachedListener() { + @Override + public void onTimerReached() { + wasLongPressRegistered = true; + textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + removeUrlHighlightColor(textView); + dispatchUrlLongClick(textView, clickableSpanUnderTouch); + } + }; + startTimerForRegisteringLongClick(textView, longClickListener); + } + return touchStartedOverAClickableSpan; + + case MotionEvent.ACTION_UP: + // Register a click only if the touch started and ended on the same URL. + if (!wasLongPressRegistered && touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) { + dispatchUrlClick(textView, clickableSpanUnderTouch); + } + cleanupOnTouchUp(textView); + + // Consume this event even if we could not find any spans to avoid letting Android handle this event. + // Android's TextView implementation has a bug where links get clicked even when there is no more text + // next to the link and the touch lies outside its bounds in the same direction. + return touchStartedOverAClickableSpan; + + case MotionEvent.ACTION_CANCEL: + cleanupOnTouchUp(textView); + return false; + + case MotionEvent.ACTION_MOVE: + // Stop listening for a long-press as soon as the user wanders off to unknown lands. + if (clickableSpanUnderTouch != clickableSpanUnderTouchOnActionDown) { + removeLongPressCallback(textView); + } + + if (!wasLongPressRegistered) { + // Toggle highlight. + if (clickableSpanUnderTouch != null) { + highlightUrl(textView, clickableSpanUnderTouch, text); + } else { + removeUrlHighlightColor(textView); + } + } + + return touchStartedOverAClickableSpan; + + default: + return false; + } + } + + private void cleanupOnTouchUp(TextView textView) { + wasLongPressRegistered = false; + clickableSpanUnderTouchOnActionDown = null; + removeUrlHighlightColor(textView); + removeLongPressCallback(textView); + } + + /** + * Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any). + * + * @return The touched ClickableSpan or null. + */ + protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) { + // So we need to find the location in text where touch was made, regardless of whether the TextView + // has scrollable text. That is, not the entire text is currently visible. + int touchX = (int) event.getX(); + int touchY = (int) event.getY(); + + // Ignore padding. + touchX -= textView.getTotalPaddingLeft(); + touchY -= textView.getTotalPaddingTop(); + + // Account for scrollable text. + touchX += textView.getScrollX(); + touchY += textView.getScrollY(); + + final Layout layout = textView.getLayout(); + final int touchedLine = layout.getLineForVertical(touchY); + final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX); + + touchedLineBounds.left = layout.getLineLeft(touchedLine); + touchedLineBounds.top = layout.getLineTop(touchedLine); + touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left; + touchedLineBounds.bottom = layout.getLineBottom(touchedLine); + + if (touchedLineBounds.contains(touchX, touchY)) { + // Find a ClickableSpan that lies under the touched area. + final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class); + for (final Object span : spans) { + if (span instanceof ClickableSpan) { + return (ClickableSpan) span; + } + } + // No ClickableSpan found under the touched location. + return null; + + } else { + // Touch lies outside the line's horizontal bounds where no spans should exist. + return null; + } + } + + /** + * Adds a background color span at clickableSpan's location. + */ + protected void highlightUrl(TextView textView, ClickableSpan clickableSpan, Spannable text) { + if (isUrlHighlighted) { + return; + } + isUrlHighlighted = true; + + int spanStart = text.getSpanStart(clickableSpan); + int spanEnd = text.getSpanEnd(clickableSpan); + BackgroundColorSpan highlightSpan = new BackgroundColorSpan(textView.getHighlightColor()); + text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE); + + textView.setTag(R.id.bettermovementmethod_highlight_background_span, highlightSpan); + + Selection.setSelection(text, spanStart, spanEnd); + } + + /** + * Removes the highlight color under the Url. + */ + protected void removeUrlHighlightColor(TextView textView) { + if (!isUrlHighlighted) { + return; + } + isUrlHighlighted = false; + + Spannable text = (Spannable) textView.getText(); + BackgroundColorSpan highlightSpan = (BackgroundColorSpan) textView.getTag(R.id.bettermovementmethod_highlight_background_span); + text.removeSpan(highlightSpan); + + Selection.removeSelection(text); + } + + protected void startTimerForRegisteringLongClick(TextView textView, LongPressTimer.OnTimerReachedListener longClickListener) { + ongoingLongPressTimer = new LongPressTimer(); + ongoingLongPressTimer.setOnTimerReachedListener(longClickListener); + textView.postDelayed(ongoingLongPressTimer, ViewConfiguration.getLongPressTimeout()); + } + + /** + * Remove the long-press detection timer. + */ + protected void removeLongPressCallback(TextView textView) { + if (ongoingLongPressTimer != null) { + textView.removeCallbacks(ongoingLongPressTimer); + ongoingLongPressTimer = null; + } + } + + protected void dispatchUrlClick(TextView textView, ClickableSpan clickableSpan) { + ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan); + boolean handled = onLinkClickListener != null && onLinkClickListener.onClick(textView, clickableSpanWithText.text()); + + if (!handled) { + // Let Android handle this click. + clickableSpanWithText.span().onClick(textView); + } + } + + protected void dispatchUrlLongClick(TextView textView, ClickableSpan clickableSpan) { + ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan); + boolean handled = onLinkLongClickListener != null && onLinkLongClickListener.onLongClick(textView, clickableSpanWithText.text()); + + if (!handled) { + // Let Android handle this long click as a short-click. + clickableSpanWithText.span().onClick(textView); + } + } + + protected static final class LongPressTimer implements Runnable { + private OnTimerReachedListener onTimerReachedListener; + + protected interface OnTimerReachedListener { + void onTimerReached(); + } + + @Override + public void run() { + onTimerReachedListener.onTimerReached(); + } + + public void setOnTimerReachedListener(OnTimerReachedListener listener) { + onTimerReachedListener = listener; + } + } + + /** + * A wrapper to support all {@link ClickableSpan}s that may or may not provide URLs. + */ + protected static class ClickableSpanWithText { + private ClickableSpan span; + private String text; + + protected static ClickableSpanWithText ofSpan(TextView textView, ClickableSpan span) { + Spanned s = (Spanned) textView.getText(); + String text; + if (span instanceof URLSpan) { + text = ((URLSpan) span).getURL(); + } else { + int start = s.getSpanStart(span); + int end = s.getSpanEnd(span); + text = s.subSequence(start, end).toString(); + } + return new ClickableSpanWithText(span, text); + } + + protected ClickableSpanWithText(ClickableSpan span, String text) { + this.span = span; + this.text = text; + } + + protected ClickableSpan span() { + return span; + } + + protected String text() { + return text; + } + } +} \ No newline at end of file diff --git a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java index dbee8a6..9861696 100644 --- a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java +++ b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java @@ -3,12 +3,15 @@ import android.graphics.Color; import android.graphics.Typeface; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.text.Editable; import android.text.TextWatcher; +import android.text.InputType; +import android.text.util.Linkify; import android.util.Log; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup.LayoutParams; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; @@ -51,6 +54,9 @@ public class ReactAztecManager extends SimpleViewManager { private static final int FOCUS_TEXT_INPUT = 1; private static final int BLUR_TEXT_INPUT = 2; + private static final int SET_HTML = 3; + private static final int SCROLL_TO_BOTTOM = 4; + private static final int SEND_SPACE_AND_BACKSPACE = 5; private static final int COMMAND_NOTIFY_APPLY_FORMAT = 100; private static final int UNSET = -1; @@ -59,6 +65,9 @@ public class ReactAztecManager extends SimpleViewManager { // see https://github.com/wordpress-mobile/react-native-aztec/pull/79 private int mFocusTextInputCommandCode = FOCUS_TEXT_INPUT; // pre-init private int mBlurTextInputCommandCode = BLUR_TEXT_INPUT; // pre-init + private int mSetHTMLCommandCode = SET_HTML; + private int mScrollToBottomCode = SCROLL_TO_BOTTOM; + private int mSendSpaceAndBackspaceCode = SEND_SPACE_AND_BACKSPACE; private static final String TAG = "ReactAztecText"; @@ -87,6 +96,9 @@ protected ReactAztecText createViewInstance(ThemedReactContext reactContext) { aztecText.setFocusableInTouchMode(true); aztecText.setFocusable(true); aztecText.setCalypsoMode(false); + aztecText.setLinksClickable(true); + aztecText.setAutoLinkMask(Linkify.WEB_URLS); + aztecText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); return aztecText; } @@ -286,6 +298,15 @@ public void setPlaceholderTextColor(ReactAztecText view, @Nullable Integer color } } + @ReactProp(name = "autoCorrect") + public void setAutoCorrect(ReactAztecText view, Boolean autoCorrect) { + if (autoCorrect) { + view.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + } else { + view.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } + } + @ReactProp(name = "maxImagesWidth") public void setMaxImagesWidth(ReactAztecText view, int maxWidth) { view.setMaxImagesWidth(maxWidth); @@ -381,6 +402,9 @@ public Map getCommandsMap() { .put("applyFormat", COMMAND_NOTIFY_APPLY_FORMAT) .put("focusTextInput", mFocusTextInputCommandCode) .put("blurTextInput", mBlurTextInputCommandCode) + .put("setHTML", mSetHTMLCommandCode) + .put("scrollToBottom", mScrollToBottomCode) + .put("sendSpaceAndBackspace", mSendSpaceAndBackspaceCode) .build(); } @@ -398,6 +422,15 @@ public void receiveCommand(final ReactAztecText parent, int commandType, @Nullab } else if (commandType == mBlurTextInputCommandCode) { parent.clearFocusFromJS(); return; + } else if (commandType == mSetHTMLCommandCode) { + final String html = args.getString(0); + setTextfromJS(parent, html); + return; + } else if (commandType == mScrollToBottomCode) { + Log.d("SCROLLING", "1"); + parent.scrollToBottom(); + } else if (commandType == mSendSpaceAndBackspaceCode) { + parent.sendSpaceAndBackspace(); } super.receiveCommand(parent, commandType, args); } @@ -465,14 +498,6 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { return; } - // The event that contains the event counter and updates it must be sent first. - // TODO: t7936714 merge these events - mEventDispatcher.dispatchEvent( - new ReactTextChangedEvent( - mEditText.getId(), - mEditText.toHtml(false), - mEditText.incrementAndGetEventCounter())); - mEventDispatcher.dispatchEvent( new ReactTextInputEvent( mEditText.getId(), @@ -484,6 +509,11 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { @Override public void afterTextChanged(Editable s) { + mEventDispatcher.dispatchEvent( + new ReactTextChangedEvent( + mEditText.getId(), + mEditText.toHtml(false), + mEditText.incrementAndGetEventCounter())); } } @@ -562,3 +592,4 @@ public void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { } } } + diff --git a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index 7a58d09..edeb58d 100644 --- a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -2,11 +2,16 @@ import android.content.Context; import android.graphics.Rect; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.view.inputmethod.InputMethodManager; +import android.text.Spannable; +import android.view.Gravity; +import android.widget.TextView; +import android.view.inputmethod.BaseInputConnection; +import android.view.KeyEvent; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; @@ -19,6 +24,7 @@ import com.facebook.react.views.textinput.ScrollWatcher; import org.wordpress.aztec.AztecText; +import org.wordpress.aztec.AlignmentRendering; import org.wordpress.aztec.AztecTextFormat; import org.wordpress.aztec.ITextFormat; import org.wordpress.aztec.plugins.IAztecPlugin; @@ -56,10 +62,7 @@ public ReactAztecText(ThemedReactContext reactContext) { super(reactContext); this.setAztecKeyListener(new ReactAztecText.OnAztecKeyListener() { @Override - public boolean onEnterKey() { - if (shouldHandleOnEnter) { - return onEnter(); - } + public boolean onEnterKey(Spannable text, boolean firedAfterTextChanged, int selStart, int selEnd) { return false; } @Override @@ -80,6 +83,19 @@ public void onSelectionChanged(int selStart, int selEnd) { } }); this.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + this.setGravity(Gravity.TOP | Gravity.START); + + BetterLinkMovementMethod linkClick = BetterLinkMovementMethod.newInstance(); + + linkClick.setOnLinkClickListener(new BetterLinkMovementMethod.OnLinkClickListener() { + @Override + public boolean onClick(TextView textView, String url) { + hideSoftKeyboard(); + return false; + } + }); + + this.setMovementMethod(linkClick); } @Override @@ -127,10 +143,35 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { }*/ setFocusableInTouchMode(true); boolean focused = super.requestFocus(direction, previouslyFocusedRect); + + final int scrollAmount = this.getLayout().getLineTop(this.getLineCount()) - this.getHeight(); + if (scrollAmount > 0) { + this.scrollTo(0, scrollAmount + 50); + } + + super.setSelection(this.length()); + showSoftKeyboard(); return focused; } + public void sendSpaceAndBackspace() { + BaseInputConnection inputConnection = new BaseInputConnection(this, true); + inputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)); + inputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); + } + + public void scrollToBottom() { + final int scrollAmount = this.getLayout().getLineTop(this.getLineCount()) - this.getHeight() + 50; + + // only scroll if the user is already at the bottom. ignore otherwise + if (scrollAmount > 0 && this.getSelectionStart() >= this.getText().toString().length() - 1) { + this.scrollTo(0, scrollAmount); + + super.setSelection(this.length()); + } + } + private boolean showSoftKeyboard() { return mInputMethodManager.showSoftInput(this, 0); } @@ -194,6 +235,10 @@ private void updateToolbarButtons(ArrayList appliedStyles) { if (currentStyle == AztecTextFormat.FORMAT_STRIKETHROUGH) { formattingOptions.add("strikethrough"); } + + if (currentStyle == AztecTextFormat.FORMAT_UNDERLINE) { + formattingOptions.add("underline"); + } } // Check if the same formatting event was already sent @@ -331,6 +376,9 @@ public void applyFormat(String format) { case ("strikethrough"): newFormats.add(AztecTextFormat.FORMAT_STRIKETHROUGH); break; + case ("underline"): + newFormats.add(AztecTextFormat.FORMAT_UNDERLINE); + break; } if (newFormats.size() == 0) { @@ -413,3 +461,4 @@ public void afterTextChanged(Editable s) { } } } + diff --git a/android/src/main/res/values/betterlinkmovementmethod.xml b/android/src/main/res/values/betterlinkmovementmethod.xml new file mode 100644 index 0000000..eac24ae --- /dev/null +++ b/android/src/main/res/values/betterlinkmovementmethod.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ios/RNTAztecView.xcodeproj/project.pbxproj b/ios/RNTAztecView.xcodeproj/project.pbxproj index 27063dc..3bf45e8 100644 --- a/ios/RNTAztecView.xcodeproj/project.pbxproj +++ b/ios/RNTAztecView.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 397407BB263770E700DE50C1 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397407BA263770E600DE50C1 /* MediaProvider.swift */; }; 7ECFA93C21C39B5000FC131B /* HeadingBlockFormatHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECFA93B21C39B5000FC131B /* HeadingBlockFormatHandler.swift */; }; 7ECFA94021C39BA000FC131B /* BlockFormatHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECFA93F21C39BA000FC131B /* BlockFormatHandler.swift */; }; 7ECFA94221C39BBB00FC131B /* BlockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECFA94121C39BBB00FC131B /* BlockModel.swift */; }; @@ -53,6 +54,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 397407BA263770E600DE50C1 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; 7ECFA93B21C39B5000FC131B /* HeadingBlockFormatHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadingBlockFormatHandler.swift; sourceTree = ""; }; 7ECFA93F21C39BA000FC131B /* BlockFormatHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockFormatHandler.swift; sourceTree = ""; }; 7ECFA94121C39BBB00FC131B /* BlockModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockModel.swift; sourceTree = ""; }; @@ -105,6 +107,7 @@ 7ECFA94121C39BBB00FC131B /* BlockModel.swift */, 7ECFA93B21C39B5000FC131B /* HeadingBlockFormatHandler.swift */, 7ECFA93F21C39BA000FC131B /* BlockFormatHandler.swift */, + 397407BA263770E600DE50C1 /* MediaProvider.swift */, ); path = RNTAztecView; sourceTree = ""; @@ -217,6 +220,7 @@ buildActionMask = 2147483647; files = ( 7ECFA94021C39BA000FC131B /* BlockFormatHandler.swift in Sources */, + 397407BB263770E700DE50C1 /* MediaProvider.swift in Sources */, 7ECFA93C21C39B5000FC131B /* HeadingBlockFormatHandler.swift in Sources */, F13BF4A020ECF5450047D3F9 /* RCTAztecViewManager.swift in Sources */, F1A879D020EE90C000FABD31 /* RCTAztecView.swift in Sources */, @@ -287,7 +291,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -341,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/ios/RNTAztecView/HeadingBlockFormatHandler.swift b/ios/RNTAztecView/HeadingBlockFormatHandler.swift index 214f513..b9bb9c0 100644 --- a/ios/RNTAztecView/HeadingBlockFormatHandler.swift +++ b/ios/RNTAztecView/HeadingBlockFormatHandler.swift @@ -11,15 +11,15 @@ struct HeadingBlockFormatHandler: BlockFormatHandler { } self.level = level headerFormatter = HeaderFormatter(headerLevel: level) - } + } func forceTypingFormat(on textView: RCTAztecView) { - var attributes = textView.typingAttributesSwifted - - attributes = paragraphFormatter.remove(from: attributes) - attributes = headerFormatter.apply(to: attributes, andStore: nil) - - textView.typingAttributesSwifted = attributes +// var attributes = textView.typingAttributesSwifted +// +// attributes = paragraphFormatter.remove(from: attributes) +// attributes = headerFormatter.apply(to: attributes, andStore: nil) +// +// textView.typingAttributesSwifted = attributes } private static func headerLevel(from levelString: String) -> Header.HeaderType? { diff --git a/ios/RNTAztecView/MediaProvider.swift b/ios/RNTAztecView/MediaProvider.swift new file mode 100644 index 0000000..37875ef --- /dev/null +++ b/ios/RNTAztecView/MediaProvider.swift @@ -0,0 +1,61 @@ +import Aztec +import Foundation + +class MediaProvider: Aztec.TextViewAttachmentDelegate { + + func textView(_ textView: TextView, attachment: NSTextAttachment, imageAt url: URL, onSuccess success: @escaping (UIImage) -> Void, onFailure failure: @escaping () -> Void) { + + DispatchQueue.main.async { + let image = UIImage() + + success(image) + } + } + + func textView(_ textView: TextView, urlFor imageAttachment: ImageAttachment) -> URL? { + return URL(string: "www.google.com") + } + + func textView(_ textView: TextView, placeholderFor attachment: NSTextAttachment) -> UIImage { + return UIImage() + } + + func textView(_ textView: TextView, deletedAttachment attachment: MediaAttachment) { + textView.setNeedsDisplay() + } + + func textView(_ textView: TextView, selected attachment: NSTextAttachment, atPosition position: CGPoint) { + } + + func textView(_ textView: TextView, deselected attachment: NSTextAttachment, atPosition position: CGPoint) { + } + + +} + +extension MediaProvider: Aztec.TextViewAttachmentImageProvider { + func textView(_ textView: TextView, shouldRender attachment: NSTextAttachment) -> Bool { + return true + } + + func textView(_ textView: TextView, boundsFor attachment: NSTextAttachment, with lineFragment: CGRect) -> CGRect { + return CGRect(x: 0, y: 0, width: 0, height: 0) + } + + func textView(_ textView: TextView, imageFor attachment: NSTextAttachment, with size: CGSize) -> UIImage? { + let image = UIImage() + + return image; + +// return imageWithImage(image: image, scaledToSize: size) + } + +// func imageWithImage(image:UIImage, scaledToSize newSize:CGSize) -> UIImage{ +// UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0); +// image.draw(in: CGRect(origin: CGPoint.zero, size: CGSize(width: newSize.width, height: newSize.height))) +// let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()! +// UIGraphicsEndImageContext() +// return newImage +// } + +} diff --git a/ios/RNTAztecView/RCTAztecView.swift b/ios/RNTAztecView/RCTAztecView.swift index 3eafeef..2bcdcf8 100644 --- a/ios/RNTAztecView/RCTAztecView.swift +++ b/ios/RNTAztecView/RCTAztecView.swift @@ -2,6 +2,8 @@ import Aztec import Foundation import UIKit +var lastTimeShouldChangeCalled = 0.0; + class RCTAztecView: Aztec.TextView { @objc var onBackspace: RCTBubblingEventBlock? = nil @objc var onChange: RCTBubblingEventBlock? = nil @@ -12,6 +14,7 @@ class RCTAztecView: Aztec.TextView { @objc var onSelectionChange: RCTBubblingEventBlock? = nil @objc var onActiveFormatsChange: RCTBubblingEventBlock? = nil @objc var onActiveFormatAttributesChange: RCTBubblingEventBlock? = nil + @objc var autoCorrect: Bool = false @objc var blockType: NSDictionary? = nil { didSet { guard let block = blockType, let tag = block["tag"] as? String else { @@ -27,6 +30,10 @@ class RCTAztecView: Aztec.TextView { } } + override func didSetProps(_ changedProps: [String]!) { + autocorrectionType = self.autoCorrect ? .yes : .no + } + private var previousContentSize: CGSize = .zero private lazy var placeholderLabel: UILabel = { @@ -37,6 +44,7 @@ class RCTAztecView: Aztec.TextView { private let formatStringMap: [FormattingIdentifier: String] = [ .bold: "bold", .italic: "italic", + .underline: "underline", .strikethrough: "strikethrough", .link: "link", ] @@ -44,6 +52,8 @@ class RCTAztecView: Aztec.TextView { override init(defaultFont: UIFont, defaultParagraphStyle: ParagraphStyle, defaultMissingImage: UIImage) { super.init(defaultFont: defaultFont, defaultParagraphStyle: defaultParagraphStyle, defaultMissingImage: defaultMissingImage) commonInit() + + } required init?(coder aDecoder: NSCoder) { @@ -61,6 +71,7 @@ class RCTAztecView: Aztec.TextView { placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: contentInset.left + textContainerInset.left + textContainer.lineFragmentPadding), placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: contentInset.top + textContainerInset.top) ]) + autocorrectionType = autoCorrect ? .yes : .no; } // MARK - View Height: Match to content height @@ -87,9 +98,9 @@ class RCTAztecView: Aztec.TextView { // MARK: - Edits open override func insertText(_ text: String) { - guard !interceptEnter(text) else { - return - } +// guard !interceptEnter(text) else { +// return +// } super.insertText(text) updatePlaceholderVisibility() @@ -210,6 +221,7 @@ class RCTAztecView: Aztec.TextView { switch format { case "bold": toggleBold(range: selectedRange) case "italic": toggleItalic(range: selectedRange) + case "underline": toggleUnderline(range: selectedRange) case "strikethrough": toggleStrikethrough(range: selectedRange) default: print("Format not recognized") } @@ -234,6 +246,11 @@ class RCTAztecView: Aztec.TextView { } removeLink(inRange: expandedRange) } + + @objc + func hideKeyboard() { + self.resignFirstResponder() + } func linkAttributes() -> [String: Any] { var attributes: [String: Any] = ["isActive": false] @@ -301,7 +318,24 @@ extension RCTAztecView: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { forceTypingAttributesIfNeeded() propagateFormatChanges() - propagateContentChanges() + + let nowTime = NSDate().timeIntervalSince1970 + + if (nowTime - Double(lastTimeShouldChangeCalled) > 0.1) { + propagateContentChanges() + } + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + lastTimeShouldChangeCalled = NSDate().timeIntervalSince1970 + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.forceTypingAttributesIfNeeded() + self.propagateFormatChanges() + self.propagateContentChanges() + } + + return true } func textViewDidBeginEditing(_ textView: UITextView) { diff --git a/ios/RNTAztecView/RCTAztecViewManager.m b/ios/RNTAztecView/RCTAztecViewManager.m index 3a8858e..6bae310 100644 --- a/ios/RNTAztecView/RCTAztecViewManager.m +++ b/ios/RNTAztecView/RCTAztecViewManager.m @@ -18,8 +18,13 @@ @interface RCT_EXTERN_MODULE(RCTAztecViewManager, NSObject) RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL) + RCT_EXTERN_METHOD(applyFormat:(nonnull NSNumber *)node format:(NSString *)format) RCT_EXTERN_METHOD(setLink:(nonnull NSNumber *)node url:(nonnull NSString *)url title:(nullable NSString *)title) RCT_EXTERN_METHOD(removeLink:(nonnull NSNumber *)node) +RCT_EXTERN_METHOD(focusTextInput:(nonnull NSNumber *)node) +RCT_EXTERN_METHOD(blurTextInput:(nonnull NSNumber *)node) +RCT_EXTERN_METHOD(setHTML:(nonnull NSNumber *)node html:(NSString *)html) @end diff --git a/ios/RNTAztecView/RCTAztecViewManager.swift b/ios/RNTAztecView/RCTAztecViewManager.swift index bb445c4..ea937f9 100644 --- a/ios/RNTAztecView/RCTAztecViewManager.swift +++ b/ios/RNTAztecView/RCTAztecViewManager.swift @@ -4,9 +4,12 @@ import Foundation @objc (RCTAztecViewManager) public class RCTAztecViewManager: RCTViewManager { - public var attachmentDelegate: Aztec.TextViewAttachmentDelegate? - public var imageProvider: Aztec.TextViewAttachmentImageProvider? - + public static var attachmentDelegate: Aztec.TextViewAttachmentDelegate? + public static var imageProvider: Aztec.TextViewAttachmentImageProvider? + + @objc + public static var defaultFont: UIFont? + public override static func requiresMainQueueSetup() -> Bool { return true } @@ -17,7 +20,32 @@ public class RCTAztecViewManager: RCTViewManager { aztecView.apply(format: format) }, onNode: node) } + + @objc + func focusTextInput(_ node: NSNumber) { + executeBlock({ (aztecView) in + aztecView.becomeFirstResponder() + + let newPosition = aztecView.endOfDocument + aztecView.selectedTextRange = aztecView.textRange(from: newPosition, to: newPosition) + }, onNode: node) + } + + @objc + func setHTML(_ node: NSNumber, html: String) { + executeBlock({ (aztecView) in + aztecView.setHTML(html) + aztecView.updatePlaceholderVisibility() + }, onNode: node) + } + @objc + func blurTextInput(_ node: NSNumber) { + executeBlock({ (aztecView) in + aztecView.hideKeyboard() + }, onNode: node) + } + @objc func removeLink(_ node: NSNumber) { executeBlock({ (aztecView) in @@ -31,21 +59,43 @@ public class RCTAztecViewManager: RCTViewManager { aztecView.setLink(with: url, and: title) }, onNode: node) } - + + + @objc public override func view() -> UIView { + if (RCTAztecViewManager.defaultFont == nil) { + RCTAztecViewManager.defaultFont = UIFont(name: "Inter-Regular", size: 16.0); + } let view = RCTAztecView( defaultFont: defaultFont, defaultParagraphStyle: .default, defaultMissingImage: UIImage()) - view.isScrollEnabled = false + view.isScrollEnabled = true + + view.autocorrectionType = .no + + let defaultMediaProvider = MediaProvider() + + if (RCTAztecViewManager.attachmentDelegate == nil) { + RCTAztecViewManager.attachmentDelegate = defaultMediaProvider + } + + if (RCTAztecViewManager.imageProvider == nil) { + RCTAztecViewManager.imageProvider = defaultMediaProvider + } - view.textAttachmentDelegate = attachmentDelegate - if let imageProvider = imageProvider { + view.textAttachmentDelegate = RCTAztecViewManager.attachmentDelegate + + if let imageProvider = RCTAztecViewManager.imageProvider { view.registerAttachmentImageProvider(imageProvider) } - + + if #available(iOS 13, *) { + view.overrideUserInterfaceStyle = .light + } + return view } @@ -59,20 +109,20 @@ public class RCTAztecViewManager: RCTViewManager { } } - private var defaultFont: UIFont { - if let font = UIFont(name: "NotoSerif", size: 16) { - return font - } + private var defaultFont: UIFont { + if let font = UIFont(name: "NotoSerif", size: 16) { + return font + } - let defaultFont = UIFont.systemFont(ofSize: 16) - guard let url = Bundle.main.url(forResource: "NotoSerif-Regular", withExtension: "ttf") else { - return defaultFont - } - CTFontManagerRegisterFontsForURL(url as CFURL, CTFontManagerScope.process, nil) - if let font = UIFont(name: "NotoSerif", size: 16) { - return font - } + let defaultFont = UIFont.systemFont(ofSize: 16) + guard let url = Bundle.main.url(forResource: "NotoSerif-Regular", withExtension: "ttf") else { + return defaultFont + } + CTFontManagerRegisterFontsForURL(url as CFURL, CTFontManagerScope.process, nil) + if let font = UIFont(name: "NotoSerif", size: 16) { + return font + } - return defaultFont - } + return defaultFont + } } diff --git a/package.json b/package.json index 5fe3c65..4ff60eb 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,59 @@ { - "name": "react-native-aztec", - "version": "0.1.5", - "license": "(MPL-2.0 OR GPL-2.0)", - "scripts": { - "install-aztec-ios": "cd ./ios && carthage bootstrap --platform iOS --cache-builds", - "clean": "yarn clean-watchman; yarn clean-node; yarn clean-react; yarn clean-metro; yarn clean-jest;", - "clean-jest": "rm -rf $TMPDIR/jest_*;", - "clean-metro": "rm -rf $TMPDIR/metro-cache-*; rm -rf $TMPDIR/metro-bundler-cache-*;", - "clean-node": "rm -rf node_modules/;", - "clean-react": "rm -rf $TMPDIR/react-*; rm -rf $TMPDIR/react-native-packager-cache-*;", - "clean-watchman": "command -v watchman >/dev/null 2>&1 && watchman watch-del-all;", - "clean:install": "yarn clean && yarn install" + "_from": "git+https://github.com/ewindso/react-native-aztec.git", + "_id": "react-native-aztec@0.1.5", + "_inBundle": false, + "_integrity": "", + "_location": "/react-native-aztec", + "_phantomChildren": { + "asap": "2.0.6", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.4.0", + "object-assign": "4.1.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.23" }, - "peerDependencies": { - "react": "16.6.1", - "react-native": "0.57.5" + "_requested": { + "type": "git", + "raw": "react-native-aztec@git+https://github.com/ewindso/react-native-aztec.git", + "name": "react-native-aztec", + "escapedName": "react-native-aztec", + "rawSpec": "git+https://github.com/ewindso/react-native-aztec.git", + "saveSpec": "git+https://github.com/ewindso/react-native-aztec.git", + "fetchSpec": "https://github.com/ewindso/react-native-aztec.git", + "gitCommittish": null }, + "_requiredBy": [ + "/" + ], + "_resolved": "git+https://github.com/ewindso/react-native-aztec.git#ff1999f5e6075c4361e1b1051e66a45f7007bc6e", + "_spec": "react-native-aztec@git+https://github.com/ewindso/react-native-aztec.git", + "_where": "/Users/elijahwindsor/Github/GrowthDayMobile", + "bundleDependencies": false, "dependencies": { "prop-types": "15.6.0" }, + "deprecated": false, + "description": "# react-native-aztec", "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-flow": "^6.23.0", "flow-bin": "^0.69.0" - } + }, + "license": "(MPL-2.0 OR GPL-2.0)", + "name": "react-native-aztec", + "peerDependencies": { + "react": "16.13.1", + "react-native": "0.63.2" + }, + "scripts": { + "clean": "yarn clean-watchman; yarn clean-node; yarn clean-react; yarn clean-metro; yarn clean-jest;", + "clean-jest": "rm -rf $TMPDIR/jest_*;", + "clean-metro": "rm -rf $TMPDIR/metro-cache-*; rm -rf $TMPDIR/metro-bundler-cache-*;", + "clean-node": "rm -rf node_modules/;", + "clean-react": "rm -rf $TMPDIR/react-*; rm -rf $TMPDIR/react-native-packager-cache-*;", + "clean-watchman": "command -v watchman >/dev/null 2>&1 && watchman watch-del-all;", + "clean:install": "yarn clean && yarn install", + "install-aztec-ios": "cd ./ios && carthage bootstrap --platform iOS --cache-builds" + }, + "version": "0.1.5" } diff --git a/src/AztecView.js b/src/AztecView.js index f1cb465..10ca440 100644 --- a/src/AztecView.js +++ b/src/AztecView.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ReactNative, {requireNativeComponent, ViewPropTypes, UIManager, ColorPropType, TouchableWithoutFeedback} from 'react-native'; -import TextInputState from 'react-native/lib/TextInputState'; +import ReactNative, {Platform, ViewPropTypes, UIManager, ColorPropType, TouchableWithoutFeedback} from 'react-native'; +import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; +import RCTAztecView from './RCTAztecView'; const AztecManager = UIManager.RCTAztecView; @@ -14,6 +15,7 @@ class AztecView extends React.Component { text: PropTypes.object, placeholder: PropTypes.string, placeholderTextColor: ColorPropType, + autoCorrect: PropTypes.boolean, color: ColorPropType, maxImagesWidth: PropTypes.number, minImagesWidth: PropTypes.number, @@ -33,15 +35,22 @@ class AztecView extends React.Component { ...ViewPropTypes, // include the default view properties } - dispatch(command, params) { + dispatch = (command, params) => { params = params || []; - UIManager.dispatchViewManagerCommand( - ReactNative.findNodeHandle(this), - command, - params, - ); + + try { + UIManager.dispatchViewManagerCommand( + ReactNative.findNodeHandle(this), + command, + params, + ); + } catch(error) { + console.log({error}) + } } + _inputRef = null; + applyFormat(format) { this.dispatch(AztecManager.Commands.applyFormat, [format]) } @@ -54,6 +63,33 @@ class AztecView extends React.Component { this.dispatch(AztecManager.Commands.setLink, [url, title]) } + focus() { + } + + focusEndOfDocument() { + this.dispatch(AztecManager.Commands.focusTextInput) + } + + blur() { + this.dispatch(AztecManager.Commands.blurTextInput) + } + + setHTML(html) { + this.dispatch(AztecManager.Commands.setHTML, [html]) + } + + scrollToBottom() { + if (Platform.OS === 'android') { + this.dispatch(AztecManager.Commands.scrollToBottom); + } + } + + sendSpaceAndBackspace() { + if(Platform.OS === 'android') { + this.dispatch(AztecManager.Commands.sendSpaceAndBackspace); + } + } + requestHTMLWithCursor() { this.dispatch(AztecManager.Commands.returnHTMLWithCursor) } @@ -83,6 +119,8 @@ class AztecView extends React.Component { const size = event.nativeEvent.contentSize; const { onContentSizeChange } = this.props; onContentSizeChange(size); + + this.scrollToBottom(); } _onEnter = (event) => { @@ -126,7 +164,6 @@ class AztecView extends React.Component { _onBlur = (event) => { this.selectionEndCaretY = null; - TextInputState.blurTextInput(ReactNative.findNodeHandle(this)); if (!this.props.onBlur) { return; @@ -151,14 +188,6 @@ class AztecView extends React.Component { } } - blur = () => { - TextInputState.blurTextInput(ReactNative.findNodeHandle(this)); - } - - focus = () => { - TextInputState.focusTextInput(ReactNative.findNodeHandle(this)); - } - isFocused = () => { const focusedField = TextInputState.currentlyFocusedField(); return focusedField && ( focusedField === ReactNative.findNodeHandle(this) ); @@ -167,6 +196,12 @@ class AztecView extends React.Component { _onPress = (event) => { this.focus(event); // Call to move the focus in RN way (TextInputState) this._onFocus(event); // Check if there are listeners set on the focus event + + if (Platform.OS === 'android') { + setTimeout(() => { + this.sendSpaceAndBackspace(); + }, 250); + } } render() { @@ -174,6 +209,7 @@ class AztecView extends React.Component { return ( this._inputRef = ref} onActiveFormatsChange={ this._onActiveFormatsChange } onActiveFormatAttributesChange={ this._onActiveFormatAttributesChange } onContentSizeChange = { this._onContentSizeChange } @@ -192,6 +228,4 @@ class AztecView extends React.Component { } } -const RCTAztecView = requireNativeComponent('RCTAztecView', AztecView); - export default AztecView \ No newline at end of file diff --git a/src/RCTAztecView.js b/src/RCTAztecView.js new file mode 100644 index 0000000..28ee0c9 --- /dev/null +++ b/src/RCTAztecView.js @@ -0,0 +1,5 @@ +import {requireNativeComponent} from 'react-native'; + +const RCTAztecView = requireNativeComponent('RCTAztecView'); + +export default RCTAztecView \ No newline at end of file