From 112b24a93523cea44cde21eefc0d0bc03973fb41 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 14:11:27 -0800 Subject: [PATCH 01/24] Bump Xamarin.Forms to 2.5.0.122203 They added a required platform API Fixes #55 --- Ooui.Forms/Forms.cs | 4 ++++ Ooui.Forms/Ooui.Forms.csproj | 2 +- PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj | 2 +- Samples/Samples.csproj | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index bcae580..a2b1f37 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -139,6 +139,10 @@ namespace Xamarin.Forms }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); } } + + public void QuitApplication() + { + } } public class ViewInitializedEventArgs diff --git a/Ooui.Forms/Ooui.Forms.csproj b/Ooui.Forms/Ooui.Forms.csproj index cacc1d4..440e8f2 100644 --- a/Ooui.Forms/Ooui.Forms.csproj +++ b/Ooui.Forms/Ooui.Forms.csproj @@ -20,7 +20,7 @@ true - + diff --git a/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj b/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj index 4e05f3e..6fed386 100644 --- a/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj +++ b/PlatformSamples/AspNetCoreMvc/AspNetCoreMvc.csproj @@ -9,7 +9,7 @@ - + diff --git a/Samples/Samples.csproj b/Samples/Samples.csproj index 3ba2b0d..98ffe10 100644 --- a/Samples/Samples.csproj +++ b/Samples/Samples.csproj @@ -10,7 +10,7 @@ - + From 03f9ed2d91e589ffb34e59f4277c7c87fc6003c0 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 14:17:29 -0800 Subject: [PATCH 02/24] Update comparison chart --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 306c7ad..467f7f8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ msbuild dotnet run --project Samples/Samples.csproj --no-build ``` -There is currently an issue with Xamarin.Forms and building from the dotnet cli, so for now we use the msbuild command and then set the --no-build flag on dotnet run but this will eventually change when the issue is resolved. +*(There is currently an issue with Xamarin.Forms and building from the dotnet cli, so for now we use the msbuild command and then set the --no-build flag on dotnet run but this will eventually change when the issue is resolved.)* This will open the default starting page for the Samples. Now point your browser at [http://localhost:8080/shared-button](http://localhost:8080/shared-button) @@ -111,8 +111,8 @@ When the user clicks or otherwise interacts with the UI, those events are sent b How big is it? -50 KB -650 KB +80 KB +850 KB 1,300 KB @@ -160,7 +160,7 @@ When the user clicks or otherwise interacts with the UI, those events are sent b How do I style things? -CSS baby! (soon) +CSS baby! XAML resources CSS From 0ff69fd3b713fba09881dc0a0317f3281990a5c7 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 14:23:42 -0800 Subject: [PATCH 03/24] Support SSL web sockets This should make Ooui work with HTTPS Fixes #51 --- Ooui/Client.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Ooui/Client.js b/Ooui/Client.js index f77d6e1..094aa1a 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -43,7 +43,11 @@ function ooui (rootElementPath) { var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; - socket = new WebSocket ("ws://" + document.location.host + rootElementPath + wsArgs, "ooui"); + var proto = "ws"; + if (location.protocol == "https:") { + proto = "wss"; + } + socket = new WebSocket (proto + "://" + document.location.host + rootElementPath + wsArgs, "ooui"); socket.addEventListener ("open", function (event) { console.log ("Web socket opened"); From f05f232d5bb362a00be8078ff7ae424430867a66 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 15:09:37 -0800 Subject: [PATCH 04/24] Fix returning to pages without re-requesting them Chrome likes to cache HTML and was making a new web socket request without first requesting a new page. The server however expires a web socket url after its first use. To fix this, web socket urls have a rolling cache policy and can be reconnected to. Fixes #52 --- Ooui.AspNetCore/WebSocketHandler.cs | 31 ++++++++++--------- Ooui/Client.js | 7 +---- .../AspNetCoreMvc/Views/Home/Index.cshtml | 1 + 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs index a483e01..c93d3c4 100644 --- a/Ooui.AspNetCore/WebSocketHandler.cs +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -13,19 +13,19 @@ namespace Ooui.AspNetCore public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (5); - static readonly ConcurrentDictionary pendingSessions = - new ConcurrentDictionary (); + static readonly ConcurrentDictionary activeSessions = + new ConcurrentDictionary (); public static string BeginSession (HttpContext context, Element element) { var id = Guid.NewGuid ().ToString ("N"); - var s = new PendingSession { + var s = new ActiveSession { Element = element, - RequestTimeUtc = DateTime.UtcNow, + LastConnectTimeUtc = DateTime.UtcNow, }; - if (!pendingSessions.TryAdd (id, s)) { + if (!activeSessions.TryAdd (id, s)) { throw new Exception ("Failed to schedule pending session"); } @@ -60,20 +60,21 @@ namespace Ooui.AspNetCore } // - // Find the pending session + // Clear old sessions // - if (!pendingSessions.TryRemove (id, out var pendingSession)) { - BadRequest ("Unknown `id`"); - return; + var toClear = activeSessions.Where (x => (DateTime.UtcNow - x.Value.LastConnectTimeUtc) > SessionTimeout).ToList (); + foreach (var c in toClear) { + activeSessions.TryRemove (c.Key, out var _); } // - // Reject the session if it's old + // Find the pending session // - if ((DateTime.UtcNow - pendingSession.RequestTimeUtc) > SessionTimeout) { - BadRequest ("Old `id`"); + if (!activeSessions.TryGetValue (id, out var activeSession)) { + BadRequest ("Unknown `id`"); return; } + activeSession.LastConnectTimeUtc = DateTime.UtcNow; // // Set the element's dimensions @@ -97,14 +98,14 @@ namespace Ooui.AspNetCore // var token = CancellationToken.None; var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui"); - var session = new Ooui.UI.Session (webSocket, pendingSession.Element, w, h, token); + var session = new Ooui.UI.Session (webSocket, activeSession.Element, w, h, token); await session.RunAsync ().ConfigureAwait (false); } - class PendingSession + class ActiveSession { public Element Element; - public DateTime RequestTimeUtc; + public DateTime LastConnectTimeUtc; } } } diff --git a/Ooui/Client.js b/Ooui/Client.js index 094aa1a..e053d5c 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -37,7 +37,6 @@ function getSize () { // Main entrypoint function ooui (rootElementPath) { - var opened = false; var initialSize = getSize (); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + @@ -47,11 +46,11 @@ function ooui (rootElementPath) { if (location.protocol == "https:") { proto = "wss"; } + socket = new WebSocket (proto + "://" + document.location.host + rootElementPath + wsArgs, "ooui"); socket.addEventListener ("open", function (event) { console.log ("Web socket opened"); - opened = true; }); socket.addEventListener ("error", function (event) { @@ -60,10 +59,6 @@ function ooui (rootElementPath) { socket.addEventListener ("close", function (event) { console.error ("Web socket close", event); - if (opened) { - alert ("Connection to the server has been lost. Please try refreshing the page."); - opened = false; - } }); socket.addEventListener("message", function (event) { diff --git a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml index 6fb9f7f..a2cc879 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml @@ -9,6 +9,7 @@

Ooui

Write interactive web apps in C# and F#

+

Source Code

