Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ccdce63
feat(ios): add textAlignment
kacperzolkiewski Jan 29, 2026
9e157ed
feat: add parsing from html into input
kacperzolkiewski Jan 29, 2026
178c83d
feat: separate alignment logic to AlignmentUtils
kacperzolkiewski Jan 29, 2026
6823e42
fix: android build
kacperzolkiewski Jan 30, 2026
6640463
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski Feb 2, 2026
6d2bf85
fix: layout for ZWS
kacperzolkiewski Mar 10, 2026
7dc4737
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski Mar 11, 2026
51b54d5
fix: add alignment to Toolbar
kacperzolkiewski Mar 11, 2026
a0e9f64
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski Mar 25, 2026
d978723
fix: review changes
kacperzolkiewski Mar 26, 2026
962b042
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski Apr 30, 2026
161ff8c
fix: resolve conflicts in parser
kacperzolkiewski Apr 30, 2026
e62df66
fix: alignment after mataatributes remake
kacperzolkiewski May 6, 2026
e21de1f
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski May 6, 2026
869ce6d
fix: refactor alignment to style
kacperzolkiewski May 7, 2026
76734f3
fix: removing checkboxList
kacperzolkiewski May 7, 2026
e7d5b8e
fix: preserve aignment during typing attributes reset
kacperzolkiewski May 7, 2026
bea7294
fix: alignment detection
kacperzolkiewski May 8, 2026
521fdc2
fix: remove unused code
kacperzolkiewski May 8, 2026
b09f68b
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski May 8, 2026
aef3a57
fix: cursor alignment for empty lines
kacperzolkiewski May 11, 2026
98d7873
fix: placeholder alignment
kacperzolkiewski May 11, 2026
8f81868
fix: linting
kacperzolkiewski May 11, 2026
3c8b79e
test: add e2e tests for alignment
kacperzolkiewski May 11, 2026
9e6b747
Merge branch 'main' into @kacperzolkiewski/text-alignment
kacperzolkiewski May 12, 2026
180e643
Update ios/utils/TextListUtils.h
kacperzolkiewski May 14, 2026
8d5ee49
Update .maestro/enrichedInput/flows/paragraph_styles_alignment_visual…
kacperzolkiewski May 14, 2026
f94e12b
Update ios/enrichedInputTextView/EnrichedInputTextView/EnrichedInputT…
kacperzolkiewski May 14, 2026
611eded
Update ios/utils/TextListUtils.h
kacperzolkiewski May 14, 2026
a3a4d9c
Update ios/utils/ParagraphAttributesUtils.mm
kacperzolkiewski May 14, 2026
ee57d0c
Update ios/utils/TextListUtils.h
kacperzolkiewski May 14, 2026
555181a
fix: review changes
kacperzolkiewski May 14, 2026
9a965d1
fix: parser
kacperzolkiewski May 14, 2026
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 @@ -422,6 +422,13 @@ class EnrichedTextInputViewManager :
view?.requestHTML(requestId)
}

override fun setTextAlignment(
view: EnrichedTextInputView?,
alignment: String,
) {
TODO("Not yet implemented")
}

override fun measure(
context: Context,
localData: ReadableMap?,
Expand Down
27 changes: 27 additions & 0 deletions apps/example/src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ const STYLE_ITEMS = [
name: 'checkbox-list',
icon: 'check-square-o',
},
{
name: 'align-left',
icon: 'align-left',
},
{
name: 'align-center',
icon: 'align-center',
},
{
name: 'align-right',
icon: 'align-right',
},
] as const;

type Item = (typeof STYLE_ITEMS)[number];
Expand Down Expand Up @@ -168,6 +180,15 @@ export const Toolbar: FC<ToolbarProps> = ({
case 'mention':
editorRef.current?.startMention('@');
break;
case 'align-left':
editorRef.current?.setTextAlignment('left');
break;
case 'align-center':
editorRef.current?.setTextAlignment('center');
break;
case 'align-right':
editorRef.current?.setTextAlignment('right');
break;
}
};

