diff --git a/android/build.gradle b/android/build.gradle index aae3b77..19aa20d 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.16' } repositories { 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 aa33d5b..8428986 100644 --- a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java +++ b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java @@ -39,6 +39,8 @@ import org.wordpress.aztec.plugins.wpcomments.toolbar.MoreToolbarButton; import java.util.Map; +import java.util.ArrayList; +import java.util.Arrays; public class ReactAztecManager extends SimpleViewManager { @@ -46,7 +48,6 @@ 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 COMMAND_NOTIFY_APPLY_FORMAT = 100; // we define the same codes in ReactAztecText as they have for ReactNative's TextInput, so // it's easier to handle focus between Aztec and TextInput instances on the same screen. @@ -171,6 +172,19 @@ private void setTextfromJS(ReactAztecText view, String text) { view.setIsSettingTextFromJS(false); } + @ReactProp(name = "activeFormats", defaultBoolean = false) + public void setActiveFormats(final ReactAztecText view, @Nullable ReadableArray activeFormats) { + if (activeFormats != null) { + String[] activeFormatsArray = new String[activeFormats.size()]; + for (int i = 0; i < activeFormats.size(); i++) { + activeFormatsArray[i] = activeFormats.getString(i); + } + view.setActiveFormats(Arrays.asList(activeFormatsArray)); + } else { + view.setActiveFormats(new ArrayList()); + } + } + @ReactProp(name = "color", customType = "Color") public void setColor(ReactAztecText view, @Nullable Integer color) { int newColor = Color.BLACK; @@ -254,11 +268,6 @@ public void setOnContentSizeChange(final ReactAztecText view, boolean onContentS } } - @ReactProp(name = "onActiveFormatsChange", defaultBoolean = false) - public void setOnActiveFormatsChange(final ReactAztecText view, boolean onActiveFormatsChange) { - view.shouldHandleActiveFormatsChange = onActiveFormatsChange; - } - @ReactProp(name = "onSelectionChange", defaultBoolean = false) public void setOnSelectionChange(final ReactAztecText view, boolean onSelectionChange) { view.shouldHandleOnSelectionChange = onSelectionChange; @@ -286,7 +295,6 @@ public void setOnBackspaceHandling(final ReactAztecText view, boolean onBackspac @Override public Map getCommandsMap() { return MapBuilder.builder() - .put("applyFormat", COMMAND_NOTIFY_APPLY_FORMAT) .put("focusTextInput", mFocusTextInputCommandCode) .put("blurTextInput", mBlurTextInputCommandCode) .build(); @@ -295,12 +303,7 @@ public Map getCommandsMap() { @Override public void receiveCommand(final ReactAztecText parent, int commandType, @Nullable ReadableArray args) { Assertions.assertNotNull(parent); - if (commandType == COMMAND_NOTIFY_APPLY_FORMAT) { - final String format = args.getString(0); - Log.d(TAG, String.format("Apply format: %s", format)); - parent.applyFormat(format); - return; - } else if (commandType == mFocusTextInputCommandCode) { + if (commandType == mFocusTextInputCommandCode) { parent.requestFocusFromJS(); return; } else if (commandType == mBlurTextInputCommandCode) { 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..78b0c99 100644 --- a/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -26,6 +26,10 @@ import java.util.ArrayList; import java.util.LinkedList; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; public class ReactAztecText extends AztecText { @@ -52,6 +56,16 @@ public class ReactAztecText extends AztecText { boolean shouldHandleOnSelectionChange = false; boolean shouldHandleActiveFormatsChange = false; + private static final HashMap typingFormatsMap = new HashMap() { + { + put(AztecTextFormat.FORMAT_BOLD, "bold"); + put(AztecTextFormat.FORMAT_STRONG, "bold"); + put(AztecTextFormat.FORMAT_ITALIC, "italic"); + put(AztecTextFormat.FORMAT_CITE, "italic"); + put(AztecTextFormat.FORMAT_STRIKETHROUGH, "bold"); + } + }; + public ReactAztecText(ThemedReactContext reactContext) { super(reactContext); this.setAztecKeyListener(new ReactAztecText.OnAztecKeyListener() { @@ -316,66 +330,27 @@ private boolean onBackspace() { return true; } - public void applyFormat(String format) { - ArrayList newFormats = new ArrayList<>(); - switch (format) { - case ("bold"): - case ("strong"): - newFormats.add(AztecTextFormat.FORMAT_STRONG); - newFormats.add(AztecTextFormat.FORMAT_BOLD); - break; - case ("italic"): - newFormats.add(AztecTextFormat.FORMAT_ITALIC); - newFormats.add(AztecTextFormat.FORMAT_CITE); - break; - case ("strikethrough"): - newFormats.add(AztecTextFormat.FORMAT_STRIKETHROUGH); - break; - } - - if (newFormats.size() == 0) { - return; - } - - if (!isTextSelected()) { - final ArrayList newStylesList = getNewStylesList(newFormats); - setSelectedStyles(newStylesList); - // Update the toolbar state - updateToolbarButtons(newStylesList); - } else { - toggleFormatting(newFormats.get(0)); - // Update the toolbar state - updateToolbarButtons(getSelectionStart(), getSelectionEnd()); - } - - // emit onChange because the underlying HTML has changed applying the style - ReactContext reactContext = (ReactContext) getContext(); - EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); - eventDispatcher.dispatchEvent( - new ReactTextChangedEvent( - getId(), - toHtml(false), - incrementAndGetEventCounter()) - ); - } - - // Removes all formats in the list but if none found, applies the first one - private ArrayList getNewStylesList(ArrayList newFormats) { - ArrayList textFormats = new ArrayList<>(); - textFormats.addAll(getSelectedStyles()); - boolean wasRemoved = false; - for (ITextFormat newFormat : newFormats) { - if (textFormats.contains(newFormat)) { - wasRemoved = true; - textFormats.remove(newFormat); + public void setActiveFormats(Iterable newFormats) { + Set selectedStylesSet = new HashSet<>(getSelectedStyles()); + Set newFormatsSet = new HashSet<>(); + for (String newFormat : newFormats) { + switch (newFormat) { + case "bold": + newFormatsSet.add(AztecTextFormat.FORMAT_STRONG); + break; + case "italic": + newFormatsSet.add(AztecTextFormat.FORMAT_CITE); + break; + case "strikethrough": + newFormatsSet.add(AztecTextFormat.FORMAT_STRIKETHROUGH); + break; } } - - if (!wasRemoved) { - textFormats.add(newFormats.get(0)); - } - - return textFormats; + selectedStylesSet.removeAll(typingFormatsMap.keySet()); + selectedStylesSet.addAll(newFormatsSet); + ArrayList newStylesList = new ArrayList<>(selectedStylesSet); + setSelectedStyles(newStylesList); + updateToolbarButtons(newStylesList); } /** diff --git a/ios/RNTAztecView/RCTAztecView.swift b/ios/RNTAztecView/RCTAztecView.swift index 3eafeef..f31c487 100644 --- a/ios/RNTAztecView/RCTAztecView.swift +++ b/ios/RNTAztecView/RCTAztecView.swift @@ -10,8 +10,6 @@ class RCTAztecView: Aztec.TextView { @objc var onBlur: RCTBubblingEventBlock? = nil @objc var onContentSizeChange: RCTBubblingEventBlock? = nil @objc var onSelectionChange: RCTBubblingEventBlock? = nil - @objc var onActiveFormatsChange: RCTBubblingEventBlock? = nil - @objc var onActiveFormatAttributesChange: RCTBubblingEventBlock? = nil @objc var blockType: NSDictionary? = nil { didSet { guard let block = blockType, let tag = block["tag"] as? String else { @@ -20,6 +18,14 @@ class RCTAztecView: Aztec.TextView { blockModel = BlockModel(tag: tag) } } + @objc var activeFormats: NSSet? = nil { + didSet { + let currentTypingAttributes = formattingIdentifiersForTypingAttributes() + for (key, value) in formatStringMap where currentTypingAttributes.contains(key) != activeFormats?.contains(value) { + toggleFormat(format: value) + } + } + } var blockModel = BlockModel(tag: "") { didSet { @@ -206,44 +212,16 @@ class RCTAztecView: Aztec.TextView { // MARK: - Formatting interface - @objc func apply(format: String) { + @objc func toggleFormat(format: String) { + let emptyRange = NSRange(location: selectedRange.location, length: 0) switch format { - case "bold": toggleBold(range: selectedRange) - case "italic": toggleItalic(range: selectedRange) - case "strikethrough": toggleStrikethrough(range: selectedRange) + case "bold": toggleBold(range: emptyRange) + case "italic": toggleItalic(range: emptyRange) + case "strikethrough": toggleStrikethrough(range: emptyRange) default: print("Format not recognized") } } - @objc - func setLink(with url: String, and title: String?) { - guard let url = URL(string: url) else { - return - } - if let title = title { - setLink(url, title: title, inRange: selectedRange) - } else { - setLink(url, inRange: selectedRange) - } - } - - @objc - func removeLink() { - guard let expandedRange = linkFullRange(forRange: selectedRange) else { - return - } - removeLink(inRange: expandedRange) - } - - func linkAttributes() -> [String: Any] { - var attributes: [String: Any] = ["isActive": false] - if let expandedRange = linkFullRange(forRange: selectedRange) { - attributes["url"] = linkURL(forRange: expandedRange)?.absoluteString ?? "" - attributes["isActive"] = true - } - return attributes - } - func forceTypingAttributesIfNeeded() { if let formatHandler = HeadingBlockFormatHandler(block: blockModel) { formatHandler.forceTypingFormat(on: self) @@ -259,27 +237,6 @@ class RCTAztecView: Aztec.TextView { } } - func propagateFormatChanges() { - guard let onActiveFormatsChange = onActiveFormatsChange else { - return - } - let identifiers: Set - if selectedRange.length > 0 { - identifiers = formattingIdentifiersSpanningRange(selectedRange) - } else { - identifiers = formattingIdentifiersForTypingAttributes() - } - let formats = identifiers.compactMap { formatStringMap[$0] } - onActiveFormatsChange(["formats": formats]) - } - - func propagateAttributesChanges() { - let attributes: [String: [String: Any]] = [ - "link": linkAttributes() - ] - onActiveFormatAttributesChange?(["attributes": attributes]) - } - func propagateSelectionChanges() { guard let onSelectionChange = onSelectionChange else { return @@ -293,14 +250,11 @@ class RCTAztecView: Aztec.TextView { extension RCTAztecView: UITextViewDelegate { func textViewDidChangeSelection(_ textView: UITextView) { - propagateAttributesChanges() - propagateFormatChanges() propagateSelectionChanges() } func textViewDidChange(_ textView: UITextView) { forceTypingAttributesIfNeeded() - propagateFormatChanges() propagateContentChanges() } diff --git a/ios/RNTAztecView/RCTAztecViewManager.m b/ios/RNTAztecView/RCTAztecViewManager.m index 3a8858e..0a2029b 100644 --- a/ios/RNTAztecView/RCTAztecViewManager.m +++ b/ios/RNTAztecView/RCTAztecViewManager.m @@ -11,6 +11,7 @@ @interface RCT_EXTERN_MODULE(RCTAztecViewManager, NSObject) RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(blockType, NSDictionary) +RCT_EXPORT_VIEW_PROPERTY(activeFormats, NSSet) RCT_EXPORT_VIEW_PROPERTY(onActiveFormatsChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onActiveFormatAttributesChange, RCTBubblingEventBlock) diff --git a/ios/RNTAztecView/RCTAztecViewManager.swift b/ios/RNTAztecView/RCTAztecViewManager.swift index bb445c4..6fe4108 100644 --- a/ios/RNTAztecView/RCTAztecViewManager.swift +++ b/ios/RNTAztecView/RCTAztecViewManager.swift @@ -11,27 +11,6 @@ public class RCTAztecViewManager: RCTViewManager { return true } - @objc - func applyFormat(_ node: NSNumber, format: String) { - executeBlock({ (aztecView) in - aztecView.apply(format: format) - }, onNode: node) - } - - @objc - func removeLink(_ node: NSNumber) { - executeBlock({ (aztecView) in - aztecView.removeLink() - }, onNode: node) - } - - @objc - func setLink(_ node: NSNumber, url: String, title: String?) { - executeBlock({ (aztecView) in - aztecView.setLink(with: url, and: title) - }, onNode: node) - } - @objc public override func view() -> UIView { let view = RCTAztecView( diff --git a/src/AztecView.js b/src/AztecView.js index f1cb465..0e6a8b2 100644 --- a/src/AztecView.js +++ b/src/AztecView.js @@ -9,6 +9,7 @@ class AztecView extends React.Component { selectionEndCaretY: number; static propTypes = { + activeFormats: PropTypes.array, isSelected: PropTypes.bool, disableGutenbergMode: PropTypes.bool, text: PropTypes.object, @@ -24,8 +25,6 @@ class AztecView extends React.Component { onEnter: PropTypes.func, onBackspace: PropTypes.func, onScroll: PropTypes.func, - onActiveFormatsChange: PropTypes.func, - onActiveFormatAttributesChange: PropTypes.func, onSelectionChange: PropTypes.func, onHTMLContentWithCursor: PropTypes.func, onCaretVerticalPositionChange: PropTypes.func, @@ -42,40 +41,10 @@ class AztecView extends React.Component { ); } - applyFormat(format) { - this.dispatch(AztecManager.Commands.applyFormat, [format]) - } - - removeLink() { - this.dispatch(AztecManager.Commands.removeLink) - } - - setLink(url, title) { - this.dispatch(AztecManager.Commands.setLink, [url, title]) - } - requestHTMLWithCursor() { this.dispatch(AztecManager.Commands.returnHTMLWithCursor) } - _onActiveFormatsChange = (event) => { - if (!this.props.onActiveFormatsChange) { - return; - } - const formats = event.nativeEvent.formats; - const { onActiveFormatsChange } = this.props; - onActiveFormatsChange(formats); - } - - _onActiveFormatAttributesChange = (event) => { - if (!this.props.onActiveFormatAttributesChange) { - return; - } - const attributes = event.nativeEvent.attributes; - const { onActiveFormatAttributesChange } = this.props; - onActiveFormatAttributesChange(attributes); - } - _onContentSizeChange = (event) => { if (!this.props.onContentSizeChange) { return; @@ -174,8 +143,6 @@ class AztecView extends React.Component { return (