diff --git a/ReactNative.Tests/ReactNative.Tests.csproj b/ReactNative.Tests/ReactNative.Tests.csproj index f671724536d..6f686279b2e 100644 --- a/ReactNative.Tests/ReactNative.Tests.csproj +++ b/ReactNative.Tests/ReactNative.Tests.csproj @@ -144,7 +144,6 @@ UnitTestApp.xaml - diff --git a/ReactNative.Tests/Views/TextInput/ReactTextBoxPropertiesTests.cs b/ReactNative.Tests/Views/TextInput/ReactTextBoxPropertiesTests.cs deleted file mode 100644 index 12fcce0af3a..00000000000 --- a/ReactNative.Tests/Views/TextInput/ReactTextBoxPropertiesTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; -using ReactNative.Views.TextInput; -using ReactNative.UIManager; -using Windows.UI.Text; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; - -namespace ReactNative.Tests.Views.TextInput -{ - [TestClass] - public class ReactTextBoxPropertiesTests - { - [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] - public void ReactTextBoxPropertiesTests_SuccessfulSrcControlPropMerge() - { - var textBox = new TextBox(); - textBox.Text = ""; - - var reactTextBox = new ReactTextBoxProperties() { - LineHeight = 12, - Padding = new Thickness(12, 23, 1, 23), - FontSize = 12, - FontStyle = FontStyle.Italic - }; - - textBox.SetReactTextBoxProperties(reactTextBox); - Assert.AreEqual(textBox.FontStyle, reactTextBox.FontStyle); - Assert.AreEqual(textBox.Padding, reactTextBox.Padding); - } - - [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] - public void ReactTextBoxPropertiesTests_NullSrcControlPropMerge() - { - var textBox = new TextBox(); - textBox.FontSize = 2; - - var reactTextBox = new ReactTextBoxProperties() - { - LineHeight = 12, - Padding = new Thickness(12, 23, 1, 23), - FontSize = 12, - FontStyle = FontStyle.Italic - }; - - textBox.SetReactTextBoxProperties(reactTextBox); - Assert.AreEqual(textBox.FontStyle, reactTextBox.FontStyle); - Assert.AreEqual(textBox.Padding, reactTextBox.Padding); - } - - [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] - public void ReactTextBoxPropertiesTests_OverwriteControlPropsMerge() - { - var textBox = new TextBox(); - textBox.FontStyle = FontStyle.Normal; - - var reactTextBox = new ReactTextBoxProperties() - { - LineHeight = 12, - Padding = new Thickness(12, 23, 1, 23), - FontSize = 12, - FontStyle = FontStyle.Italic - }; - - textBox.SetReactTextBoxProperties(reactTextBox); - Assert.AreEqual(textBox.FontStyle, reactTextBox.FontStyle); - } - } -} diff --git a/ReactNative/ReactNative.csproj b/ReactNative/ReactNative.csproj index 6eb7052fc77..66e05eba48b 100644 --- a/ReactNative/ReactNative.csproj +++ b/ReactNative/ReactNative.csproj @@ -250,6 +250,7 @@ + @@ -287,8 +288,14 @@ - - + + + + + + + + @@ -320,19 +327,16 @@ - + - - - diff --git a/ReactNative/Shell/MainReactPackage.cs b/ReactNative/Shell/MainReactPackage.cs index 2edd0435c2e..e18a06b97aa 100644 --- a/ReactNative/Shell/MainReactPackage.cs +++ b/ReactNative/Shell/MainReactPackage.cs @@ -78,7 +78,6 @@ public IReadOnlyList CreateViewManagers( new ReactScrollViewManager(), new ReactSwitchManager(), new ReactTextInputManager(), - new ReactMultilineTextInputManager(), new ReactTextViewManager(), //new ReactToolbarManager(), new ReactViewManager(), diff --git a/ReactNative/UIManager/CSSNodeVisitor.cs b/ReactNative/UIManager/CSSNodeVisitor.cs new file mode 100644 index 00000000000..480f071f3dc --- /dev/null +++ b/ReactNative/UIManager/CSSNodeVisitor.cs @@ -0,0 +1,35 @@ +using Facebook.CSSLayout; +using System; +using System.Collections.Generic; + +namespace ReactNative.UIManager +{ + abstract class CSSNodeVisitor + { + public T Visit(CSSNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var n = node.ChildCount; + if (n == 0) + { + return Make(node, Array.Empty()); + } + else + { + var children = new List(n); + foreach (var child in node.Children) + { + children.Add(Visit(child)); + } + + return Make(node, children); + } + } + + protected abstract T Make(CSSNode node, IList children); + } +} diff --git a/ReactNative/UIManager/ILayoutManager.cs b/ReactNative/UIManager/ILayoutManager.cs new file mode 100644 index 00000000000..116914754ba --- /dev/null +++ b/ReactNative/UIManager/ILayoutManager.cs @@ -0,0 +1,19 @@ +using Windows.UI.Xaml; + +namespace ReactNative.UIManager +{ + /// + /// Interface for overriding layout behavior for a view. + /// + public interface ILayoutManager + { + /// + /// Updates the layout of the current instance. + /// + /// The left coordinate. + /// The top coordinate. + /// The layout width. + /// The layout height. + void UpdateLayout(int x, int y, int width, int height); + } +} diff --git a/ReactNative/UIManager/LayoutAnimation/LayoutAnimationController.cs b/ReactNative/UIManager/LayoutAnimation/LayoutAnimationController.cs index 590e6044097..32c191a04d7 100644 --- a/ReactNative/UIManager/LayoutAnimation/LayoutAnimationController.cs +++ b/ReactNative/UIManager/LayoutAnimation/LayoutAnimationController.cs @@ -70,7 +70,7 @@ public void InitializeFromConfig(JObject config) /// public bool ShouldAnimateLayout(FrameworkElement view) { - return _shouldAnimateLayout && view.Parent != null; + return _shouldAnimateLayout && view.Parent != null && !(view is ILayoutManager); } /// diff --git a/ReactNative/UIManager/NativeViewHierarchyManager.cs b/ReactNative/UIManager/NativeViewHierarchyManager.cs index 06ef6285315..b983f175a5a 100644 --- a/ReactNative/UIManager/NativeViewHierarchyManager.cs +++ b/ReactNative/UIManager/NativeViewHierarchyManager.cs @@ -109,7 +109,7 @@ public void UpdateViewExtraData(int tag, object extraData) /// The parent view tag. /// The view tag. /// The left coordinate. - /// The right coordinate. + /// The top coordinate. /// The layout width. /// The layout height. public void UpdateLayout(int parentTag, int tag, int x, int y, int width, int height) @@ -507,10 +507,15 @@ private void DropView(FrameworkElement view) private void UpdateLayout(FrameworkElement viewToUpdate, int x, int y, int width, int height) { + var layoutManager = default(ILayoutManager); if (_layoutAnimator.ShouldAnimateLayout(viewToUpdate)) { _layoutAnimator.ApplyLayoutUpdate(viewToUpdate, x, y, width, height); } + else if ((layoutManager = viewToUpdate as ILayoutManager) != null) + { + layoutManager.UpdateLayout(x, y, width, height); + } else { Canvas.SetLeft(viewToUpdate, x); diff --git a/ReactNative/UIManager/ViewProperties.cs b/ReactNative/UIManager/ViewProperties.cs index 05cb0292e74..be94c05d75e 100644 --- a/ReactNative/UIManager/ViewProperties.cs +++ b/ReactNative/UIManager/ViewProperties.cs @@ -60,6 +60,7 @@ public static class ViewProperties public const string Value = "value"; public const string ResizeMode = "resizeMode"; public const string TextAlign = "textAlign"; + public const string TextAlignVertical = "textAlignVertical"; public const string BorderWidth = "borderWidth"; public const string BorderLeftWidth = "borderLeftWidth"; diff --git a/ReactNative/Views/Text/ReactTextShadowNode.cs b/ReactNative/Views/Text/ReactTextShadowNode.cs index 32f8fad39a9..8700b0648b3 100644 --- a/ReactNative/Views/Text/ReactTextShadowNode.cs +++ b/ReactNative/Views/Text/ReactTextShadowNode.cs @@ -2,6 +2,7 @@ using ReactNative.Bridge; using ReactNative.UIManager; using System; +using System.Collections.Generic; using Windows.Foundation; using Windows.UI.Text; using Windows.UI.Xaml; @@ -16,25 +17,19 @@ namespace ReactNative.Views.Text /// public class ReactTextShadowNode : LayoutShadowNode { - private const string INLINE_IMAGE_PLACEHOLDER = "I"; - private const int UNSET = -1; + private const int Unset = -1; - private const string PROP_TEXT = "text"; - - private int _lineHeight = UNSET; private bool _isColorSet = false; private uint _color; private bool _isBackgroundColorSet = false; private uint _backgroundColor; - private int _numberOfLines = UNSET; - private int _fontSize = UNSET; + private int _fontSize = Unset; private FontStyle? _fontStyle; private FontWeight? _fontWeight; private string _fontFamily; - private string _text; private Inline _inline; @@ -84,6 +79,15 @@ public override bool IsVirtualAnchor } } + /// + /// The text value. + /// + protected string Text + { + get; + private set; + } + /// /// Called once per batch of updates by the /// if the text node is dirty. @@ -95,7 +99,7 @@ public override void OnBeforeLayout() return; } - _inline = DispatcherHelpers.CallOnDispatcher(() => FromTextCSSNode(this)).Result; + _inline = DispatcherHelpers.CallOnDispatcher(() => ReactTextShadowNodeInlineVisitor.Apply(this)).Result; MarkUpdated(); } @@ -125,29 +129,7 @@ public override void OnCollectExtraUpdates(UIViewOperationQueue uiViewOperationQ [ReactProperty("text")] public void SetText(string text) { - _text = text; - MarkUpdated(); - } - - /// - /// Sets the number of lines for the node. - /// - /// The number of lines. - [ReactProperty(ViewProperties.NumberOfLines, DefaultInteger = UNSET)] - public void SetNumberOfLines(int numberOfLines) - { - _numberOfLines = numberOfLines; - MarkUpdated(); - } - - /// - /// Sets the line height for the node. - /// - /// The line height. - [ReactProperty(ViewProperties.LineHeight, DefaultInteger = UNSET)] - public void SetLineHeight(int lineHeight) - { - _lineHeight = lineHeight; + Text = text; MarkUpdated(); } @@ -155,7 +137,7 @@ public void SetLineHeight(int lineHeight) /// Sets the font size for the node. /// /// The font size. - [ReactProperty(ViewProperties.FontSize, DefaultDouble = UNSET)] + [ReactProperty(ViewProperties.FontSize, DefaultDouble = Unset)] public void SetFontSize(double fontSize) { _fontSize = (int)fontSize; @@ -253,6 +235,44 @@ protected override void MarkUpdated() } } + /// + /// Formats an inline instance with shadow properties.. + /// + /// The text shadow node. + /// The inline. + /// Signals if the operation is used only for measurement. + protected static void FormatInline(ReactTextShadowNode textNode, Inline inline, bool measureOnly) + { + if (!measureOnly && textNode._isColorSet) + { + inline.Foreground = new SolidColorBrush(ColorHelpers.Parse(textNode._color)); + } + + if (textNode._fontSize != Unset) + { + var fontSize = textNode._fontSize; + inline.FontSize = fontSize; + } + + if (textNode._fontStyle.HasValue) + { + var fontStyle = textNode._fontStyle.Value; + inline.FontStyle = fontStyle; + } + + if (textNode._fontWeight.HasValue) + { + var fontWeight = textNode._fontWeight.Value; + inline.FontWeight = fontWeight; + } + + if (textNode._fontFamily != null) + { + var fontFamily = new FontFamily(textNode._fontFamily); + inline.FontFamily = fontFamily; + } + } + private static MeasureOutput MeasureText(CSSNode node, float width, float height) { // This is not a terribly efficient way of projecting the height of @@ -264,19 +284,18 @@ private static MeasureOutput MeasureText(CSSNode node, float width, float height // TODO: determine another way to measure text elements. var task = DispatcherHelpers.CallOnDispatcher(() => { - var shadowNode = (ReactTextShadowNode)node; var textBlock = new TextBlock { TextWrapping = TextWrapping.Wrap, }; - textBlock.Inlines.Add(FromTextCSSNode(shadowNode)); + textBlock.Inlines.Add(ReactTextShadowNodeInlineVisitor.Apply(node)); try { - var adjustedWidth = float.IsNaN(width) ? double.PositiveInfinity : width; - var adjustedHeight = float.IsNaN(height) ? double.PositiveInfinity : height; - textBlock.Measure(new Size(adjustedWidth, adjustedHeight)); + var normalizedWidth = CSSConstants.IsUndefined(width) ? double.PositiveInfinity : width; + var normalizedHeight = CSSConstants.IsUndefined(height) ? double.PositiveInfinity : height; + textBlock.Measure(new Size(normalizedWidth, normalizedHeight)); return new MeasureOutput( (float)textBlock.DesiredSize.Width, (float)textBlock.DesiredSize.Height); @@ -289,70 +308,48 @@ private static MeasureOutput MeasureText(CSSNode node, float width, float height return task.Result; } - - private static Inline FromTextCSSNode(ReactTextShadowNode textNode) - { - return BuildInlineFromTextCSSNode(textNode); - } - private static Inline BuildInlineFromTextCSSNode(ReactTextShadowNode textNode) + class ReactTextShadowNodeInlineVisitor : CSSNodeVisitor { - var length = textNode.ChildCount; - var inline = default(Inline); - if (length == 0) + private static ReactTextShadowNodeInlineVisitor s_instance = new ReactTextShadowNodeInlineVisitor(); + + public static Inline Apply(CSSNode node) { - inline = new Run { Text = textNode._text }; - } - else + return s_instance.Visit(node); + } + + protected sealed override Inline Make(CSSNode node, IList children) { - var span = new Span(); - for (var i = 0; i < length; ++i) + var textNode = (ReactTextShadowNode)node; + if (textNode._isVirtual) + { + textNode.MarkUpdateSeen(); + } + + var text = textNode.Text; + if (text != null && children.Count > 0) { - var child = textNode.GetChildAt(i); - var textChild = child as ReactTextShadowNode; - if (textChild == null) + throw new InvalidOperationException("Only leaf nodes can contain text."); + } + else if (text != null) + { + var inline = new Run(); + inline.Text = text; + FormatInline(textNode, inline, false); + return inline; + } + else + { + var inline = new Span(); + foreach (var child in children) { - throw new InvalidOperationException( - $"Unexpected view type '{child.GetType()}' nested under text node."); + inline.Inlines.Add(child); } - var childInline = BuildInlineFromTextCSSNode(textChild); - span.Inlines.Add(childInline); + FormatInline(textNode, inline, false); + return inline; } - - inline = span; } - - if (textNode._isColorSet) - { - inline.Foreground = new SolidColorBrush(ColorHelpers.Parse(textNode._color)); - } - - if (textNode._fontSize != UNSET) - { - var fontSize = textNode._fontSize; - inline.FontSize = fontSize; - } - - if (textNode._fontStyle.HasValue) - { - var fontStyle = textNode._fontStyle.Value; - inline.FontStyle = fontStyle; - } - - if (textNode._fontWeight.HasValue) - { - var fontWeight = textNode._fontWeight.Value; - inline.FontWeight = fontWeight; - } - - if (textNode._fontFamily != null) - { - var fontFamily = new FontFamily(textNode._fontFamily); - inline.FontFamily = fontFamily; - } - - return inline; } } } \ No newline at end of file diff --git a/ReactNative/Views/TextInput/InputScopeHelpers.cs b/ReactNative/Views/TextInput/InputScopeHelpers.cs new file mode 100644 index 00000000000..63606d788cf --- /dev/null +++ b/ReactNative/Views/TextInput/InputScopeHelpers.cs @@ -0,0 +1,32 @@ +using Windows.UI.Xaml.Input; + +namespace ReactNative.Views.TextInput +{ + static class InputScopeHelpers + { + public static InputScopeNameValue FromString(string inputScope) + { + switch (inputScope) + { + case "url": + return InputScopeNameValue.Url; + case "number-pad": + return InputScopeNameValue.NumericPin; + case "phone-pad": + return InputScopeNameValue.TelephoneNumber; + case "name-phone-pad": + return InputScopeNameValue.NameOrPhoneNumber; + case "email-address": + return InputScopeNameValue.EmailNameOrAddress; + case "decimal-pad": + return InputScopeNameValue.Digits; + case "web-search": + return InputScopeNameValue.Search; + case "numeric": + return InputScopeNameValue.Number; + default: + return InputScopeNameValue.Default; + } + } + } +} diff --git a/ReactNative/Views/TextInput/ReactMultilineTextInputManager.cs b/ReactNative/Views/TextInput/ReactMultilineTextInputManager.cs deleted file mode 100644 index 898810192b3..00000000000 --- a/ReactNative/Views/TextInput/ReactMultilineTextInputManager.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ReactNative.UIManager; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; - -namespace ReactNative.Views.TextInput -{ - /// - /// Native component to support a multiline control. - /// - class ReactMultilineTextInputManager : ReactTextInputManager - { - private static readonly string ReactClass = "RCTTextView"; - private const string PROP_MULTILINE = "multiline"; - - /// - /// The name of the view manager. - /// - public override string Name - { - get - { - return ReactClass; - } - } - - /// - /// Determines if there should be multiple lines allowed for the . - /// - /// The text input box control. - /// To allow multiline. - [ReactProperty(PROP_MULTILINE)] - public void SetMultiline(TextBox view, bool multiline) - { - view.AcceptsReturn = multiline; - view.TextWrapping = TextWrapping.Wrap; - } - } -} diff --git a/ReactNative/Views/TextInput/ReactTextBox.cs b/ReactNative/Views/TextInput/ReactTextBox.cs new file mode 100644 index 00000000000..540fbd33e75 --- /dev/null +++ b/ReactNative/Views/TextInput/ReactTextBox.cs @@ -0,0 +1,87 @@ +using ReactNative.UIManager; +using System.Threading; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.Views.TextInput +{ + class ReactTextBox : TextBox, ILayoutManager + { + private int _eventCount; + private double _lastWidth; + private double _lastHeight; + + public ReactTextBox() + { + LayoutUpdated += OnLayoutUpdated; + } + + public int CurrentEventCount + { + get + { + return _eventCount; + } + } + + public bool ClearTextOnFocus + { + get; + set; + } + + public bool SelectTextOnFocus + { + get; + set; + } + + public int IncrementEventCount() + { + return Interlocked.Increment(ref _eventCount); + } + + public void UpdateLayout(int x, int y, int width, int height) + { + Canvas.SetLeft(this, x); + Canvas.SetTop(this, y); + Width = width; + } + + protected override void OnGotFocus(RoutedEventArgs e) + { + if (ClearTextOnFocus) + { + Text = ""; + } + + if (SelectTextOnFocus) + { + SelectionStart = 0; + SelectionLength = Text.Length; + } + } + + private void OnLayoutUpdated(object sender, object e) + { + var width = ActualWidth; + var height = ActualHeight; + if (width != _lastWidth || height != _lastHeight) + { + _lastWidth = width; + _lastHeight = height; + + this.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactTextChangedEvent( + this.GetTag(), + Text, + width, + height, + IncrementEventCount())); + } + } + } +} diff --git a/ReactNative/Views/TextInput/ReactTextBoxProperties.cs b/ReactNative/Views/TextInput/ReactTextBoxProperties.cs deleted file mode 100644 index a63fe64fe9b..00000000000 --- a/ReactNative/Views/TextInput/ReactTextBoxProperties.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Windows.UI; -using Windows.UI.Text; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Media; - -namespace ReactNative.Views.TextInput -{ - /// - /// A Data model which holds measurement related styling updates for - /// . - /// - public class ReactTextBoxProperties - { - private const int UNSET = -1; - - /// - /// The padding thickness. - /// - public Thickness? Padding { get; set; } - - /// - /// The font style. - /// - public FontStyle? FontStyle { get; set; } - - /// - /// The font weight. - /// - public FontWeight? FontWeight { get; set; } - - /// - /// The border color. - /// - public Color? BorderColor { get; set; } - - /// - /// The font family. - /// - public FontFamily FontFamily { get; set; } - - /// - /// The font size. - /// - public int FontSize { get; set; } = UNSET; - - /// - /// The text value. - /// - public string Text { get; set; } - - /// - /// The line height. - /// - public int LineHeight { get; set; } = UNSET; - } -} diff --git a/ReactNative/Views/TextInput/ReactTextChangedEvent.cs b/ReactNative/Views/TextInput/ReactTextChangedEvent.cs index 894548d1c0e..f7f0f5aa455 100644 --- a/ReactNative/Views/TextInput/ReactTextChangedEvent.cs +++ b/ReactNative/Views/TextInput/ReactTextChangedEvent.cs @@ -13,13 +13,23 @@ class ReactTextChangedEvent : Event private readonly string _text; private readonly double _contextWidth; private readonly double _contentHeight; + private readonly int _eventCount; - public ReactTextChangedEvent(int viewId, string text, double contentWidth, double contentHeight) - : base(viewId, TimeSpan.FromTicks(Environment.TickCount)) + /// + /// Instantiates a . + /// + /// The view tag. + /// The text. + /// The content width. + /// The content height. + /// The event count. + public ReactTextChangedEvent(int viewTag, string text, double contentWidth, double contentHeight, int eventCount) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) { _text = text; _contextWidth = contentWidth; _contentHeight = contentHeight; + _eventCount = eventCount; } /// @@ -53,21 +63,21 @@ public override bool CanCoalesce /// The event emitter. public override void Dispatch(RCTEventEmitter rctEventEmitter) { - rctEventEmitter.receiveEvent(this.ViewTag, this.EventName, this.GetEventJavascriptProperties); - } + var contentSize = new JObject + { + { "width", _contextWidth }, + { "height", _contentHeight }, + }; - private JObject GetEventJavascriptProperties - { - get + var eventData = new JObject { - return new JObject() - { - { "width", _contextWidth }, - { "height", _contentHeight }, - { "text", _text }, - { "target", ViewTag } - }; - } + { "text", _text }, + { "contentSize", contentSize }, + { "eventCount", _eventCount }, + { "target", ViewTag }, + }; + + rctEventEmitter.receiveEvent(ViewTag, EventName, eventData); } } } diff --git a/ReactNative/Views/TextInput/ReactTextInputBlurEvent.cs b/ReactNative/Views/TextInput/ReactTextInputBlurEvent.cs index 0bde7d55de6..149ef624d13 100644 --- a/ReactNative/Views/TextInput/ReactTextInputBlurEvent.cs +++ b/ReactNative/Views/TextInput/ReactTextInputBlurEvent.cs @@ -10,13 +10,17 @@ namespace ReactNative.Views.TextInput /// class ReactTextInputBlurEvent : Event { - public ReactTextInputBlurEvent(int viewId) - : base(viewId, TimeSpan.FromTicks(Environment.TickCount)) + /// + /// Instantiate a . + /// + /// The view tag. + public ReactTextInputBlurEvent(int viewTag) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) { } /// - /// Gets the name of the Event + /// The event name. /// public override string EventName { diff --git a/ReactNative/Views/TextInput/ReactTextInputEndEditingEvent.cs b/ReactNative/Views/TextInput/ReactTextInputEndEditingEvent.cs new file mode 100644 index 00000000000..4dc9996c7e8 --- /dev/null +++ b/ReactNative/Views/TextInput/ReactTextInputEndEditingEvent.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json.Linq; +using ReactNative.UIManager.Events; +using System; + +namespace ReactNative.Views.TextInput +{ + class ReactTextInputEndEditingEvent : Event + { + private readonly string _text; + + public ReactTextInputEndEditingEvent(int viewTag, string text) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) + { + _text = text; + } + + public override string EventName + { + get + { + return "topEndEditing"; + } + } + + public override bool CanCoalesce + { + get + { + return false; + } + } + + public override void Dispatch(RCTEventEmitter eventEmitter) + { + var eventData = new JObject + { + { "target", ViewTag }, + { "text", _text }, + }; + + eventEmitter.receiveEvent(ViewTag, EventName, eventData); + } + } +} \ No newline at end of file diff --git a/ReactNative/Views/TextInput/ReactTextInputFocusEvent.cs b/ReactNative/Views/TextInput/ReactTextInputFocusEvent.cs index 7568542c0da..5c1ae5aa854 100644 --- a/ReactNative/Views/TextInput/ReactTextInputFocusEvent.cs +++ b/ReactNative/Views/TextInput/ReactTextInputFocusEvent.cs @@ -10,11 +10,18 @@ namespace ReactNative.Views.TextInput /// class ReactTextInputFocusEvent : Event { - public ReactTextInputFocusEvent(int viewId) - : base(viewId, TimeSpan.FromTicks(Environment.TickCount)) + /// + /// Instantiates a . + /// + /// The view tag. + public ReactTextInputFocusEvent(int viewTag) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) { } + /// + /// The event name. + /// public override string EventName { get diff --git a/ReactNative/Views/TextInput/ReactTextInputManager.cs b/ReactNative/Views/TextInput/ReactTextInputManager.cs index c03a17a9e58..9afb1ecf23a 100644 --- a/ReactNative/Views/TextInput/ReactTextInputManager.cs +++ b/ReactNative/Views/TextInput/ReactTextInputManager.cs @@ -1,36 +1,43 @@ using Newtonsoft.Json.Linq; +using ReactNative.Reflection; using ReactNative.UIManager; -using ReactNative.UIManager.Events; +using ReactNative.Views.Text; using System; using System.Collections.Generic; +using Windows.System; +using Windows.UI; +using Windows.UI.Text; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; namespace ReactNative.Views.TextInput { - class ReactTextInputManager : BaseViewManager + /// + /// View manager for . + /// + class ReactTextInputManager : BaseViewManager { - private static readonly int FOCUS_TEXT_INPUT = 1; - private static readonly int BLUR_TEXT_INPUT = 2; - private static readonly string REACT_CLASS = "RCTTextField"; - - private const string PROP_ROTATION_X = "rotationX"; - private const string PROP_PLACEHOLDER = "placeholder"; - private const string PROP_TEXT_ALIGN = "textAlign"; - private const string PROP_VERTICAL_TEXT_ALIGN = "textAlignVertical"; - private const string PROP_MAX_LENGTH = "maxLength"; - private const string PROP_TEXT = "text"; - private const string PROP_IS_EDITABLE = "editable"; + private static readonly int FocusTextInput = 1; + private static readonly int BlurTextInput = 2; + private bool _onSelectionChange; + + /// + /// The name of the view manager. + /// public override string Name { get { - return REACT_CLASS; + return "RCTTextBox"; } } + /// + /// The exported custom bubbling event types. + /// public override IReadOnlyDictionary ExportedCustomBubblingEventTypeConstants { get @@ -38,15 +45,15 @@ public override IReadOnlyDictionary ExportedCustomBubblingEventT return new Dictionary() { { - "topFocus", + "topSubmitEditing", new Dictionary() { { "phasedRegistrationNames", new Dictionary() { - { "bubbled" , "onFocus" }, - { "captured" , "onFocusCapture" } + { "bubbled" , "onSubmitEditing" }, + { "captured" , "onSubmitEditingCapture" } } } } @@ -65,6 +72,20 @@ public override IReadOnlyDictionary ExportedCustomBubblingEventT } } }, + { + "topFocus", + new Dictionary() + { + { + "phasedRegistrationNames", + new Dictionary() + { + { "bubbled" , "onFocus" }, + { "captured" , "onFocusCapture" } + } + } + } + }, { "topBlur", new Dictionary() @@ -92,198 +113,456 @@ public override IReadOnlyDictionary CommandsMap { return new Dictionary() { - { "focusTextInput", FOCUS_TEXT_INPUT }, - { "blurTextInput", BLUR_TEXT_INPUT } + { "focusTextInput", FocusTextInput }, + { "blurTextInput", BlurTextInput }, }; } } /// - /// Sets the text alignment property on the . + /// Sets the font size on the . /// - /// The text input box control. - /// The text alignment. - [ReactProperty(PROP_TEXT_ALIGN)] - public void SetTextAlign(TextBox view, string alignment) + /// The view instance. + /// The font size. + [ReactProperty(ViewProperties.FontSize)] + public void SetFontSize(ReactTextBox view, double fontSize) + { + view.FontSize = fontSize; + } + + /// + /// Sets the font color for the node. + /// + /// The view instance. + /// The masked color value. + [ReactProperty(ViewProperties.Color, CustomType = "Color")] + public void SetColor(ReactTextBox view, uint? color) { - var textAlignment = default(TextAlignment); - if (Enum.TryParse(alignment, out textAlignment)) + if (color.HasValue) { - view.TextAlignment = textAlignment; + view.Foreground = new SolidColorBrush(ColorHelpers.Parse(color.Value)); + } + else + { + view.Foreground = new SolidColorBrush(Colors.Black); } } /// - /// Sets the text alignment property on the . + /// Sets the font family for the node. /// - /// The text input box control. - /// The text alignment. - /// - /// TODO: test this out. - /// - [ReactProperty(PROP_VERTICAL_TEXT_ALIGN)] - public void SetTextVerticalAlign(TextBox view, string alignment) - { - var textAlignment = default(VerticalAlignment); - if (Enum.TryParse(alignment, out textAlignment)) + /// The view instance. + /// The font family. + [ReactProperty(ViewProperties.FontFamily)] + public void SetFontFamily(ReactTextBox view, string familyName) + { + if (familyName != null) + { + view.FontFamily = new FontFamily(familyName); + } + else { - view.VerticalContentAlignment = textAlignment; + view.FontFamily = new FontFamily("Segoe UI"); } } /// - /// Sets the editablity property on the . + /// Sets the font weight for the node. /// - /// The text input box control. - /// The text alignment. - [ReactProperty(PROP_IS_EDITABLE)] - public void SetEditable(TextBox view, bool editable) + /// The view instance. + /// The font weight string. + [ReactProperty(ViewProperties.FontWeight)] + public void SetFontWeight(ReactTextBox view, string fontWeightString) { - view.IsReadOnly = editable; + var fontWeight = FontStyleHelpers.ParseFontWeight(fontWeightString); + if (fontWeight.HasValue) + { + view.FontWeight = fontWeight.Value; + } + else + { + view.FontWeight = FontWeights.Normal; + } } /// - /// Sets the default text placeholder property on the . + /// Sets the font style for the node. /// - /// The text input box control. - /// placeholder text. - [ReactProperty(PROP_PLACEHOLDER)] - public void SetPlaceholder(TextBox view, string placeholder) + /// The view instance. + /// The font style string. + [ReactProperty(ViewProperties.FontStyle)] + public void SetFontStyle(ReactTextBox view, string fontStyleString) { - view.PlaceholderText = placeholder; + var fontStyle = FontStyleHelpers.ParseFontStyle(fontStyleString); + if (fontStyle.HasValue) + { + view.FontStyle = fontStyle.Value; + } + else + { + view.FontStyle = FontStyle.Normal; + } } /// - /// Sets the foreground color property on the . + /// Sets whether to track selection changes on the . /// - /// The text input box control. - /// The masked color value. - [ReactProperty(ViewProperties.Color, CustomType = "Color")] - public void SetColor(TextBox view, uint? color) + /// The view instance. + /// The indicator. + [ReactProperty("onSelectionChange", DefaultBoolean = false)] + public void SetSelectionChange(ReactTextBox view, bool? onSelectionChange) { - if (color.HasValue) + if (onSelectionChange.HasValue && onSelectionChange.Value) { - view.Foreground = new SolidColorBrush(ColorHelpers.Parse(color.Value)); + _onSelectionChange = true; + view.SelectionChanged += OnSelectionChanged; + } + else + { + _onSelectionChange = false; + view.SelectionChanged -= OnSelectionChanged; } } /// - /// Sets the max charcter length property on the . + /// Sets the default text placeholder property on the . /// - /// The text input box control. - /// The text alignment. - [ReactProperty(PROP_MAX_LENGTH)] - public void SetMaxLength(TextBox view, int maxCharLength) + /// The view instance. + /// The placeholder text. + [ReactProperty("placeholder")] + public void SetPlaceholder(ReactTextBox view, string placeholder) { - view.MaxLength = maxCharLength; + view.PlaceholderText = placeholder; } /// - /// The event interceptor for focus lost events for the native control. + /// Sets the selection color for the . /// - /// The source sender view. - /// The received event args - public void OnInterceptLostFocusEvent(object sender, RoutedEventArgs @event) + /// The view instance. + /// The masked color value. + [ReactProperty("selectionColor", CustomType = "Color")] + public void SetSelectionColor(ReactTextBox view, uint color) { - var senderTextInput = (TextBox)sender; - GetEventDispatcher(senderTextInput).DispatchEvent(new ReactTextInputBlurEvent(senderTextInput.GetTag())); + view.SelectionHighlightColor = new SolidColorBrush(ColorHelpers.Parse(color)); } /// - /// The event interceptor for text change events for the native control. + /// Sets the text alignment property on the . /// - /// The source sender view. - /// The received event args - public void OnInterceptTextChangeEvent(object sender, TextChangedEventArgs e) + /// The view instance. + /// The text alignment. + [ReactProperty(ViewProperties.TextAlign)] + public void SetTextAlign(ReactTextBox view, string alignment) { - var senderTextInput = (TextBox)sender; - GetEventDispatcher(senderTextInput).DispatchEvent(new ReactTextChangedEvent(senderTextInput.GetTag(), senderTextInput.Text, senderTextInput.Width, senderTextInput.Height)); + view.TextAlignment = EnumHelpers.Parse(alignment); } /// - /// Called when view is detached from view hierarchy and allows for - /// additional cleanup by the . - /// subclass. Unregister all event handlers for the . + /// Sets the text alignment property on the . /// - /// The react context. - /// The . - public override void OnDropViewInstance(ThemedReactContext reactContext, TextBox view) + /// The view instance. + /// The text alignment. + [ReactProperty(ViewProperties.TextAlignVertical)] + public void SetTextVerticalAlign(ReactTextBox view, string alignment) { - view.TextChanged -= this.OnInterceptTextChangeEvent; - // TODO: Figure out how to get intercept focus to work this to work. - view.LostFocus -= this.OnInterceptLostFocusEvent; + view.VerticalContentAlignment = EnumHelpers.Parse(alignment); } /// - /// Returns the view instance for . + /// Sets the editablity property on the . /// - /// - /// - protected override TextBox CreateViewInstance(ThemedReactContext reactContext) + /// The view instance. + /// The editable flag. + [ReactProperty("editable")] + public void SetEditable(ReactTextBox view, bool editable) { - return new TextBox(); + view.IsEnabled = editable; } /// - /// Installing the textchanged event emitter on the Control. + /// Sets the max character length property on the . /// - /// The react context. - /// The view instance. - protected override void AddEventEmitters(ThemedReactContext reactContext, TextBox view) + /// The view instance. + /// The max length. + [ReactProperty("maxLength")] + public void SetMaxLength(ReactTextBox view, int maxCharLength) { - view.TextChanged += this.OnInterceptTextChangeEvent; - // TODO: Figure out how to get intercept focus to work this to work. - view.LostFocus += this.OnInterceptLostFocusEvent; + view.MaxLength = maxCharLength; } /// - /// Sets the border width for a . + /// Sets whether to enable autocorrect on the . /// - /// The text box instance. - /// The border width. - [ReactProperty(ViewProperties.BorderWidth)] - public void SetBorderWidth(TextBox root, int width) + /// The view instance. + /// The autocorrect flag. + [ReactProperty("autoCorrect")] + public void SetAutoCorrect(ReactTextBox view, bool autoCorrect) { - root.BorderThickness = new Thickness(width); + view.IsSpellCheckEnabled = autoCorrect; } - public override void UpdateExtraData(TextBox root, object extraData) + /// + /// Sets whether to enable multiline input on the . + /// + /// The view instance. + /// The multiline flag. + [ReactProperty("multiline", DefaultBoolean = false)] + public void SetMultiline(ReactTextBox view, bool multiline) { - var reactTextBoxStyle = (ReactTextBoxProperties)extraData; + view.AcceptsReturn = multiline; + } - if (reactTextBoxStyle == null) + /// + /// Sets the keyboard type on the . + /// + /// The view instance. + /// The keyboard type. + [ReactProperty("keyboardType")] + public void SetKeyboardType(ReactTextBox view, string keyboardType) + { + view.InputScope = null; + if (keyboardType != null) { - throw new InvalidOperationException("ReactTextBoxProperties is undefined exception. We were unable to measure the dimensions of the TextBox control."); + var inputScope = new InputScope(); + inputScope.Names.Add( + new InputScopeName( + InputScopeHelpers.FromString(keyboardType))); + + view.InputScope = inputScope; } + } - root.SetReactTextBoxProperties(reactTextBoxStyle); + /// + /// Sets the border width for a . + /// + /// The view instance. + /// The border width. + [ReactProperty(ViewProperties.BorderWidth)] + public void SetBorderWidth(ReactTextBox view, int width) + { + view.BorderThickness = new Thickness(width); + } + + /// + /// Sets whether the text should be cleared on focus. + /// + /// The view instance. + /// The indicator. + [ReactProperty("clearTextOnFocus")] + public void SetClearTextOnFocus(ReactTextBox view, bool clearTextOnFocus) + { + view.ClearTextOnFocus = clearTextOnFocus; } + /// + /// Sets whether the text should be selected on focus. + /// + /// The view instance. + /// The indicator. + [ReactProperty("selectTextOnFocus")] + public void SetSelectTextOnFocus(ReactTextBox view, bool selectTextOnFocus) + { + view.SelectTextOnFocus = selectTextOnFocus; + } + + /// + /// Create the shadow node instance. + /// + /// The shadow node instance. public override ReactTextInputShadowNode CreateShadowNodeInstance() { - return new ReactTextInputShadowNode(false); + return new ReactTextInputShadowNode(); } /// /// Implement this method to receive events/commands directly from - /// JavaScript through the . + /// JavaScript through the . /// /// /// The view instance that should receive the command. /// /// Identifer for the command. /// Optional arguments for the command. - public override void ReceiveCommand(TextBox view, int commandId, JArray args) + public override void ReceiveCommand(ReactTextBox view, int commandId, JArray args) { - if (commandId == FOCUS_TEXT_INPUT) + if (commandId == FocusTextInput) { + // Sometimes, the focus command is received before the view + // is actually rendered on screen. In this case, we defer + // the focus command until after the view is ready. + // TODO: (#271) resolve issues with focus. view.Focus(FocusState.Programmatic); } + else if (commandId == BlurTextInput) + { + var frame = Window.Current?.Content as Frame; + frame?.Focus(FocusState.Programmatic); + } + } + + /// + /// Update the view with extra data. + /// + /// The view instance. + /// The extra data. + public override void UpdateExtraData(ReactTextBox view, object extraData) + { + var paddings = extraData as float[]; + var textUpdate = default(Tuple); + if (paddings != null) + { + view.Padding = new Thickness( + paddings[0], + paddings[1], + paddings[2], + paddings[3]); + } + else if ((textUpdate = extraData as Tuple) != null) + { + if (textUpdate.Item1 < view.CurrentEventCount) + { + return; + } + + var text = textUpdate.Item2; + + view.TextChanged -= OnTextChanged; + if (_onSelectionChange) + { + view.SelectionChanged -= OnSelectionChanged; + } + + var selectionStart = view.SelectionStart; + var selectionLength = view.SelectionLength; + var textLength = text?.Length ?? 0; + var maxLength = textLength - selectionLength; + + view.Text = text ?? ""; + view.SelectionStart = Math.Min(selectionStart, textLength); + view.SelectionLength = Math.Min(selectionLength, maxLength < 0 ? 0 : maxLength); + + view.TextChanged += OnTextChanged; + if (_onSelectionChange) + { + view.SelectionChanged += OnSelectionChanged; + } + } + } + + /// + /// Called when view is detached from view hierarchy and allows for + /// additional cleanup by the . + /// subclass. Unregister all event handlers for the . + /// + /// The react context. + /// The . + public override void OnDropViewInstance(ThemedReactContext reactContext, ReactTextBox view) + { + view.KeyDown -= OnKeyDown; + view.LostFocus -= OnLostFocus; + view.GotFocus -= OnGotFocus; + view.TextChanged -= OnTextChanged; + } + + /// + /// Returns the view instance for . + /// + /// + /// + protected override ReactTextBox CreateViewInstance(ThemedReactContext reactContext) + { + return new ReactTextBox + { + AcceptsReturn = false, + }; + } + + /// + /// Installing the textchanged event emitter on the Control. + /// + /// The react context. + /// The view instance. + protected override void AddEventEmitters(ThemedReactContext reactContext, ReactTextBox view) + { + view.TextChanged += OnTextChanged; + view.GotFocus += OnGotFocus; + view.LostFocus += OnLostFocus; + view.KeyDown += OnKeyDown; + } + + private void OnTextChanged(object sender, TextChangedEventArgs e) + { + var textBox = (ReactTextBox)sender; + textBox.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactTextChangedEvent( + textBox.GetTag(), + textBox.Text, + textBox.ActualWidth, + textBox.ActualHeight, + textBox.IncrementEventCount())); + } + + private void OnGotFocus(object sender, RoutedEventArgs e) + { + var textBox = (ReactTextBox)sender; + textBox.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactTextInputFocusEvent(textBox.GetTag())); + } + + private void OnLostFocus(object sender, RoutedEventArgs e) + { + var textBox = (ReactTextBox)sender; + var eventDispatcher = textBox.GetReactContext() + .GetNativeModule() + .EventDispatcher; + + eventDispatcher.DispatchEvent( + new ReactTextInputBlurEvent(textBox.GetTag())); + + eventDispatcher.DispatchEvent( + new ReactTextInputEndEditingEvent( + textBox.GetTag(), + textBox.Text)); + } + + private void OnKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter) + { + var textBox = (ReactTextBox)sender; + if (!textBox.AcceptsReturn) + { + e.Handled = true; + textBox.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactTextInputSubmitEditingEvent( + textBox.GetTag(), + textBox.Text)); + } + } } - - private EventDispatcher GetEventDispatcher(TextBox textBox) + + private void OnSelectionChanged(object sender, RoutedEventArgs e) { - return textBox?.GetReactContext().GetNativeModule().EventDispatcher; + var textBox = (ReactTextBox)sender; + var start = textBox.SelectionStart; + var length = textBox.SelectionLength; + textBox.GetReactContext() + .GetNativeModule() + .EventDispatcher + .DispatchEvent( + new ReactTextInputSelectionEvent( + textBox.GetTag(), + start, + start + length)); } } } diff --git a/ReactNative/Views/TextInput/ReactTextInputSelectionEvent.cs b/ReactNative/Views/TextInput/ReactTextInputSelectionEvent.cs new file mode 100644 index 00000000000..4c6869f46b8 --- /dev/null +++ b/ReactNative/Views/TextInput/ReactTextInputSelectionEvent.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json.Linq; +using ReactNative.UIManager.Events; +using System; + +namespace ReactNative.Views.TextInput +{ + class ReactTextInputSelectionEvent : Event + { + private readonly int _end; + private readonly int _start; + + public ReactTextInputSelectionEvent(int viewTag, int start, int end) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) + { + _start = start; + _end = end; + } + + public override string EventName + { + get + { + return "topSelectionChange"; + } + } + + public override void Dispatch(RCTEventEmitter eventEmitter) + { + var selectionData = new JObject + { + { "start", _start }, + { "end", _end }, + }; + + var eventData = new JObject + { + { "selection", selectionData }, + }; + + eventEmitter.receiveEvent(ViewTag, EventName, eventData); + } + } +} \ No newline at end of file diff --git a/ReactNative/Views/TextInput/ReactTextInputShadowNode.cs b/ReactNative/Views/TextInput/ReactTextInputShadowNode.cs index cdfb10dd648..0f3245cbe5e 100644 --- a/ReactNative/Views/TextInput/ReactTextInputShadowNode.cs +++ b/ReactNative/Views/TextInput/ReactTextInputShadowNode.cs @@ -2,11 +2,11 @@ using ReactNative.Bridge; using ReactNative.UIManager; using ReactNative.Views.Text; +using System; using Windows.Foundation; -using Windows.UI.Text; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Documents; namespace ReactNative.Views.TextInput { @@ -14,188 +14,153 @@ namespace ReactNative.Views.TextInput /// This extension of is responsible for /// measuring the layout for Native . /// - public class ReactTextInputShadowNode : LayoutShadowNode + public class ReactTextInputShadowNode : ReactTextShadowNode { - private const int UNSET = -1; + private const int Unset = -1; - private readonly bool _isVirtual; + private float[] _computedPadding; + + private int _numberOfLines = Unset; + private int _jsEventCount = Unset; - private ReactTextBoxProperties _textBoxStyle; - /// /// Instantiates the . /// - /// - /// Indicates whether the shadow node is virtual or not. - /// - public ReactTextInputShadowNode(bool isVirtual) + public ReactTextInputShadowNode() + : base(false) { - _textBoxStyle = new ReactTextBoxProperties(); - _isVirtual = isVirtual; - - if (!isVirtual) - { - MeasureFunction = MeasureText; - } + var computedPadding = GetDefaultPaddings(); + SetPadding(CSSSpacingType.Left, computedPadding[0]); + SetPadding(CSSSpacingType.Top, computedPadding[1]); + SetPadding(CSSSpacingType.Right, computedPadding[2]); + SetPadding(CSSSpacingType.Bottom, computedPadding[3]); + MeasureFunction = MeasureText; } /// - /// Nodes that return true will be treated as "virtual" - /// nodes. That is, nodes that are not mapped into native views (e.g., - /// nested text node). + /// Set the most recent event count in JavaScript. /// - public override bool IsVirtual + /// The event count. + [ReactProperty("mostRecentEventCount")] + public void SetMostRecentEventCount(int mostRecentEventCount) { - get - { - return _isVirtual; - } + _jsEventCount = mostRecentEventCount; } /// - /// Queues up the view operations onto the . + /// Set the number of lines for the text input. /// - /// - public override void OnCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) + /// The event count. + [ReactProperty("numberOfLines")] + public void SetNumberOfLines(int numberOfLines) { - if (_isVirtual) - { - return; - } - - base.OnCollectExtraUpdates(uiViewOperationQueue); - if (_textBoxStyle != null) - { - uiViewOperationQueue.EnqueueUpdateExtraData(ReactTag, _textBoxStyle); - } + _numberOfLines = numberOfLines; } /// - /// This lifecycle method is called by to bind the CSS styling to the . + /// Called once per batch of updates by the + /// if the text node is dirty. /// public override void OnBeforeLayout() { - DispatcherHelpers.AssertOnDispatcher(); - - if (_isVirtual) - { - return; - } - - MarkUpdated(); - } - - /// - /// Sets the font size for the . - /// - /// The font size. - [ReactProperty(ViewProperties.FontSize, DefaultDouble = UNSET)] - public void SetFontSize(double fontSize) - { - _textBoxStyle.FontSize = (int)fontSize; - MarkUpdated(); + return; } /// - /// Sets the text for the . + /// Called to aggregate the current text and event counter. /// - /// Font family string. - [ReactProperty(ViewProperties.FontFamily)] - public void SetFontFamily(string fontFamily) + /// The UI operation queue. + public override void OnCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { - _textBoxStyle.FontFamily = new FontFamily(fontFamily); - MarkUpdated(); - } + base.OnCollectExtraUpdates(uiViewOperationQueue); - /// - /// Sets the text for the . - /// - /// Font weight string. - [ReactProperty(ViewProperties.FontWeight)] - public void SetFontWeight(string fontWeightString) - { - var fontWeight = FontStyleHelpers.ParseFontWeight(fontWeightString); - if (_textBoxStyle.FontWeight.HasValue != fontWeight.HasValue || - (_textBoxStyle.FontWeight.HasValue && fontWeight.HasValue && - _textBoxStyle.FontWeight.Value.Weight != fontWeight.Value.Weight)) + if (_computedPadding != null) { - _textBoxStyle.FontWeight = fontWeight; - MarkUpdated(); + uiViewOperationQueue.EnqueueUpdateExtraData(ReactTag, _computedPadding); + _computedPadding = null; } - } - /// - /// Sets the text for the . - /// - /// Font style string. - [ReactProperty(ViewProperties.FontStyle)] - public void SetFontStyle(string fontStyleString) - { - var fontStyle = FontStyleHelpers.ParseFontStyle(fontStyleString); - if (_textBoxStyle.FontStyle != fontStyle) + if (_jsEventCount != Unset) { - _textBoxStyle.FontStyle = fontStyle; - MarkUpdated(); + uiViewOperationQueue.EnqueueUpdateExtraData(ReactTag, Tuple.Create(_jsEventCount, Text)); } } - /// - /// Sets the text value for the . - /// - /// The text. - [ReactProperty("text")] - public void SetText(string text) - { - _textBoxStyle.Text = text; - MarkUpdated(); - } - - /// - /// Sets the the border color for a . - /// - /// The masked color value. - [ReactProperty("borderColor")] - public void SetBorderColor(uint? color) + private MeasureOutput MeasureText(CSSNode node, float width, float height) { - if (color.HasValue) + _computedPadding = GetComputedPadding(); + + var normalizedWidth = CSSConstants.IsUndefined(width) ? double.PositiveInfinity : width; + var normalizedHeight = CSSConstants.IsUndefined(height) ? double.PositiveInfinity : height; + + var borderLeftWidth = GetBorder(CSSSpacingType.Left); + var borderRightWidth = GetBorder(CSSSpacingType.Right); + + normalizedWidth -= _computedPadding[0]; + normalizedWidth -= _computedPadding[2]; + normalizedWidth -= CSSConstants.IsUndefined(borderLeftWidth) ? 0 : borderLeftWidth; + normalizedWidth -= CSSConstants.IsUndefined(borderRightWidth) ? 0 : borderRightWidth; + + // This is not a terribly efficient way of projecting the height of + // the text elements. It requires that we have access to the + // dispatcher in order to do measurement, which, for obvious + // reasons, can cause perceived performance issues as it will block + // the UI thread from handling other work. + // + // TODO: determine another way to measure text elements. + var task = DispatcherHelpers.CallOnDispatcher(() => { - _textBoxStyle.BorderColor = ColorHelpers.Parse(color.Value); - MarkUpdated(); - } - } + var textNode = (ReactTextInputShadowNode)node; - /// - /// Marks the node as updated/dirty. This occurs on any property - /// changes affecting the measurement of the . - /// - protected override void MarkUpdated() - { - base.MarkUpdated(); + var textBlock = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + }; - if (!_isVirtual) - { - dirty(); - } + var normalizedText = string.IsNullOrEmpty(textNode.Text) ? " " : textNode.Text; + var inline = new Run { Text = normalizedText }; + FormatInline(textNode, inline, true); + + textBlock.Inlines.Add(inline); + + textBlock.Measure(new Size(normalizedWidth, normalizedHeight)); + + var borderTopWidth = GetBorder(CSSSpacingType.Top); + var borderBottomWidth = GetBorder(CSSSpacingType.Bottom); + + var finalizedHeight = (float)textBlock.DesiredSize.Height; + finalizedHeight += _computedPadding[1]; + finalizedHeight += _computedPadding[3]; + finalizedHeight += CSSConstants.IsUndefined(borderTopWidth) ? 0 : borderTopWidth; + finalizedHeight += CSSConstants.IsUndefined(borderBottomWidth) ? 0 : borderBottomWidth; + + return new MeasureOutput(width, finalizedHeight); + }); + + return task.Result; } - private MeasureOutput MeasureText(CSSNode node, float width, float height) + private float[] GetDefaultPaddings() { - var shadowNode = (ReactTextInputShadowNode)node; - var textBox = new TextBox(); - - textBox.SetReactTextBoxProperties(shadowNode._textBoxStyle); - - if (!float.IsNaN(width)) + // TODO: calculate dynamically + return new[] { - textBox.MaxWidth = width; - } + 10f, + 3f, + 6f, + 5f, + }; + } - if (!float.IsNaN(height)) + private float[] GetComputedPadding() + { + return new float[] { - textBox.MaxHeight = height; - } - - return new MeasureOutput((float)textBox.DesiredSize.Width, (float)textBox.DesiredSize.Height); + GetPadding(CSSSpacingType.Left), + GetPadding(CSSSpacingType.Top), + GetPadding(CSSSpacingType.Right), + GetPadding(CSSSpacingType.Bottom), + }; } } } diff --git a/ReactNative/Views/TextInput/ReactTextInputSubmitEditingEvent.cs b/ReactNative/Views/TextInput/ReactTextInputSubmitEditingEvent.cs new file mode 100644 index 00000000000..2c633c18259 --- /dev/null +++ b/ReactNative/Views/TextInput/ReactTextInputSubmitEditingEvent.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json.Linq; +using ReactNative.UIManager.Events; +using System; + +namespace ReactNative.Views.TextInput +{ + class ReactTextInputSubmitEditingEvent : Event + { + private readonly string _text; + + public ReactTextInputSubmitEditingEvent(int viewTag, string text) + : base(viewTag, TimeSpan.FromTicks(Environment.TickCount)) + { + _text = text; + } + + public override string EventName + { + get + { + return "topSubmitEditing"; + } + } + + public override bool CanCoalesce + { + get + { + return false; + } + } + + public override void Dispatch(RCTEventEmitter eventEmitter) + { + var eventData = new JObject + { + { "target", ViewTag }, + { "text", _text }, + }; + + eventEmitter.receiveEvent(ViewTag, EventName, eventData); + } + } +} \ No newline at end of file diff --git a/ReactNative/Views/TextInput/TextBoxExtensions.cs b/ReactNative/Views/TextInput/TextBoxExtensions.cs deleted file mode 100644 index 906f3c482a5..00000000000 --- a/ReactNative/Views/TextInput/TextBoxExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ReactNative.Views.TextInput; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media; - -namespace ReactNative.Views.TextInput -{ - static class TextBoxExtensions - { - private const int UNSET = -1; - - /// - /// Assigns any set property from the instance. - /// - /// The current context. - /// The property instance. - public static void SetReactTextBoxProperties(this TextBox textBox, ReactTextBoxProperties reactProperties) - { - textBox.Text = reactProperties.Text != null ? reactProperties.Text : ""; - - if (reactProperties.FontWeight.HasValue) - { - textBox.FontWeight = reactProperties.FontWeight.Value; - } - - if (reactProperties.FontStyle.HasValue) - { - textBox.FontStyle = reactProperties.FontStyle.Value; - } - - if (reactProperties.FontSize != UNSET) - { - textBox.FontSize = reactProperties.FontSize; - } - - if (reactProperties.FontFamily != null) - { - textBox.FontFamily = reactProperties.FontFamily; - } - - if (reactProperties.BorderColor.HasValue) - { - textBox.BorderBrush = new SolidColorBrush(reactProperties.BorderColor.Value); - } - - if (reactProperties.Padding.HasValue) - { - textBox.Padding = reactProperties.Padding.Value; - } - } - } -}