Expand Down Expand Up @@ -256,6 +277,12 @@ export const Toolbar: FC<ToolbarProps> = ({
return stylesState.mention.isActive;
case 'checkbox-list':
return stylesState.checkboxList.isActive;
case 'align-left':
return stylesState.alignment === 'left';
case 'align-center':
return stylesState.alignment === 'center';
case 'align-right':
return stylesState.alignment === 'right';
default:
return false;
}
Expand Down
1 change: 1 addition & 0 deletions apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const DEFAULT_STYLES: StylesState = {
image: DEFAULT_STYLE_STATE,
mention: DEFAULT_STYLE_STATE,
checkboxList: DEFAULT_STYLE_STATE,
alignment: 'left',
};

export const DEFAULT_LINK_STATE = {
Expand Down
10 changes: 10 additions & 0 deletions apps/example/src/screens/DevScreen.tsx
Comment thread
kacperzolkiewski marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,14 @@ const styles = StyleSheet.create({
height: 1000,
backgroundColor: 'rgb(0, 26, 114)',
},
alignmentLabel: {
marginTop: 20,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
color: 'rgb(0, 26, 114)',
},
alignmentButton: {
width: '25%',
},
});
12 changes: 12 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,14 @@ interface OnChangeStateEvent {
isConflicting: boolean;
isBlocking: boolean;
};
alignment: string;
}
```

- `isActive` indicates if the style is active within current selection.
- `isBlocking` indicates if the style is blocked by other currently active, meaning it can't be toggled.
- `isConflicting` indicates if the style is in conflict with other currently active styles, meaning toggling it will remove conflicting style.
- `alignment` indicates the current text alignment of the paragraph at the cursor position. Possible values: `'left'`, `'center'`, `'right'`, `'justify'`.

| Type | Platform |
|-------------------------------------------------------------|----------|
Expand Down Expand Up @@ -614,6 +616,16 @@ Sets the selection at the given indexes.
- `start: number` - starting index of the selection.
- `end: number` - first index after the selection's ending index. For just a cursor in place (no selection), `start` equals `end`.

### `.setTextAlignment()`

```ts
setTextAlignment: (alignment: 'left' | 'center' | 'right' | 'justify' | 'default') => void;
```

Sets text alignment for the paragraph(s) at the current selection. When inside a list, the alignment is applied to all contiguous list items.

- `alignment` - the desired text alignment. Use `'default'` to reset to the system natural alignment.

### `.startMention()`

```ts
Expand Down
18 changes: 17 additions & 1 deletion ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#import "EnrichedTextInputView.h"
#import "AlignmentUtils.h"
#import "CoreText/CoreText.h"
#import "ImageAttachment.h"
#import "LayoutManagerExtension.h"
#import "ParagraphAttributesUtils.h"
#import "ParagraphsUtils.h"
#import "RCTFabricComponentsPlugins.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
Expand Down Expand Up @@ -51,6 +53,7 @@ @implementation EnrichedTextInputView {
BOOL _emitTextChange;
NSMutableDictionary<NSValue *, UIImageView *> *_attachmentViews;
NSArray<NSDictionary *> *_contextMenuItems;
NSString *_recentlyEmittedAlignment;
}

// MARK: - Component utils
Expand Down Expand Up @@ -89,6 +92,7 @@ - (void)setDefaults {
_blockedStyles = [[NSMutableSet alloc] init];
_recentlyActiveLinkRange = NSMakeRange(0, 0);
_recentlyActiveMentionRange = NSMakeRange(0, 0);
_recentlyEmittedAlignment = @"left";
recentlyChangedRange = NSMakeRange(0, 0);
_recentInputString = @"";
_recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
Expand Down Expand Up @@ -1121,12 +1125,20 @@ - (void)tryUpdatingActiveStyles {
}
}

// detect alignment change
NSString *currentAlignment =
[AlignmentUtils currentAlignmentStringForInput:self];
if (![currentAlignment isEqualToString:_recentlyEmittedAlignment]) {
updateNeeded = YES;
}