From 7b9bf654963cca8f565a5594ebdaa14a09231265 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 18:43:23 -0800 Subject: [PATCH 05/24] Use attributes instead of properties --- Ooui.Forms/Renderers/WebViewRenderer.cs | 4 +- Ooui/Anchor.cs | 5 +- Ooui/Button.cs | 4 +- Ooui/Canvas.cs | 10 +- Ooui/Element.cs | 131 ++++++++++++++++++++++-- Ooui/EventTarget.cs | 13 ++- Ooui/Form.cs | 14 ++- Ooui/FormControl.cs | 9 +- Ooui/Iframe.cs | 12 +-- Ooui/Image.cs | 8 +- Ooui/Input.cs | 42 ++++---- Ooui/Label.cs | 5 +- Ooui/Message.cs | 2 + Ooui/Option.cs | 15 ++- Ooui/Select.cs | 9 +- Ooui/TextArea.cs | 9 +- 16 files changed, 197 insertions(+), 95 deletions(-) diff --git a/Ooui.Forms/Renderers/WebViewRenderer.cs b/Ooui.Forms/Renderers/WebViewRenderer.cs index 381e905..496f45a 100644 --- a/Ooui.Forms/Renderers/WebViewRenderer.cs +++ b/Ooui.Forms/Renderers/WebViewRenderer.cs @@ -24,7 +24,7 @@ namespace Ooui.Forms.Renderers if (_iframe != null) { - _iframe.Src = html; + _iframe.Source = html; } } catch (Exception ex) @@ -47,7 +47,7 @@ namespace Ooui.Forms.Renderers if (_iframe != null) { - _iframe.Src = url; + _iframe.Source = url; } } catch (Exception ex) diff --git a/Ooui/Anchor.cs b/Ooui/Anchor.cs index 864011b..6ea2853 100644 --- a/Ooui/Anchor.cs +++ b/Ooui/Anchor.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Anchor : Element { - string href = ""; public string HRef { - get => href; - set => SetProperty (ref href, value ?? "", "href"); + get => GetStringAttribute ("href", ""); + set => SetAttributeProperty ("href", value); } public Anchor () diff --git a/Ooui/Button.cs b/Ooui/Button.cs index 386fcd9..4102dcd 100644 --- a/Ooui/Button.cs +++ b/Ooui/Button.cs @@ -9,8 +9,8 @@ namespace Ooui { ButtonType typ = ButtonType.Submit; public ButtonType Type { - get => typ; - set => SetProperty (ref typ, value, "type"); + get => GetAttribute ("type", ButtonType.Submit); + set => SetAttributeProperty ("type", value); } public Button () diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs index b3dc144..2c009ac 100644 --- a/Ooui/Canvas.cs +++ b/Ooui/Canvas.cs @@ -7,16 +7,14 @@ namespace Ooui CanvasRenderingContext2D context2d = new CanvasRenderingContext2D (); int gotContext2d = 0; - int width = 300; public int Width { - get => width; - set => SetProperty (ref width, value <= 0 ? 150 : value, "width"); + get => GetAttribute ("width", 300); + set => SetAttributeProperty ("width", value < 0 ? 0 : value); } - int height = 150; public int Height { - get => height; - set => SetProperty (ref height, value <= 0 ? 150 : value, "height"); + get => GetAttribute ("height", 150); + set => SetAttributeProperty ("width", value < 0 ? 0 : value); } public Canvas () diff --git a/Ooui/Element.cs b/Ooui/Element.cs index bc08de7..05626be 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -1,28 +1,29 @@ using System; +using System.Collections.Generic; using System.ComponentModel; namespace Ooui { public abstract class Element : Node { - string className = ""; + readonly Dictionary attributes = new Dictionary (); + public string ClassName { - get => className; - set => SetProperty (ref className, value, "className"); + get => GetStringAttribute ("class", ""); + set => SetAttributeProperty ("class", value); } public Style Style { get; private set; } = new Style (); - string title = ""; public string Title { - get => title; - set => SetProperty (ref title, value, "title"); + get => GetStringAttribute ("title", ""); + set => SetAttributeProperty ("title", value); } bool hidden = false; public bool IsHidden { - get => hidden; - set => SetProperty (ref hidden, value, "hidden"); + get => GetBooleanAttribute ("hidden"); + set => SetBooleanAttributeProperty ("hidden", value); } public event TargetEventHandler Click { @@ -102,8 +103,64 @@ namespace Ooui Style.PropertyChanged += HandleStylePropertyChanged; } - public void SetAttribute (string attributeName, string value) + protected bool SetAttributeProperty (string attributeName, object newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { + var old = GetAttribute (attributeName); + if (old != null && old.Equals (newValue)) + return false; + SetAttribute (attributeName, newValue); + OnPropertyChanged (propertyName); + return true; + } + + protected bool SetBooleanAttributeProperty (string attributeName, bool newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + { + var old = GetAttribute (attributeName) != null; + if (old != newValue) + return false; + if (newValue) + SetAttribute (attributeName, string.Empty); + else + RemoveAttribute (attributeName); + OnPropertyChanged (propertyName); + return true; + } + + protected bool UpdateAttributeProperty (string attributeName, object newValue, string propertyName) + { + lock (attributes) { + if (attributes.TryGetValue (attributeName, out var oldValue)) { + if (newValue != null && newValue.Equals (oldValue)) + return false; + } + attributes[attributeName] = newValue; + } + OnPropertyChanged (propertyName); + return true; + } + + protected bool UpdateBooleanAttributeProperty (string attributeName, bool newValue, string propertyName) + { + lock (attributes) { + var oldValue = attributes.ContainsKey (attributeName); + if (newValue == oldValue) + return false; + if (newValue) { + attributes[attributeName] = ""; + } + else { + attributes.Remove (attributeName); + } + } + OnPropertyChanged (propertyName); + return true; + } + + public void SetAttribute (string attributeName, object value) + { + lock (attributes) { + attributes[attributeName] = value; + } Send (new Message { MessageType = MessageType.SetAttribute, TargetId = Id, @@ -112,6 +169,62 @@ namespace Ooui }); } + public object GetAttribute (string attributeName) + { + lock (attributes) { + attributes.TryGetValue (attributeName, out var v); + return v; + } + } + + public T GetAttribute (string attributeName, T defaultValue) + { + lock (attributes) { + attributes.TryGetValue (attributeName, out var v); + if (v is T) { + return (T)v; + } + else { + return defaultValue; + } + } + } + + public bool GetBooleanAttribute (string attributeName) + { + lock (attributes) { + return attributes.TryGetValue (attributeName, out var _); + } + } + + public string GetStringAttribute (string attributeName, string defaultValue) + { + lock (attributes) { + if (attributes.TryGetValue (attributeName, out var v)) { + if (v == null) return "null"; + else return v.ToString (); + } + else { + return defaultValue; + } + } + } + + public void RemoveAttribute (string attributeName) + { + bool removed; + lock (attributes) { + removed = attributes.Remove (attributeName); + } + if (removed) { + Send (new Message { + MessageType = MessageType.RemoveAttribute, + TargetId = Id, + Key = attributeName, + }); + } + } + void HandleStylePropertyChanged (object sender, PropertyChangedEventArgs e) { SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index e3e26c1..4e8c273 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -83,12 +83,12 @@ namespace Ooui } } - protected bool SetProperty (ref T backingStore, T newValue, string attributeName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + protected bool SetProperty (ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { if (EqualityComparer.Default.Equals (backingStore, newValue)) return false; backingStore = newValue; - SendSet (attributeName, newValue); + SendSet (jsPropertyName, newValue); OnPropertyChanged (propertyName); return true; } @@ -120,12 +120,12 @@ namespace Ooui Send (Message.Call (Id, methodName, args)); } - protected void SendSet (string attributeName, object value) + protected void SendSet (string jsPropertyName, object value) { Send (new Message { MessageType = MessageType.Set, TargetId = Id, - Key = attributeName, + Key = jsPropertyName, Value = value, }); } @@ -169,6 +169,11 @@ namespace Ooui state.Add (message); }); break; + case MessageType.RemoveAttribute: + this.UpdateStateMessages (state => { + state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); + }); + return true; case MessageType.Listen: AddStateMessage (message); break; diff --git a/Ooui/Form.cs b/Ooui/Form.cs index 809397a..897a319 100644 --- a/Ooui/Form.cs +++ b/Ooui/Form.cs @@ -6,20 +6,18 @@ namespace Ooui { string action = ""; public string Action { - get => action; - set => SetProperty (ref action, value ?? "", "action"); + get => GetStringAttribute ("action", ""); + set => SetAttributeProperty ("action", value ?? ""); } - string method = "GET"; public string Method { - get => method; - set => SetProperty (ref method, value ?? "", "method"); + get => GetStringAttribute ("method", "GET"); + set => SetAttributeProperty ("method", value ?? ""); } - string enctype = "application/x-www-form-urlencoded"; public string EncodingType { - get => enctype; - set => SetProperty (ref enctype, value ?? "", "enctype"); + get => GetStringAttribute ("enctype", "application/x-www-form-urlencoded"); + set => SetAttributeProperty ("enctype", value ?? ""); } public event TargetEventHandler Submit { diff --git a/Ooui/FormControl.cs b/Ooui/FormControl.cs index e98607e..461b301 100644 --- a/Ooui/FormControl.cs +++ b/Ooui/FormControl.cs @@ -4,16 +4,15 @@ namespace Ooui { public abstract class FormControl : Element { - string name = ""; public string Name { - get => name; - set => SetProperty (ref name, value, "name"); + get => GetStringAttribute ("name", ""); + set => SetAttributeProperty ("name", value); } bool isDisabled = false; public bool IsDisabled { - get => isDisabled; - set => SetProperty (ref isDisabled, value, "disabled"); + get => GetBooleanAttribute ("disabled"); + set => SetBooleanAttributeProperty ("disabled", value); } public FormControl (string tagName) diff --git a/Ooui/Iframe.cs b/Ooui/Iframe.cs index 587109e..26a917c 100644 --- a/Ooui/Iframe.cs +++ b/Ooui/Iframe.cs @@ -2,17 +2,15 @@ { public class Iframe : Element { - public Iframe() - : base("iframe") + public string Source { - + get => GetStringAttribute ("src", null); + set => SetAttributeProperty ("src", value); } - string src = null; - public string Src + public Iframe () + : base ("iframe") { - get => src; - set => SetProperty(ref src, value, "src"); } } } diff --git a/Ooui/Image.cs b/Ooui/Image.cs index 6bba7ff..24bff04 100644 --- a/Ooui/Image.cs +++ b/Ooui/Image.cs @@ -4,10 +4,10 @@ namespace Ooui { public class Image : Element { - string src = ""; - public string Source { - get => src; - set => SetProperty (ref src, value ?? "", "src"); + public string Source + { + get => GetStringAttribute ("src", null); + set => SetAttributeProperty ("src", value); } public Image () diff --git a/Ooui/Input.cs b/Ooui/Input.cs index 90f73d8..9574d11 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -7,16 +7,14 @@ namespace Ooui { public class Input : FormControl { - InputType typ = InputType.Text; public InputType Type { - get => typ; - set => SetProperty (ref typ, value, "type"); + get => GetAttribute ("type", InputType.Text); + set => SetAttributeProperty ("type", value); } - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } public double NumberValue { @@ -35,37 +33,33 @@ namespace Ooui remove => RemoveEventListener ("change", value); } - string placeholder = ""; public string Placeholder { - get => placeholder; - set => SetProperty (ref placeholder, value, "placeholder"); + get => GetStringAttribute ("placeholder", ""); + set => SetAttributeProperty ("placeholder", value ?? ""); } - bool isChecked = false; public bool IsChecked { - get => isChecked; + get => GetBooleanAttribute ("checked"); set { - SetProperty (ref isChecked, value, "checked"); - TriggerEventFromMessage (Message.Event (Id, "change", isChecked)); + if (SetBooleanAttributeProperty ("checked", value)) { + TriggerEventFromMessage (Message.Event (Id, "change", IsChecked)); + } } } - double minimum = 0; public double Minimum { - get => minimum; - set => SetProperty (ref minimum, value, "min"); + get => GetAttribute ("min", 0.0); + set => SetAttributeProperty ("min", value); } - double maximum = 100; public double Maximum { - get => maximum; - set => SetProperty (ref maximum, value, "max"); + get => GetAttribute ("max", 100.0); + set => SetAttributeProperty ("max", value); } - double step = 1; public double Step { - get => step; - set => SetProperty (ref step, value, "step"); + get => GetAttribute ("step", 1.0); + set => SetAttributeProperty ("step", value); } public Input () @@ -86,10 +80,10 @@ namespace Ooui if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { // Don't need to notify here because the base implementation will fire the event if (Type == InputType.Checkbox) { - isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false; + UpdateBooleanAttributeProperty ("checked", message.Value != null ? Convert.ToBoolean (message.Value) : false, "IsChecked"); } else { - val = message.Value != null ? Convert.ToString (message.Value) : ""; + UpdateAttributeProperty ("value", message.Value != null ? Convert.ToString (message.Value) : "", "Value"); } } return base.TriggerEventFromMessage (message); diff --git a/Ooui/Label.cs b/Ooui/Label.cs index 9dcfd7f..ca27a15 100644 --- a/Ooui/Label.cs +++ b/Ooui/Label.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Label : Element { - Element htmlFor = null; public Element For { - get => htmlFor; - set => SetProperty (ref htmlFor, value, "htmlFor"); + get => GetAttribute ("for", null); + set => SetAttributeProperty ("for", value); } public Label () diff --git a/Ooui/Message.cs b/Ooui/Message.cs index 0498437..1e78fc9 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -48,6 +48,8 @@ namespace Ooui Set, [EnumMember (Value = "setAttr")] SetAttribute, + [EnumMember(Value = "remAttr")] + RemoveAttribute, [EnumMember(Value = "call")] Call, [EnumMember(Value = "listen")] diff --git a/Ooui/Option.cs b/Ooui/Option.cs index 86d918e..22984eb 100644 --- a/Ooui/Option.cs +++ b/Ooui/Option.cs @@ -4,22 +4,19 @@ namespace Ooui { public class Option : Element { - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } - string label = ""; public string Label { - get => label; - set => SetProperty (ref label, value ?? "", "label"); + get => GetStringAttribute ("label", ""); + set => SetAttributeProperty ("label", value ?? ""); } - bool defaultSelected = false; public bool DefaultSelected { - get => defaultSelected; - set => SetProperty (ref defaultSelected, value, "defaultSelected"); + get => GetBooleanAttribute ("selected"); + set => SetBooleanAttributeProperty ("selected", value); } public Option () diff --git a/Ooui/Select.cs b/Ooui/Select.cs index c88eed4..885de9b 100644 --- a/Ooui/Select.cs +++ b/Ooui/Select.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Select : FormControl { - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } public event TargetEventHandler Change { @@ -35,6 +34,7 @@ namespace Ooui protected override void OnChildInsertedBefore (Node newChild, Node referenceChild) { base.OnChildInsertedBefore (newChild, referenceChild); + var val = Value; if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) { val = o.Value; } @@ -43,7 +43,8 @@ namespace Ooui protected override bool TriggerEventFromMessage (Message message) { if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { - val = message.Value != null ? Convert.ToString (message.Value) : ""; + SetAttribute ("value", message.Value != null ? Convert.ToString (message.Value) : ""); + OnPropertyChanged ("Value"); } return base.TriggerEventFromMessage (message); } diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index a35b962..b7cc341 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -20,16 +20,15 @@ namespace Ooui set => SetProperty (ref val, value ?? "", "value"); } - int rows = 2; public int Rows { - get => rows; - set => SetProperty (ref rows, value, "rows"); + get => GetAttribute ("rows", 2); + set => SetAttributeProperty ("rows", value); } int cols = 20; public int Columns { - get => cols; - set => SetProperty (ref cols, value, "cols"); + get => GetAttribute ("cols", 20); + set => SetAttributeProperty ("cols", value); } public TextArea () From 0c03a0bf49658b9881f391073e27ce5f2a225940 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 18:51:00 -0800 Subject: [PATCH 06/24] Fix Canvas size bugs --- Ooui/Canvas.cs | 2 +- Tests/CanvasTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs index 2c009ac..66ff2cb 100644 --- a/Ooui/Canvas.cs +++ b/Ooui/Canvas.cs @@ -14,7 +14,7 @@ namespace Ooui public int Height { get => GetAttribute ("height", 150); - set => SetAttributeProperty ("width", value < 0 ? 0 : value); + set => SetAttributeProperty ("height", value < 0 ? 0 : value); } public Canvas () diff --git a/Tests/CanvasTests.cs b/Tests/CanvasTests.cs index ac8def7..dc93a19 100644 --- a/Tests/CanvasTests.cs +++ b/Tests/CanvasTests.cs @@ -58,8 +58,8 @@ namespace Tests Assert.AreEqual (480, c.Height); c.Width = 0; c.Height = -100; - Assert.AreEqual (150, c.Width); - Assert.AreEqual (150, c.Height); + Assert.AreEqual (0, c.Width); + Assert.AreEqual (0, c.Height); } } } From 4148ea17beadc54f94123e051e57b6a98713569b Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 20:18:16 -0800 Subject: [PATCH 07/24] Generate initial static html --- Ooui.AspNetCore/ElementResult.cs | 24 +++---- Ooui.Forms/Renderers/ViewRenderer.cs | 2 + Ooui.Forms/VisualElementRenderer.cs | 2 + Ooui/Client.js | 2 + Ooui/Div.cs | 2 + Ooui/Element.cs | 33 ++++++++++ Ooui/Node.cs | 19 ++++++ Ooui/Style.cs | 2 +- Ooui/TextArea.cs | 7 ++ Ooui/TextNode.cs | 5 ++ Ooui/UI.cs | 9 ++- Tests/WriteHtmlTests.cs | 95 ++++++++++++++++++++++++++++ 12 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 Tests/WriteHtmlTests.cs diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index 23234a6..ae8cc0f 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -1,28 +1,30 @@ using System; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + namespace Ooui.AspNetCore { public class ElementResult : ActionResult - { + { readonly Element element; - readonly string title; - + readonly string title; + public ElementResult (Element element, string title = "") - { + { this.element = element; - this.title = title; + this.title = title; } - public override async Task ExecuteResultAsync (ActionContext context) + public override async Task ExecuteResultAsync (ActionContext context) { var response = context.HttpContext.Response; response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); - var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title); + var initialHtml = element.OuterHtml; + Console.WriteLine(initialHtml); + var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml); var htmlBytes = Encoding.UTF8.GetBytes (html); response.ContentLength = htmlBytes.Length; using (var s = response.Body) { diff --git a/Ooui.Forms/Renderers/ViewRenderer.cs b/Ooui.Forms/Renderers/ViewRenderer.cs index b425349..0344223 100644 --- a/Ooui.Forms/Renderers/ViewRenderer.cs +++ b/Ooui.Forms/Renderers/ViewRenderer.cs @@ -21,6 +21,8 @@ namespace Ooui.Forms.Renderers /// protected virtual bool ManageNativeControlLifetime => true; + protected override bool HtmlNeedsFullEndElement => TagName == "div"; + public ViewRenderer (string tagName = "div") : base (tagName) { diff --git a/Ooui.Forms/VisualElementRenderer.cs b/Ooui.Forms/VisualElementRenderer.cs index 628d863..b436710 100644 --- a/Ooui.Forms/VisualElementRenderer.cs +++ b/Ooui.Forms/VisualElementRenderer.cs @@ -62,6 +62,8 @@ namespace Ooui.Forms } } + protected override bool HtmlNeedsFullEndElement => TagName == "div"; + public VisualElementRenderer (string tagName = "div") : base (tagName) { _propertyChangedHandler = OnElementPropertyChanged; diff --git a/Ooui/Client.js b/Ooui/Client.js index e053d5c..d19f249 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -38,6 +38,8 @@ function getSize () { // Main entrypoint function ooui (rootElementPath) { + return; + var initialSize = getSize (); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; diff --git a/Ooui/Div.cs b/Ooui/Div.cs index d4d8009..f9098c4 100644 --- a/Ooui/Div.cs +++ b/Ooui/Div.cs @@ -5,6 +5,8 @@ namespace Ooui { public class Div : Element { + protected override bool HtmlNeedsFullEndElement => true; + public Div () : base ("div") { diff --git a/Ooui/Element.cs b/Ooui/Element.cs index 05626be..2e32512 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -243,5 +243,38 @@ namespace Ooui return base.SaveStateMessageIfNeeded (message); } } + + protected virtual bool HtmlNeedsFullEndElement => false; + + public override void WriteOuterHtml (System.Xml.XmlWriter w) + { + w.WriteStartElement (TagName); + w.WriteAttributeString ("id", Id); + var style = Style.ToString (); + if (style.Length > 0) { + w.WriteAttributeString ("style", style); + } + lock (attributes) { + foreach (var a in attributes) { + var value = (a.Value == null) ? "null" : Convert.ToString (a.Value, System.Globalization.CultureInfo.InvariantCulture); + w.WriteAttributeString (a.Key, value); + } + } + WriteInnerHtml (w); + if (HtmlNeedsFullEndElement) { + w.WriteFullEndElement (); + } + else { + w.WriteEndElement (); + } + } + + public virtual void WriteInnerHtml (System.Xml.XmlWriter w) + { + var children = Children; + foreach (var c in children) { + c.WriteOuterHtml (w); + } + } } } diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 18d16a0..374c016 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -180,5 +180,24 @@ namespace Ooui } return false; } + + public virtual string OuterHtml { + get { + using (var stream = new System.IO.MemoryStream ()) { + var settings = new System.Xml.XmlWriterSettings { + OmitXmlDeclaration = true, + ConformanceLevel = System.Xml.ConformanceLevel.Fragment, + CloseOutput = false, + }; + using (var w = System.Xml.XmlWriter.Create (stream, settings)) { + WriteOuterHtml (w); + } + stream.Position = 0; + return new System.IO.StreamReader (stream).ReadToEnd (); + } + } + } + + public abstract void WriteOuterHtml (System.Xml.XmlWriter w); } } diff --git a/Ooui/Style.cs b/Ooui/Style.cs index 6a6f307..71f3177 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -399,7 +399,7 @@ namespace Ooui o.Append (head); o.Append (p.Key); o.Append (":"); - o.Append (String.Format (System.Globalization.CultureInfo.InvariantCulture, "{0}", p.Value)); + o.Append (Convert.ToString (p.Value, System.Globalization.CultureInfo.InvariantCulture)); head = ";"; } } diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index b7cc341..f3f2d5f 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -31,6 +31,8 @@ namespace Ooui set => SetAttributeProperty ("cols", value); } + protected override bool HtmlNeedsFullEndElement => true; + public TextArea () : base ("textarea") { @@ -52,5 +54,10 @@ namespace Ooui } return base.TriggerEventFromMessage (message); } + + public override void WriteInnerHtml (System.Xml.XmlWriter w) + { + w.WriteString (val ?? ""); + } } } diff --git a/Ooui/TextNode.cs b/Ooui/TextNode.cs index 47441c6..8fccf8c 100644 --- a/Ooui/TextNode.cs +++ b/Ooui/TextNode.cs @@ -20,5 +20,10 @@ namespace Ooui { Text = text; } + + public override void WriteOuterHtml (System.Xml.XmlWriter w) + { + w.WriteString (text); + } } } diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 33c39ac..c865a3c 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -44,7 +44,10 @@ namespace Ooui -
+ +
+@InitialHtml +
@@ -390,9 +393,9 @@ namespace Ooui } } - public static string RenderTemplate (string webSocketPath, string title = "") + public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "") { - return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title); + return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml); } class DataHandler : RequestHandler diff --git a/Tests/WriteHtmlTests.cs b/Tests/WriteHtmlTests.cs new file mode 100644 index 0000000..5d95fb8 --- /dev/null +++ b/Tests/WriteHtmlTests.cs @@ -0,0 +1,95 @@ +using System; + +#if NUNIT +using NUnit.Framework; +using TestClassAttribute = NUnit.Framework.TestFixtureAttribute; +using TestMethodAttribute = NUnit.Framework.TestCaseAttribute; +#else +using Microsoft.VisualStudio.TestTools.UnitTesting; +#endif + +using Ooui; + +namespace Tests +{ + [TestClass] + public class WriteHtmlTests + { + System.Text.RegularExpressions.Regex idre = new System.Text.RegularExpressions.Regex ("\\sid=\"[^\"]*\""); + + string OuterHtmlWithoutIds (Element e) + { + return idre.Replace (e.OuterHtml, ""); + } + + [TestMethod] + public void TextAreaWithTextStyled () + { + var e = new TextArea { + Value = "Hello World!", + }; + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TextAreaEmptyStyled () + { + var e = new TextArea (); + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void Style () + { + var e = new Div (); + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TwoGrandChildren () + { + var e = new Div (new Div (new Anchor (), new Anchor ()), new Paragraph ()); + Assert.AreEqual ("

", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void Child () + { + var e = new Div (new Anchor ()); + Assert.AreEqual ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TextChild () + { + var e = new Paragraph ("Hello world!"); + Assert.AreEqual ("

Hello world!

", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void IdIsFirst () + { + var e = new Anchor (); + Assert.IsTrue (e.OuterHtml.StartsWith ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void AnchorHRef () + { + var e = new Anchor { + HRef = "http://google.com" + }; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + } +} From 28c8fac046bc74dec58766ae200486663b453285 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 21:02:59 -0800 Subject: [PATCH 08/24] Store window height in cookies so initial html is the right size --- Ooui.AspNetCore/ElementResult.cs | 23 ++++++++++++++++++++++- Ooui/Client.js | 19 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index ae8cc0f..e918d73 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -1,6 +1,7 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Ooui.AspNetCore @@ -21,9 +22,14 @@ namespace Ooui.AspNetCore var response = context.HttpContext.Response; response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; + + if (element.WantsFullScreen) { + element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000); + element.Style.Height = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowHeight", 24, 480, 10000); + } + var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); var initialHtml = element.OuterHtml; - Console.WriteLine(initialHtml); var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml); var htmlBytes = Encoding.UTF8.GetBytes (html); response.ContentLength = htmlBytes.Length; @@ -31,5 +37,20 @@ namespace Ooui.AspNetCore await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false); } } + + static double GetCookieDouble (IRequestCookieCollection cookies, string key, double min, double def, double max) + { + if (cookies.TryGetValue (key, out var s)) { + if (double.TryParse (s, out var d)) { + if (d < min) return min; + if (d > max) return max; + return d; + } + return def; + } + else { + return def; + } + } } } diff --git a/Ooui/Client.js b/Ooui/Client.js index d19f249..0abe37b 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -35,12 +35,29 @@ function getSize () { }; } +function setCookie (name, value, days) { + var expires = ""; + if (days) { + var date = new Date (); + date.setTime(date.getTime () + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} + +function saveSize (s) { + setCookie ("oouiWindowWidth", s.width, 7); + setCookie ("oouiWindowHeight", s.height, 7); +} + // Main entrypoint function ooui (rootElementPath) { + var initialSize = getSize (); + saveSize (initialSize); + return; - var initialSize = getSize (); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; From 1207194e1e5829bd358808e6ddd966a62ad37e05 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 21:37:21 -0800 Subject: [PATCH 09/24] Use existing html nodes with socket --- Ooui/Client.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Ooui/Client.js b/Ooui/Client.js index 0abe37b..723a910 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -2,6 +2,7 @@ var debug = false; const nodes = {}; +const hasText = {}; let socket = null; @@ -56,8 +57,6 @@ function ooui (rootElementPath) { var initialSize = getSize (); saveSize (initialSize); - return; - var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; @@ -142,12 +141,22 @@ function getNode (id) { } } +function getOrCreateElement (id, tagName) { + var e = document.getElementById (id); + if (e) { + if (e.firstChild && e.firstChild.nodeType == Node.TEXT_NODE) + hasText[e.id] = true; + return e; + } + return document.createElement (tagName); +} + function msgCreate (m) { const id = m.id; const tagName = m.k; const node = tagName === "#text" ? document.createTextNode ("") : - document.createElement (tagName); + getOrCreateElement (id, tagName); if (tagName !== "#text") node.id = id; nodes[id] = node; @@ -183,6 +192,17 @@ function msgSetAttr (m) { if (debug) console.log ("SetAttr", node, m.k, m.v); } +function msgRemAttr (m) { + const id = m.id; + const node = getNode (id); + if (!node) { + console.error ("Unknown node id", m); + return; + } + node.removeAttribute(m.k); + if (debug) console.log ("RemAttr", node, m.k); +} + function msgCall (m) { const id = m.id; const node = getNode (id); @@ -192,6 +212,12 @@ function msgCall (m) { } const isJQuery = m.k.startsWith ("$."); const target = isJQuery ? $(node) : node; + if (m.k === "insertBefore" && m.v[0].nodeType == Node.TEXT_NODE && m.v[1] == null && hasText[id]) { + // Text is already set so it clear it first + if (target.firstChild) + target.removeChild (target.firstChild); + delete hasText[id]; + } const f = isJQuery ? target[m.k.slice(2)] : target[m.k]; if (debug) console.log ("Call", node, f, m.v); const r = f.apply (target, m.v); @@ -246,6 +272,9 @@ function processMessage (m) { case "setAttr": msgSetAttr (m); break; + case "remAttr": + msgRemAttr (m); + break; case "call": msgCall (m); break; From 0bbb3e95f512fcaa921d8717a0f6e76110df0a8e Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 22:19:47 -0800 Subject: [PATCH 10/24] Add verification to the computed widths --- Documentation/MeasureText.html | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Documentation/MeasureText.html b/Documentation/MeasureText.html index e645b1e..0e044ed 100644 --- a/Documentation/MeasureText.html +++ b/Documentation/MeasureText.html @@ -14,13 +14,16 @@ var ctx = canvas.getContext('2d'); var r = "static readonly double[] CharacterProportions = {\n "; var head = ""; -let size = 16; +let size = 24; ctx.font = "bold " + size + "px \"Helvetica Neue\""; var mmm = ctx.measureText("MM"); let sp = 0; let np = 0; let mw = 0; +let tx = 0; +let ty = size; +let widths = {}; for (let i = 0; i < 128; i++) { if (i > 0 && i % 8 == 0) { head = ",\n "; @@ -32,6 +35,13 @@ for (let i = 0; i < 128; i++) { let s = "M" + c + "M"; let m = ctx.measureText(s); let w = m.width - mmm.width; + if (tx + w > 320) { + tx = 0; + ty += size; + } + ctx.fillText(c, tx, ty); + ctx.strokeRect(tx, ty - size, w, size); + tx += w; let p = w / size; if (p > 1e-4) { sp += p; @@ -41,9 +51,19 @@ for (let i = 0; i < 128; i++) { mw = w; } r += head + p; + widths[c] = w; console.log (c + " = " + w); } +let test = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; +let testLen = 0; +for (const i in test) { + const w = widths[test[i]]; + testLen += w; +} +console.log("TEST COMP LEN = " + testLen); +console.log("TEST REAL LEN = " + ctx.measureText(test).width); + let ap = sp / np; let padding = (mmm.width - mw*2)/size; r += "\n};\nconst double AverageCharProportion = " + ap + ";"; From db417111fd2562067fe0a6669bd57574d0687392 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 22:52:39 -0800 Subject: [PATCH 11/24] Add Target to LinkView, LinkLabel, and Anchor Fixes #60 --- Ooui.Forms/Controls/LinkLabel.cs | 10 +++++++++- Ooui.Forms/Controls/LinkView.cs | 8 ++++++++ Ooui.Forms/Renderers/LinkLabelRenderer.cs | 6 ++++++ Ooui.Forms/Renderers/LinkViewRenderer.cs | 10 +++++++++- Ooui/Anchor.cs | 5 +++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Ooui.Forms/Controls/LinkLabel.cs b/Ooui.Forms/Controls/LinkLabel.cs index 5371101..de8384c 100644 --- a/Ooui.Forms/Controls/LinkLabel.cs +++ b/Ooui.Forms/Controls/LinkLabel.cs @@ -6,13 +6,21 @@ namespace Ooui.Forms public class LinkLabel : Xamarin.Forms.Label { public static readonly BindableProperty HRefProperty = BindableProperty.Create ("HRef", typeof (string), - typeof (LinkView), string.Empty, BindingMode.OneWay, null, null, null, null); + typeof (LinkLabel), string.Empty, BindingMode.OneWay, null, null, null, null); public string HRef { get { return (string)base.GetValue (HRefProperty); } set { base.SetValue (HRefProperty, value); } } + public static readonly BindableProperty TargetProperty = BindableProperty.Create ("Target", typeof (string), + typeof (LinkLabel), string.Empty, BindingMode.OneWay, null, null, null, null); + + public string Target { + get { return (string)base.GetValue (TargetProperty); } + set { base.SetValue (TargetProperty, value); } + } + public LinkLabel () { } diff --git a/Ooui.Forms/Controls/LinkView.cs b/Ooui.Forms/Controls/LinkView.cs index fd51d53..b372723 100644 --- a/Ooui.Forms/Controls/LinkView.cs +++ b/Ooui.Forms/Controls/LinkView.cs @@ -13,6 +13,14 @@ namespace Ooui.Forms set { base.SetValue (HRefProperty, value); } } + public static readonly BindableProperty TargetProperty = BindableProperty.Create ("Target", typeof (string), + typeof (LinkView), string.Empty, BindingMode.OneWay, null, null, null, null); + + public string Target { + get { return (string)base.GetValue (TargetProperty); } + set { base.SetValue (TargetProperty, value); } + } + public LinkView () { } diff --git a/Ooui.Forms/Renderers/LinkLabelRenderer.cs b/Ooui.Forms/Renderers/LinkLabelRenderer.cs index 3d7fe61..baece02 100644 --- a/Ooui.Forms/Renderers/LinkLabelRenderer.cs +++ b/Ooui.Forms/Renderers/LinkLabelRenderer.cs @@ -57,6 +57,7 @@ namespace Ooui.Forms.Renderers } UpdateHRef (); + UpdateTarget (); UpdateText (); UpdateTextColor (); @@ -107,6 +108,11 @@ namespace Ooui.Forms.Renderers Control.HRef = Element.HRef; } + void UpdateTarget () + { + Control.Target = Element.HRef; + } + void UpdateAlignment () { this.Style.Display = "table"; diff --git a/Ooui.Forms/Renderers/LinkViewRenderer.cs b/Ooui.Forms/Renderers/LinkViewRenderer.cs index 4ee222b..e2ceb2a 100644 --- a/Ooui.Forms/Renderers/LinkViewRenderer.cs +++ b/Ooui.Forms/Renderers/LinkViewRenderer.cs @@ -15,6 +15,7 @@ namespace Ooui.Forms.Renderers base.OnElementChanged (e); UpdateHRef (); + UpdateTarget (); } protected override void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e) @@ -24,13 +25,20 @@ namespace Ooui.Forms.Renderers if (Control == null) return; - if (e.PropertyName == Ooui.Forms.LinkLabel.HRefProperty.PropertyName) + if (e.PropertyName == Ooui.Forms.LinkView.HRefProperty.PropertyName) UpdateHRef (); + if (e.PropertyName == Ooui.Forms.LinkView.TargetProperty.PropertyName) + UpdateTarget (); } void UpdateHRef () { this.SetAttribute ("href", Element.HRef); } + + void UpdateTarget () + { + this.SetAttribute ("target", Element.Target); + } } } diff --git a/Ooui/Anchor.cs b/Ooui/Anchor.cs index 6ea2853..a475fca 100644 --- a/Ooui/Anchor.cs +++ b/Ooui/Anchor.cs @@ -9,6 +9,11 @@ namespace Ooui set => SetAttributeProperty ("href", value); } + public string Target { + get => GetStringAttribute ("target", ""); + set => SetAttributeProperty ("target", value); + } + public Anchor () : base ("a") { From 674f9d2fba54e090cbe06dd988a0ad32d4dc1011 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 09:20:24 -0800 Subject: [PATCH 12/24] Fix LinkLabel binding --- Ooui.Forms/Renderers/LinkLabelRenderer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Ooui.Forms/Renderers/LinkLabelRenderer.cs b/Ooui.Forms/Renderers/LinkLabelRenderer.cs index baece02..4e0f79c 100644 --- a/Ooui.Forms/Renderers/LinkLabelRenderer.cs +++ b/Ooui.Forms/Renderers/LinkLabelRenderer.cs @@ -79,6 +79,8 @@ namespace Ooui.Forms.Renderers if (e.PropertyName == Ooui.Forms.LinkLabel.HRefProperty.PropertyName) UpdateHRef (); + if (e.PropertyName == Ooui.Forms.LinkLabel.TargetProperty.PropertyName) + UpdateTarget (); else if (e.PropertyName == Xamarin.Forms.Label.HorizontalTextAlignmentProperty.PropertyName) UpdateAlignment (); else if (e.PropertyName == Xamarin.Forms.Label.VerticalTextAlignmentProperty.PropertyName) @@ -110,7 +112,7 @@ namespace Ooui.Forms.Renderers void UpdateTarget () { - Control.Target = Element.HRef; + Control.Target = Element.Target; } void UpdateAlignment () From ff9e9f749af3df74a00d0cb2eee28f4450d60c2a Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 17:15:38 -0800 Subject: [PATCH 13/24] Add wrapping support to text measuring This fixes #72 I also improved the default sizes of text-based elements including button. This fixes #69 --- Ooui.Forms/Extensions/ElementExtensions.cs | 4 +- Ooui.Forms/Extensions/FontExtensions.cs | 59 ++++++++++++++++------ Ooui.Forms/Renderers/ButtonRenderer.cs | 4 +- Ooui.Forms/Renderers/DatePickerRenderer.cs | 4 +- Ooui.Forms/Renderers/EntryRenderer.cs | 12 ++--- Ooui.Forms/Renderers/LabelRenderer.cs | 10 ++-- Ooui.Forms/Renderers/LinkLabelRenderer.cs | 9 ++-- Ooui.Forms/Renderers/SearchBarRenderer.cs | 4 +- Ooui.Forms/Renderers/TimePickerRenderer.cs | 5 +- Samples/WrappingTextSample.cs | 43 ++++++++++++++++ 10 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 Samples/WrappingTextSample.cs diff --git a/Ooui.Forms/Extensions/ElementExtensions.cs b/Ooui.Forms/Extensions/ElementExtensions.cs index 53c1366..32a07b2 100644 --- a/Ooui.Forms/Extensions/ElementExtensions.cs +++ b/Ooui.Forms/Extensions/ElementExtensions.cs @@ -14,7 +14,7 @@ namespace Ooui.Forms.Extensions var measured = false; if (self.Style.Width.Equals ("inherit")) { - s = self.Text.MeasureSize (self.Style); + s = self.Text.MeasureSize (self.Style, widthConstraint, heightConstraint); measured = true; rw = double.IsPositiveInfinity (s.Width) ? double.PositiveInfinity : Math.Ceiling (s.Width); } @@ -24,7 +24,7 @@ namespace Ooui.Forms.Extensions if (self.Style.Height.Equals ("inherit")) { if (!measured) { - s = self.Text.MeasureSize (self.Style); + s = self.Text.MeasureSize (self.Style, widthConstraint, heightConstraint); measured = true; } rh = double.IsPositiveInfinity (s.Height) ? double.PositiveInfinity : Math.Ceiling (s.Height * 1.4); diff --git a/Ooui.Forms/Extensions/FontExtensions.cs b/Ooui.Forms/Extensions/FontExtensions.cs index f364559..5588bda 100644 --- a/Ooui.Forms/Extensions/FontExtensions.cs +++ b/Ooui.Forms/Extensions/FontExtensions.cs @@ -38,36 +38,65 @@ namespace Ooui.Forms.Extensions } } - public static Size MeasureSize (this string text, string fontFamily, double fontSize, FontAttributes fontAttrs) + public static Size MeasureSize (this string text, string fontFamily, double fontSize, FontAttributes fontAttrs, double widthConstraint, double heightConstraint) { if (string.IsNullOrEmpty (text)) return Size.Zero; var fontHeight = fontSize; + var lineHeight = fontHeight * 1.4; var isBold = fontAttrs.HasFlag (FontAttributes.Bold); var props = isBold ? BoldCharacterProportions : CharacterProportions; var avgp = isBold ? BoldAverageCharProportion : AverageCharProportion; - var pwidth = 1.0e-6; // Tiny little padding to account for sampling errors - for (var i = 0; i < text.Length; i++) { - var c = (int)text[i]; - if (c < 128) { - pwidth += props[c]; - } - else { - pwidth += avgp; - } - } - var width = fontSize * pwidth; + var px = 0.0; + var lines = 1; + var maxPWidth = 0.0; + var pwidthConstraint = double.IsPositiveInfinity (widthConstraint) ? double.PositiveInfinity : widthConstraint / fontSize; + var lastSpaceWidth = -1.0; - return new Size (width, fontHeight); + // Tiny little padding to account for sampling errors + var pwidthHack = 1.0e-6; + var plineHack = 0.333; + + var n = text != null ? text.Length : 0; + + for (var i = 0; i < n; i++) { + var c = (int)text[i]; + var pw = (c < 128) ? props[c] : avgp; + // Should we wrap? + if (px + pw + plineHack > pwidthConstraint) { + lines++; + if (lastSpaceWidth > 0) { + maxPWidth = Math.Max (maxPWidth, lastSpaceWidth + pwidthHack); + px = pw - lastSpaceWidth; + lastSpaceWidth = -1; + } + else { + maxPWidth = Math.Max (maxPWidth, px + pwidthHack); + px = 0; + } + } + if (c == ' ') { + lastSpaceWidth = pw; + } + px += pw; + } + maxPWidth = Math.Max (maxPWidth, px + pwidthHack); + var width = fontSize * maxPWidth; + var height = lines * lineHeight; + + // Console.WriteLine ($"MEASURE TEXT SIZE {widthConstraint}x{heightConstraint} \"{text}\" == {width}x{height}"); + + return new Size (width, height); } - public static Size MeasureSize (this string text, Style style) + public static Size MeasureSize (this string text, Style style, double widthConstraint, double heightConstraint) { - return MeasureSize (text, "", 14, FontAttributes.None); + // System.Console.WriteLine("!!! MEASURE STYLED TEXT SIZE: " + style); + return MeasureSize (text, "", 14, FontAttributes.None, widthConstraint, heightConstraint); } public static string ToOouiTextAlign (this TextAlignment align) diff --git a/Ooui.Forms/Renderers/ButtonRenderer.cs b/Ooui.Forms/Renderers/ButtonRenderer.cs index 04f30b2..7687430 100644 --- a/Ooui.Forms/Renderers/ButtonRenderer.cs +++ b/Ooui.Forms/Renderers/ButtonRenderer.cs @@ -14,8 +14,8 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { - var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); - size = new Size (size.Width, size.Height * 1.428 + 14); + var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, widthConstraint, heightConstraint); + size = new Size (size.Width + 2 * Element.FontSize, size.Height + Element.FontSize); return new SizeRequest (size, size); } diff --git a/Ooui.Forms/Renderers/DatePickerRenderer.cs b/Ooui.Forms/Renderers/DatePickerRenderer.cs index 3df8d05..c97fd3b 100644 --- a/Ooui.Forms/Renderers/DatePickerRenderer.cs +++ b/Ooui.Forms/Renderers/DatePickerRenderer.cs @@ -13,8 +13,8 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { - var size = "00/00/0000".MeasureSize ("", 16.0, FontAttributes.None); - size = new Size (size.Width, size.Height * 1.428 + 14); + var size = "00/00/0000".MeasureSize ("", 16.0, FontAttributes.None, widthConstraint, heightConstraint); + size = new Size (size.Width, size.Height); return new SizeRequest (size, size); } diff --git a/Ooui.Forms/Renderers/EntryRenderer.cs b/Ooui.Forms/Renderers/EntryRenderer.cs index 4e54dbd..4704819 100644 --- a/Ooui.Forms/Renderers/EntryRenderer.cs +++ b/Ooui.Forms/Renderers/EntryRenderer.cs @@ -21,14 +21,12 @@ namespace Ooui.Forms.Renderers if (text == null || text.Length == 0) { text = Element.Placeholder; } - Size size; if (text == null || text.Length == 0) { - size = new Size (Element.FontSize * 0.25, Element.FontSize); + text = " "; } - else { - size = text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); - } - size = new Size (size.Width, size.Height * 1.428 + 14); + var size = text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, widthConstraint, heightConstraint); + var vpadding = Element.FontSize; + size = new Size (size.Width, size.Height + vpadding); return new SizeRequest (size, size); } @@ -149,7 +147,7 @@ namespace Ooui.Forms.Renderers { if (initialSize == Size.Zero) { var testString = "Tj"; - initialSize = testString.MeasureSize (Control.Style); + initialSize = testString.MeasureSize (Control.Style, double.PositiveInfinity, double.PositiveInfinity); } Element.SetStyleFont (Element.FontFamily, Element.FontSize, Element.FontAttributes, Control.Style); diff --git a/Ooui.Forms/Renderers/LabelRenderer.cs b/Ooui.Forms/Renderers/LabelRenderer.cs index 85ddc50..8405925 100644 --- a/Ooui.Forms/Renderers/LabelRenderer.cs +++ b/Ooui.Forms/Renderers/LabelRenderer.cs @@ -16,11 +16,12 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { + // System.Console.WriteLine($"Label.GetDesiredSize ({widthConstraint}, {heightConstraint})"); if (!_perfectSizeValid) { - var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, double.PositiveInfinity, double.PositiveInfinity); size.Width = Math.Ceiling (size.Width); - size.Height = Math.Ceiling (size.Height * 1.4); - _perfectSize = new SizeRequest (size, size); + size.Height = Math.Ceiling (size.Height); + _perfectSize = new SizeRequest (size, new Size (Element.FontSize, Element.FontSize)); _perfectSizeValid = true; } @@ -30,7 +31,8 @@ namespace Ooui.Forms.Renderers if (widthFits && heightFits) return _perfectSize; - var result = base.GetDesiredSize (widthConstraint, heightConstraint); + var resultRequestSize = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, widthConstraint, heightConstraint); + var result = new SizeRequest (resultRequestSize, resultRequestSize); var tinyWidth = Math.Min (10, result.Request.Width); result.Minimum = new Size (tinyWidth, result.Request.Height); diff --git a/Ooui.Forms/Renderers/LinkLabelRenderer.cs b/Ooui.Forms/Renderers/LinkLabelRenderer.cs index 4e0f79c..79cbce0 100644 --- a/Ooui.Forms/Renderers/LinkLabelRenderer.cs +++ b/Ooui.Forms/Renderers/LinkLabelRenderer.cs @@ -14,10 +14,10 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { if (!_perfectSizeValid) { - var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes); + var size = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, double.PositiveInfinity, double.PositiveInfinity); size.Width = Math.Ceiling (size.Width); - size.Height = Math.Ceiling (size.Height * 1.4); - _perfectSize = new SizeRequest (size, size); + size.Height = Math.Ceiling (size.Height); + _perfectSize = new SizeRequest (size, new Size (Element.FontSize, Element.FontSize)); _perfectSizeValid = true; } @@ -27,7 +27,8 @@ namespace Ooui.Forms.Renderers if (widthFits && heightFits) return _perfectSize; - var result = base.GetDesiredSize (widthConstraint, heightConstraint); + var resultRequestSize = Element.Text.MeasureSize (Element.FontFamily, Element.FontSize, Element.FontAttributes, widthConstraint, heightConstraint); + var result = new SizeRequest (resultRequestSize, resultRequestSize); var tinyWidth = Math.Min (10, result.Request.Width); result.Minimum = new Size (tinyWidth, result.Request.Height); diff --git a/Ooui.Forms/Renderers/SearchBarRenderer.cs b/Ooui.Forms/Renderers/SearchBarRenderer.cs index 9e117e5..adf5b87 100644 --- a/Ooui.Forms/Renderers/SearchBarRenderer.cs +++ b/Ooui.Forms/Renderers/SearchBarRenderer.cs @@ -27,9 +27,9 @@ namespace Ooui.Forms.Renderers } else { - size = text.MeasureSize(Element.FontFamily, Element.FontSize, Element.FontAttributes); + size = text.MeasureSize(Element.FontFamily, Element.FontSize, Element.FontAttributes, widthConstraint, heightConstraint); } - size = new Size(size.Width, size.Height * 1.428 + 14); + size = new Size(size.Width, size.Height + Element.FontSize); return new SizeRequest(size, size); } diff --git a/Ooui.Forms/Renderers/TimePickerRenderer.cs b/Ooui.Forms/Renderers/TimePickerRenderer.cs index b282e23..56fe30d 100644 --- a/Ooui.Forms/Renderers/TimePickerRenderer.cs +++ b/Ooui.Forms/Renderers/TimePickerRenderer.cs @@ -13,8 +13,9 @@ namespace Ooui.Forms.Renderers public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) { - var size = "00:00:00".MeasureSize(string.Empty, 16.0, FontAttributes.None); - size = new Size(size.Width, size.Height * 1.428 + 14); + var fontSize = 16.0; + var size = "00:00:00".MeasureSize(string.Empty, fontSize, FontAttributes.None, widthConstraint, heightConstraint); + size = new Size(size.Width, size.Height + fontSize); return new SizeRequest(size, size); } diff --git a/Samples/WrappingTextSample.cs b/Samples/WrappingTextSample.cs new file mode 100644 index 0000000..18e2daa --- /dev/null +++ b/Samples/WrappingTextSample.cs @@ -0,0 +1,43 @@ +using System; +using Xamarin.Forms; + +namespace Samples +{ + public class WrappingTextSample : ISample + { + public string Title => "Xamarin.Forms Wrapping Text"; + + public Ooui.Element CreateElement() + { + var rows = new StackLayout { Orientation = StackOrientation.Vertical }; + + var row0 = new StackLayout { Orientation = StackOrientation.Horizontal, BackgroundColor = Color.Azure }; + row0.Children.Add (new Label { Text = shortText, LineBreakMode = LineBreakMode.WordWrap }); + row0.Children.Add (new Label { Text = mediumText, LineBreakMode = LineBreakMode.WordWrap }); + row0.Children.Add (new Label { Text = longText, LineBreakMode = LineBreakMode.WordWrap }); + rows.Children.Add (row0); + + var row1 = new StackLayout { Orientation = StackOrientation.Horizontal, BackgroundColor = Color.GhostWhite }; + row1.Children.Add (new Label { Text = shortText, FontAttributes = FontAttributes.Bold, HorizontalOptions = LayoutOptions.Start }); + row1.Children.Add (new Label { Text = mediumText, FontAttributes = FontAttributes.Bold, HorizontalOptions = LayoutOptions.FillAndExpand }); + row1.Children.Add (new Label { Text = longText, FontAttributes = FontAttributes.Bold, HorizontalOptions = LayoutOptions.End }); + rows.Children.Add (row1); + + var page = new ContentPage + { + Content = rows + }; + + return page.GetOouiElement(); + } + + public void Publish() + { + Ooui.UI.Publish("/wrapping", CreateElement); + } + + const string shortText = "Lorem ipsum dolor sit amet."; + const string mediumText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + const string longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + } +} From b8a95536ef9d91c6df5d0121ba02c8867f4e3343 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 17:28:59 -0800 Subject: [PATCH 14/24] Improve the sample website --- .../AspNetCoreMvc/Views/Home/Index.cshtml | 25 ++++++++++++------- .../AspNetCoreMvc/Views/Shared/_Layout.cshtml | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml index a2cc879..a114823 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Home/Index.cshtml @@ -1,5 +1,7 @@ @{ ViewData["Title"] = "Ooui"; + var formsSamples = SamplesController.Samples.Where(x => x.Title.StartsWith("Xamarin.Forms ")); + var otherSamples = SamplesController.Samples.Where(x => !x.Title.StartsWith("Xamarin.Forms ")); }
-

Samples

+

Xamarin.Forms Samples

+
+
+

Plain Web Samples

+
    + @foreach (var s in otherSamples) {
  • @s.Title (Shared) @@ -25,10 +38,4 @@ }
-
-

Get it

- -
diff --git a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml index ae68cea..efe8f47 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml @@ -40,7 +40,7 @@ @RenderBody()
-

© 2017 - Frank A. Krueger

+

© 2017 - @DateTime.UtcNow.Year Frank A. Krueger

From 696e36343740159b52175007f7c36847c6fb4a0a Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 18:55:38 -0800 Subject: [PATCH 15/24] Save window size whenever it changes --- Ooui/Client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Ooui/Client.js b/Ooui/Client.js index 723a910..42f1a28 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -122,6 +122,7 @@ function ooui (rootElementPath) { k: "resize", v: getSize (), }; + saveSize (em.v); const ems = JSON.stringify (em); if (socket != null) socket.send (ems); From e24eef87c97b7ca14955d0cce65dd3ed351e4348 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 18:59:36 -0800 Subject: [PATCH 16/24] Put the version number in the js file Fixes #71 --- Ooui/Client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Ooui/Client.js b/Ooui/Client.js index 42f1a28..7e1116a 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -1,3 +1,4 @@ +// Ooui v1.0.0 var debug = false; From 957f9d73339434ac254b53ca0d0188b17eed28c7 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 23:14:07 -0800 Subject: [PATCH 17/24] Added issue #48 repro --- Ooui/Client.js | 2 +- Samples/SwitchErrorSample.cs | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Samples/SwitchErrorSample.cs diff --git a/Ooui/Client.js b/Ooui/Client.js index 7e1116a..9d9e458 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -1,6 +1,6 @@ // Ooui v1.0.0 -var debug = false; +var debug = true; const nodes = {}; const hasText = {}; diff --git a/Samples/SwitchErrorSample.cs b/Samples/SwitchErrorSample.cs new file mode 100644 index 0000000..184f90d --- /dev/null +++ b/Samples/SwitchErrorSample.cs @@ -0,0 +1,40 @@ +using Ooui; +using Xamarin.Forms; + +namespace Samples +{ + // From https://github.com/praeclarum/Ooui/issues/48 + public class SwitchErrorSample : ISample + { + public string Title => "Xamarin.Forms Switch Error"; + + public Ooui.Element CreateElement () + { + var layout = new StackLayout(); + var label = new Xamarin.Forms.Label + { + Text = "Switch state goes here", + HorizontalTextAlignment = TextAlignment.Center + }; + var sw = new Switch + { + HorizontalOptions = LayoutOptions.CenterAndExpand + }; + sw.Toggled += (sender, args) => + { + label.Text = $"Switch state is :{((Switch)sender).IsToggled}"; + }; + layout.Children.Add(label); + layout.Children.Add(sw); + return new ContentPage + { + Content = layout + }.GetOouiElement(); + } + + public void Publish() + { + UI.Publish ("/switch", CreateElement); + } + } +} From 8c97a625e1816c64b2b76ec1f09acf208add49ea Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 19:51:35 -0800 Subject: [PATCH 18/24] Fix checkbox IsChecked property --- Ooui/Element.cs | 2 +- Ooui/Input.cs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Ooui/Element.cs b/Ooui/Element.cs index 2e32512..e3193e1 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -116,7 +116,7 @@ namespace Ooui protected bool SetBooleanAttributeProperty (string attributeName, bool newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { var old = GetAttribute (attributeName) != null; - if (old != newValue) + if (old == newValue) return false; if (newValue) SetAttribute (attributeName, string.Empty); diff --git a/Ooui/Input.cs b/Ooui/Input.cs index 9574d11..d114c2d 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -40,11 +40,7 @@ namespace Ooui public bool IsChecked { get => GetBooleanAttribute ("checked"); - set { - if (SetBooleanAttributeProperty ("checked", value)) { - TriggerEventFromMessage (Message.Event (Id, "change", IsChecked)); - } - } + set => SetBooleanAttributeProperty ("checked", value); } public double Minimum { From e4b1c44aa3a05766408c77313908a1d9c6b8aeb5 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 20:18:03 -0800 Subject: [PATCH 19/24] Remove jQuery --- Ooui.Forms/Renderers/SwitchRenderer.cs | 2 -- Ooui/Client.js | 15 +++------------ Ooui/Element.cs | 14 -------------- Ooui/UI.cs | 3 --- 4 files changed, 3 insertions(+), 31 deletions(-) diff --git a/Ooui.Forms/Renderers/SwitchRenderer.cs b/Ooui.Forms/Renderers/SwitchRenderer.cs index 737eb50..8dd639b 100644 --- a/Ooui.Forms/Renderers/SwitchRenderer.cs +++ b/Ooui.Forms/Renderers/SwitchRenderer.cs @@ -27,9 +27,7 @@ namespace Ooui.Forms.Renderers if (e.NewElement != null) { if (Control == null) { var input = new Input (InputType.Checkbox); - input.SetAttribute ("data-toggle", "toggle"); SetNativeControl (input); - input.Call ("$.bootstrapToggle"); Control.Change += OnControlValueChanged; } diff --git a/Ooui/Client.js b/Ooui/Client.js index 9d9e458..d30cb0c 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -84,19 +84,11 @@ function ooui (rootElementPath) { const messages = JSON.parse (event.data); if (debug) console.log("Messages", messages); if (Array.isArray (messages)) { - const jqs = [] messages.forEach (function (m) { // console.log('Raw value from server', m.v); m.v = fixupValue (m.v); - if (m.k.startsWith ("$.")) { - jqs.push (m); - } - else { - processMessage (m); - } + processMessage (m); }); - // Run jQuery functions last since they usually require a fully built DOM - jqs.forEach (processMessage); } }); @@ -212,15 +204,14 @@ function msgCall (m) { console.error ("Unknown node id", m); return; } - const isJQuery = m.k.startsWith ("$."); - const target = isJQuery ? $(node) : node; + const target = node; if (m.k === "insertBefore" && m.v[0].nodeType == Node.TEXT_NODE && m.v[1] == null && hasText[id]) { // Text is already set so it clear it first if (target.firstChild) target.removeChild (target.firstChild); delete hasText[id]; } - const f = isJQuery ? target[m.k.slice(2)] : target[m.k]; + const f = target[m.k]; if (debug) console.log ("Call", node, f, m.v); const r = f.apply (target, m.v); if (typeof m.rid === 'string' || m.rid instanceof String) { diff --git a/Ooui/Element.cs b/Ooui/Element.cs index e3193e1..ea203b1 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -230,20 +230,6 @@ namespace Ooui SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); } - protected override bool SaveStateMessageIfNeeded (Message message) - { - if (message.TargetId != Id) - return false; - - switch (message.MessageType) { - case MessageType.Call when message.Key.StartsWith ("$.", StringComparison.Ordinal): - AddStateMessage (message); - return true; - default: - return base.SaveStateMessageIfNeeded (message); - } - } - protected virtual bool HtmlNeedsFullEndElement => false; public override void WriteOuterHtml (System.Xml.XmlWriter w) diff --git a/Ooui/UI.cs b/Ooui/UI.cs index c865a3c..540f74c 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -40,7 +40,6 @@ namespace Ooui @Title - @@ -49,8 +48,6 @@ namespace Ooui @InitialHtml - - From d58625129b992d6150e008652a43a94dd7d882c9 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 20:51:05 -0800 Subject: [PATCH 20/24] Write custom switch control --- Ooui.Forms/Renderers/SwitchRenderer.cs | 51 +++++++++++++++++++++++++- Ooui/Client.js | 2 +- Ooui/Color.cs | 40 ++++++++++++++++++-- Samples/SwitchErrorSample.cs | 2 +- 4 files changed, 87 insertions(+), 8 deletions(-) diff --git a/Ooui.Forms/Renderers/SwitchRenderer.cs b/Ooui.Forms/Renderers/SwitchRenderer.cs index 8dd639b..771e737 100644 --- a/Ooui.Forms/Renderers/SwitchRenderer.cs +++ b/Ooui.Forms/Renderers/SwitchRenderer.cs @@ -3,7 +3,7 @@ using Xamarin.Forms; namespace Ooui.Forms.Renderers { - public class SwitchRenderer : ViewRenderer + public class SwitchRenderer : ViewRenderer { public override SizeRequest GetDesiredSize (double widthConstraint, double heightConstraint) { @@ -26,7 +26,7 @@ namespace Ooui.Forms.Renderers if (e.NewElement != null) { if (Control == null) { - var input = new Input (InputType.Checkbox); + var input = new SwitchElement (); SetNativeControl (input); Control.Change += OnControlValueChanged; } @@ -47,5 +47,52 @@ namespace Ooui.Forms.Renderers { Control.IsChecked = Element.IsToggled; } + + public class SwitchElement : Div + { + public event EventHandler Change; + bool isChecked = false; + readonly Div knob = new Div (); + public bool IsChecked { + get => isChecked; + set { + isChecked = value; + UpdateUI (); + } + } + public SwitchElement () + { + AppendChild (knob); + knob.Style.Position = "absolute"; + knob.Style.BorderRadius = "10px"; + knob.Style.Cursor = "pointer"; + knob.Style.Top = "2px"; + knob.Style.Width = "18px"; + knob.Style.Height = "34px"; + + Style.BorderRadius = "10px"; + Style.Cursor = "pointer"; + Style.BorderStyle = "solid"; + Style.BorderWidth = "2px"; + Click += (s, e) => { + IsChecked = !IsChecked; + Change?.Invoke (this, EventArgs.Empty); + }; + UpdateUI (); + } + + void UpdateUI () + { + Style.BackgroundColor = isChecked ? "#337ab7" : "#888"; + Style.BorderColor = Style.BackgroundColor; + knob.Style.BackgroundColor = isChecked ? "#FFF" : "#EEE"; + if (isChecked) { + knob.Style.Left = "34px"; + } + else { + knob.Style.Left = "2px"; + } + } + } } } diff --git a/Ooui/Client.js b/Ooui/Client.js index d30cb0c..e50d972 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -1,6 +1,6 @@ // Ooui v1.0.0 -var debug = true; +var debug = false; const nodes = {}; const hasText = {}; diff --git a/Ooui/Color.cs b/Ooui/Color.cs index 476e1cd..0c072d6 100644 --- a/Ooui/Color.cs +++ b/Ooui/Color.cs @@ -65,15 +65,47 @@ namespace Ooui if (styleValue == "inherit") return Colors.Clear; - //if (styleValue[0] == '#' && styleValue.Length == 4) { - //} + if (styleValue[0] == '#' && styleValue.Length == 4) { + var r = ReadHexNibble (styleValue[1]); + var g = ReadHexNibble (styleValue[2]); + var b = ReadHexNibble (styleValue[3]); + return new Color (r, g, b, 255); + } - //if (styleValue[0] == '#' && styleValue.Length == 7) { - //} + if (styleValue[0] == '#' && styleValue.Length == 7) { + var r = ReadHexByte (styleValue[1], styleValue[2]); + var g = ReadHexByte (styleValue[3], styleValue[4]); + var b = ReadHexByte (styleValue[5], styleValue[6]); + return new Color (r, g, b, 255); + } throw new ArgumentException ($"Cannot parse color string `{styleValue}`", nameof (styleValue)); } + static byte ReadHexByte (char c0, char c1) + { + var n0 = ReadHex (c0); + var n1 = ReadHex (c1); + return (byte)((n0 << 4) | n1); + } + + static byte ReadHexNibble (char c) + { + var n = ReadHex (c); + return (byte)((n << 4) | n); + } + + static byte ReadHex (char c) + { + if ('0' <= c && c <= '9') + return (byte)(c - '0'); + if ('a' <= c && c <= 'z') + return (byte)((c - 'a') + 10); + if ('A' <= c && c <= 'Z') + return (byte)((c - 'A') + 10); + return 0; + } + public override string ToString () { if (A == 255) diff --git a/Samples/SwitchErrorSample.cs b/Samples/SwitchErrorSample.cs index 184f90d..98ba63c 100644 --- a/Samples/SwitchErrorSample.cs +++ b/Samples/SwitchErrorSample.cs @@ -22,7 +22,7 @@ namespace Samples }; sw.Toggled += (sender, args) => { - label.Text = $"Switch state is :{((Switch)sender).IsToggled}"; + label.Text = $"Switch state is: {((Switch)sender).IsToggled}"; }; layout.Children.Add(label); layout.Children.Add(sw); From 589e848f441e9a2e9febb9a56f073ed3c5019794 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 21:03:42 -0800 Subject: [PATCH 21/24] Trigger Change event when changing the checked state --- Ooui/EventTarget.cs | 18 ++++++++++++++++++ Ooui/Input.cs | 6 +++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index 4e8c273..a37b69a 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -182,6 +182,24 @@ namespace Ooui return true; } + protected virtual bool TriggerEvent (string name) + { + List handlers = null; + lock (eventListeners) { + List hs; + if (eventListeners.TryGetValue (name, out hs)) { + handlers = new List (hs); + } + } + if (handlers != null) { + var args = new TargetEventArgs (); + foreach (var h in handlers) { + h.Invoke (this, args); + } + } + return true; + } + protected virtual bool TriggerEventFromMessage (Message message) { if (message.TargetId != Id) diff --git a/Ooui/Input.cs b/Ooui/Input.cs index d114c2d..966bf08 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -40,7 +40,11 @@ namespace Ooui public bool IsChecked { get => GetBooleanAttribute ("checked"); - set => SetBooleanAttributeProperty ("checked", value); + set { + if (SetBooleanAttributeProperty ("checked", value)) { + TriggerEvent ("change"); + } + } } public double Minimum { From 449abbe4d77a7ab0bf36ccf4acb1643b88926b40 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 21:52:34 -0800 Subject: [PATCH 22/24] Remove confusing comparison entry --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 467f7f8..ea5a098 100644 --- a/README.md +++ b/README.md @@ -165,13 +165,6 @@ When the user clicks or otherwise interacts with the UI, those events are sent b CSS - -Is there databinding? -No :-( -Yes! -Debatable - - Do I need to run a server? Nope From bd802c7d1b9168020cce5499ac78dd5f57923358 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 22:43:55 -0800 Subject: [PATCH 23/24] Add Razor TagHelper for Ooui You can now use to embed Ooui in Razor pages. Fixes #63 --- Ooui.AspNetCore/TagHelpers/OouiTagHelper.cs | 16 ++++++++++++++++ .../AspNetCoreMvc/Views/Shared/_Layout.cshtml | 3 +++ .../AspNetCoreMvc/Views/_ViewImports.cshtml | 1 + 3 files changed, 20 insertions(+) create mode 100644 Ooui.AspNetCore/TagHelpers/OouiTagHelper.cs diff --git a/Ooui.AspNetCore/TagHelpers/OouiTagHelper.cs b/Ooui.AspNetCore/TagHelpers/OouiTagHelper.cs new file mode 100644 index 0000000..09da17a --- /dev/null +++ b/Ooui.AspNetCore/TagHelpers/OouiTagHelper.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Ooui.AspNetCore.TagHelpers +{ + public class OouiTagHelper : TagHelper + { + public Ooui.Element Element { get; set; } + + public override void Process (TagHelperContext context, TagHelperOutput output) + { + output.TagName = "div"; + output.TagMode = TagMode.StartTagAndEndTag; + output.Content.SetHtmlContent (Element.OuterHtml); + } + } +} diff --git a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml index efe8f47..17113f6 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml @@ -41,6 +41,9 @@

© 2017 - @DateTime.UtcNow.Year Frank A. Krueger

+ @{ var e = new Ooui.Sa.Button("Hello World"); } + +
diff --git a/PlatformSamples/AspNetCoreMvc/Views/_ViewImports.cshtml b/PlatformSamples/AspNetCoreMvc/Views/_ViewImports.cshtml index 2b7dee4..e5a9861 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/_ViewImports.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/_ViewImports.cshtml @@ -2,3 +2,4 @@ @using AspNetCoreMvc.Models @using AspNetCoreMvc.Controllers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Ooui.AspNetCore From eb5851c15164ab847e19e05d1d2232c123085945 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 2 Feb 2018 23:00:24 -0800 Subject: [PATCH 24/24] Fix the sample home page --- Ooui/Anchor.cs | 13 +++++++++++++ .../AspNetCoreMvc/Views/Shared/_Layout.cshtml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Ooui/Anchor.cs b/Ooui/Anchor.cs index a475fca..10d4c8f 100644 --- a/Ooui/Anchor.cs +++ b/Ooui/Anchor.cs @@ -18,5 +18,18 @@ namespace Ooui : base ("a") { } + + public Anchor (string href) + : this () + { + HRef = href; + } + + public Anchor (string href, string text) + : this () + { + HRef = href; + Text = text; + } } } diff --git a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml index 17113f6..df16ff9 100644 --- a/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml +++ b/PlatformSamples/AspNetCoreMvc/Views/Shared/_Layout.cshtml @@ -41,7 +41,7 @@