Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,12 @@ class EnrichedTextInputViewManager :
view?.toggleCheckboxListItem(isChecked)
}

// TODO: nested lists to be implemented on Android, no-op placeholder for now
override fun indentList(view: EnrichedTextInputView?) = Unit

// TODO: nested lists to be implemented on Android, no-op placeholder for now
override fun outdentList(view: EnrichedTextInputView?) = Unit

override fun addLink(
view: EnrichedTextInputView?,
start: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ class EnrichedSpanState(
val isConflicting = conflictingList?.any { activeStyles.contains(it) } ?: false
state.putBoolean("isConflicting", isConflicting)

// TODO: nested lists to be implemented on Android, depth is a placeholder (always 0)
if (type == EnrichedSpans.ORDERED_LIST || type == EnrichedSpans.UNORDERED_LIST || type == EnrichedSpans.CHECKBOX_LIST) {
state.putInt("depth", 0)
}

return state
}

Expand Down
14 changes: 14 additions & 0 deletions apps/example/src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ const STYLE_ITEMS = [
name: 'checkbox-list',
icon: 'check-square-o',
},
{
name: 'indent',
icon: 'indent',
},
{
name: 'outdent',
icon: 'outdent',
},
{
name: 'align-left',
icon: 'align-left',
Expand Down Expand Up @@ -171,6 +179,12 @@ export const Toolbar: FC<ToolbarProps> = ({
// Make checkbox checked by default
editorRef.current?.toggleCheckboxList(true);
break;
case 'indent':
editorRef.current?.indentList();
break;
case 'outdent':
editorRef.current?.outdentList();
break;
case 'link':
onOpenLinkModal();
break;
Expand Down
11 changes: 8 additions & 3 deletions apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const DEFAULT_STYLE_STATE = {
isBlocking: false,
};

export const DEFAULT_LIST_STYLE_STATE = {
...DEFAULT_STYLE_STATE,
depth: 0,
};

export const DEFAULT_STYLES: StylesState = {
bold: DEFAULT_STYLE_STATE,
italic: DEFAULT_STYLE_STATE,
Expand All @@ -26,12 +31,12 @@ export const DEFAULT_STYLES: StylesState = {
h6: DEFAULT_STYLE_STATE,
blockQuote: DEFAULT_STYLE_STATE,
codeBlock: DEFAULT_STYLE_STATE,
orderedList: DEFAULT_STYLE_STATE,
unorderedList: DEFAULT_STYLE_STATE,
orderedList: DEFAULT_LIST_STYLE_STATE,
unorderedList: DEFAULT_LIST_STYLE_STATE,
link: DEFAULT_STYLE_STATE,
image: DEFAULT_STYLE_STATE,
mention: DEFAULT_STYLE_STATE,
checkboxList: DEFAULT_STYLE_STATE,
checkboxList: DEFAULT_LIST_STYLE_STATE,
alignment: 'auto',
};

Expand Down
123 changes: 115 additions & 8 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
#import "utils/AttachmentLayoutUtils.h"
#import "utils/DotReplacementUtils.h"
#import "utils/KeyboardUtils.h"
#import "utils/ListIntegrityUtils.h"
#import "utils/ParagraphAttributesUtils.h"
#import "utils/ShortcutsUtils.h"
#import "utils/StyleUtils.h"
#import "utils/TextBlockTapGestureRecognizer.h"
#import "utils/TextInsertionUtils.h"
#import "utils/TextListsUtils.h"
#import "utils/WordsUtils.h"
#import "utils/ZeroWidthSpaceUtils.h"
#import <React/RCTConversions.h>
Expand All @@ -33,6 +35,14 @@
.isConflicting = [self isStyle:TYPE_ENUM activeInMap:conflictingStyles] \
}

#define GET_LIST_STYLE_STATE(TYPE_ENUM) \
{ \
.isActive = [self isStyleActive:TYPE_ENUM], \
.isBlocking = [self isStyle:TYPE_ENUM activeInMap:blockingStyles], \
.isConflicting = [self isStyle:TYPE_ENUM activeInMap:conflictingStyles], \
.depth = static_cast<int>([self listDepthAtSelectionForType:TYPE_ENUM]) \
}

using namespace facebook::react;

@interface EnrichedTextInputView () <
Expand Down Expand Up @@ -62,6 +72,7 @@ @implementation EnrichedTextInputView {
NSString *_submitBehavior;
NSDictionary<NSAttributedStringKey, id> *_capturedAttributesBeforeChange;
NSString *_recentlyEmittedAlignment;
NSMutableDictionary<NSNumber *, NSNumber *> *_recentlyEmittedListDepths;
}

@synthesize blockEmitting = blockEmitting;
Expand Down Expand Up @@ -133,6 +144,7 @@ - (void)setDefaults {
_recentlyActiveLinkRange = NSMakeRange(0, 0);
_recentlyActiveMentionRange = NSMakeRange(0, 0);
_recentlyEmittedAlignment = @"left";
_recentlyEmittedListDepths = [[NSMutableDictionary alloc] init];
_recentInputString = @"";
_recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
_emitHtml = NO;
Expand Down Expand Up @@ -1021,6 +1033,9 @@ - (void)tryUpdatingActiveStyles {
// separately
NSMutableSet *newBlockedStyles = [_blockedStyles mutableCopy];

NSMutableDictionary<NSNumber *, NSNumber *> *newListDepths =
[_recentlyEmittedListDepths mutableCopy];

// data for onLinkDetected event
LinkData *detectedLinkData;
NSRange detectedLinkRange = NSMakeRange(0, 0);
Expand Down Expand Up @@ -1060,6 +1075,16 @@ - (void)tryUpdatingActiveStyles {
}
}

if ([style isList]) {
NSInteger currentDepth =
[self listDepthAtSelectionForType:(StyleType)[type integerValue]];
NSNumber *previousDepth = newListDepths[type];
if (previousDepth == nil || previousDepth.integerValue != currentDepth) {
updateNeeded = YES;
newListDepths[type] = @(currentDepth);
}
}

// onLinkDetected event
if ([type intValue] == [LinkStyle getType]) {
if (isActive) {
Expand Down Expand Up @@ -1142,6 +1167,7 @@ - (void)tryUpdatingActiveStyles {
_activeStyles = newActiveStyles;
_blockedStyles = newBlockedStyles;
_recentlyEmittedAlignment = currentAlignment;
_recentlyEmittedListDepths = newListDepths;

emitter->onChangeState(
{.bold = GET_STYLE_STATE([BoldStyle getType]),
Expand All @@ -1157,12 +1183,12 @@ - (void)tryUpdatingActiveStyles {
.h4 = GET_STYLE_STATE([H4Style getType]),
.h5 = GET_STYLE_STATE([H5Style getType]),
.h6 = GET_STYLE_STATE([H6Style getType]),
.unorderedList = GET_STYLE_STATE([UnorderedListStyle getType]),
.orderedList = GET_STYLE_STATE([OrderedListStyle getType]),
.unorderedList = GET_LIST_STYLE_STATE([UnorderedListStyle getType]),
.orderedList = GET_LIST_STYLE_STATE([OrderedListStyle getType]),
.blockQuote = GET_STYLE_STATE([BlockQuoteStyle getType]),
.codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]),
.image = GET_STYLE_STATE([ImageStyle getType]),
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]),
.checkboxList = GET_LIST_STYLE_STATE([CheckboxListStyle getType]),
.alignment = [currentAlignment UTF8String]});
}
}
Expand Down Expand Up @@ -1214,6 +1240,28 @@ - (bool)isStyle:(StyleType)type activeInMap:(NSDictionary *)styleMap {
return false;
}

// returns the list nesting depth at the current selection,
// where 0 means no present nesting
- (NSInteger)listDepthAtSelectionForType:(StyleType)type {
if (![self.stylesDict[@(type)] isList] ||
![self.stylesDict[@(type)] detect:textView.selectedRange]) {
return 0;
}

NSRange selection = textView.selectedRange;
NSParagraphStyle *pStyle;
if (selection.length == 0 && textView.isEditable) {
pStyle = textView.typingAttributes[NSParagraphStyleAttributeName];
} else if (textView.textStorage.length > 0) {
NSUInteger index = MIN(selection.location, textView.textStorage.length - 1);
pStyle = [textView.textStorage attribute:NSParagraphStyleAttributeName
atIndex:index
effectiveRange:NULL];
}

return MAX(0, [TextListsUtils listDepthInArray:pStyle.textLists] - 1);
}

- (bool)textInputShouldReturn {
return [_submitBehavior isEqualToString:@"blurAndSubmit"];
}
Expand Down Expand Up @@ -1280,6 +1328,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
} else if ([commandName isEqualToString:@"toggleCheckboxList"]) {
BOOL checked = [args[0] boolValue];
[self toggleCheckboxList:checked];
} else if ([commandName isEqualToString:@"indentList"]) {
[self indentList];
} else if ([commandName isEqualToString:@"outdentList"]) {
[self outdentList];
} else if ([commandName isEqualToString:@"toggleBlockQuote"]) {
[self toggleRegularStyle:[BlockQuoteStyle getType]];
} else if ([commandName isEqualToString:@"toggleCodeBlock"]) {
Expand Down Expand Up @@ -1510,10 +1562,13 @@ - (void)toggleRegularStyle:(StyleType)type {
if ([style isParagraph]) {
range = [textView.textStorage.string paragraphRangeForRange:range];
}
BOOL conflictPresent = [StyleUtils conflictPresent:type
range:range
forHost:self];
if ([StyleUtils handleStyleBlocksAndConflicts:type
range:range
forHost:self]) {
[style toggle:range];
[style toggle:range withConflictReplaced:conflictPresent];
Comment thread
hejsztynx marked this conversation as resolved.
[self anyTextMayHaveBeenModified];
}
}
Expand All @@ -1526,14 +1581,62 @@ - (void)toggleCheckboxList:(BOOL)checked {
}
NSRange range = [textView.textStorage.string
paragraphRangeForRange:textView.selectedRange];
BOOL conflictPresent = [StyleUtils conflictPresent:[CheckboxListStyle getType]
range:range
forHost:self];
if ([StyleUtils handleStyleBlocksAndConflicts:[CheckboxListStyle getType]
range:range
forHost:self]) {
[style toggleWithChecked:checked range:range];
[style toggleWithChecked:checked
range:range
withConflictReplaced:conflictPresent];
[self anyTextMayHaveBeenModified];
}
}

- (StyleBase *)activeListStyleForRange:(NSRange)range {
UnorderedListStyle *ulStyle = stylesDict[@([UnorderedListStyle getType])];
OrderedListStyle *olStyle = stylesDict[@([OrderedListStyle getType])];
CheckboxListStyle *cbStyle = stylesDict[@([CheckboxListStyle getType])];

if ([ulStyle detect:range]) {
return ulStyle;
}
if ([olStyle detect:range]) {
return olStyle;
}
if ([cbStyle detect:range]) {
return cbStyle;
}
return nullptr;
}

- (void)indentList {
NSRange range = [textView.textStorage.string
paragraphRangeForRange:textView.selectedRange];
StyleBase *activeListStyle = [self activeListStyleForRange:range];
if (activeListStyle == nullptr) {
return;
}

[(ListStyleBase *)activeListStyle modifyListNestingInRange:range
increasing:YES];
[self anyTextMayHaveBeenModified];
}

- (void)outdentList {
NSRange range = [textView.textStorage.string
paragraphRangeForRange:textView.selectedRange];
StyleBase *activeListStyle = [self activeListStyleForRange:range];
if (activeListStyle == nullptr) {
return;
}

[(ListStyleBase *)activeListStyle modifyListNestingInRange:range
increasing:NO];
[self anyTextMayHaveBeenModified];
}

- (void)addLinkAt:(NSInteger)start
end:(NSInteger)end
text:(NSString *)text
Expand Down Expand Up @@ -1755,6 +1858,9 @@ - (void)anyTextMayHaveBeenModified {
}
}
}
// nested lists validation around the dirty ranges; fixes add their own dirty
// ranges
[ListIntegrityUtils validateListsInDirtyRanges:self];
// all the visible (not meta) attributes handling in the ranges that could
// have changed
[attributesManager handleDirtyRangesStyling];
Expand Down Expand Up @@ -1906,12 +2012,13 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText {
.h6 = GET_STYLE_STATE([H6Style getType]),
.codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]),
.blockQuote = GET_STYLE_STATE([BlockQuoteStyle getType]),
.orderedList = GET_STYLE_STATE([OrderedListStyle getType]),
.unorderedList = GET_STYLE_STATE([UnorderedListStyle getType]),
.orderedList = GET_LIST_STYLE_STATE([OrderedListStyle getType]),
.unorderedList =
GET_LIST_STYLE_STATE([UnorderedListStyle getType]),
.link = GET_STYLE_STATE([LinkStyle getType]),
.image = GET_STYLE_STATE([ImageStyle getType]),
.mention = GET_STYLE_STATE([MentionStyle getType]),
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]),
.checkboxList = GET_LIST_STYLE_STATE([CheckboxListStyle getType]),
.alignment = [currentAlignment UTF8String]}});
}
}
Expand Down
Loading
Loading