if (updateNeeded) {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
// update activeStyles and blockedStyles only if emitter is available
_activeStyles = newActiveStyles;
_blockedStyles = newBlockedStyles;
_recentlyEmittedAlignment = currentAlignment;

emitter->onChangeState(
{.bold = GET_STYLE_STATE([BoldStyle getStyleType]),
Expand All @@ -1147,7 +1159,8 @@ - (void)tryUpdatingActiveStyles {
.blockQuote = GET_STYLE_STATE([BlockQuoteStyle getStyleType]),
.codeBlock = GET_STYLE_STATE([CodeBlockStyle getStyleType]),
.image = GET_STYLE_STATE([ImageStyle getStyleType]),
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getStyleType])});
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getStyleType]),
.alignment = [currentAlignment UTF8String]});
}
}

Expand Down Expand Up @@ -1282,6 +1295,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
} else if ([commandName isEqualToString:@"requestHTML"]) {
NSInteger requestId = [((NSNumber *)args[0]) integerValue];
[self requestHTML:requestId];
} else if ([commandName isEqualToString:@"setTextAlignment"]) {
NSString *alignmentString = (NSString *)args[0];
[AlignmentUtils applyAlignmentFromString:alignmentString toInput:self];
}
}

Expand Down
25 changes: 16 additions & 9 deletions ios/extensions/LayoutManagerExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
NSForegroundColorAttributeName :
[typedInput->config orderedListMarkerColor]
};
CGFloat indent = pStyle.firstLineHeadIndent;

NSArray *paragraphs = [ParagraphsUtils
getSeparateParagraphsRangesIn:typedInput->textView
Expand Down Expand Up @@ -308,18 +309,21 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
marker:marker
markerAttributes:markerAttributes
origin:origin
usedRect:textUsedRect];
usedRect:textUsedRect
indent:indent];
} else if (markerFormat ==
NSTextListMarkerDisc) {
[self drawBullet:typedInput
origin:origin
usedRect:textUsedRect];
usedRect:textUsedRect
indent:indent];
} else if ([markerFormat
hasPrefix:@"{checkbox"]) {
[self drawCheckbox:typedInput
markerFormat:markerFormat
origin:origin
usedRect:textUsedRect];
usedRect:textUsedRect
indent:indent];
}
// only first line of a list gets its
// marker drawn
Expand Down Expand Up @@ -387,7 +391,8 @@ - (CGRect)getTextAlignedUsedRect:(CGRect)usedRect font:(UIFont *)font {
- (void)drawCheckbox:(EnrichedTextInputView *)typedInput
markerFormat:(NSString *)markerFormat
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
usedRect:(CGRect)usedRect
indent:(CGFloat)indent {
BOOL isChecked = [markerFormat isEqualToString:@"{checkbox:1}"];

UIImage *image = isChecked ? typedInput->config.checkboxCheckedImage
Expand All @@ -396,18 +401,19 @@ - (void)drawCheckbox:(EnrichedTextInputView *)typedInput
CGFloat boxSize = [typedInput->config checkboxListBoxSize];

CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGFloat boxX = origin.x + usedRect.origin.x - gapWidth - boxSize;
CGFloat boxX = origin.x + indent - gapWidth - boxSize;
CGFloat boxY = centerY - boxSize / 2.0;

[image drawAtPoint:CGPointMake(boxX, boxY)];
}

- (void)drawBullet:(EnrichedTextInputView *)typedInput
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
usedRect:(CGRect)usedRect
indent:(CGFloat)indent {
CGFloat gapWidth = [typedInput->config unorderedListGapWidth];
CGFloat bulletSize = [typedInput->config unorderedListBulletSize];
CGFloat bulletX = origin.x + usedRect.origin.x - gapWidth - bulletSize / 2;
CGFloat bulletX = origin.x + indent - gapWidth - bulletSize / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;

CGContextRef context = UIGraphicsGetCurrentContext();
Expand All @@ -425,10 +431,11 @@ - (void)drawDecimal:(EnrichedTextInputView *)typedInput
marker:(NSString *)marker
markerAttributes:(NSDictionary *)markerAttributes
origin:(CGPoint)origin
usedRect:(CGRect)usedRect {
usedRect:(CGRect)usedRect
indent:(CGFloat)indent {
CGFloat gapWidth = [typedInput->config orderedListGapWidth];
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
CGFloat markerX = usedRect.origin.x - gapWidth - markerSize.width / 2;
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width / 2;
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
CGFloat markerY = centerY - markerSize.height / 2.0;

Expand Down
Loading
Loading