Merge pull request #73 from praeclarum/prerender
Prerender HTML to help SEO and load time
This commit is contained in:
commit
01c452acfd
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Ooui.AspNetCore
|
namespace Ooui.AspNetCore
|
||||||
|
@ -21,13 +22,35 @@ namespace Ooui.AspNetCore
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
response.StatusCode = 200;
|
response.StatusCode = 200;
|
||||||
response.ContentType = "text/html; charset=utf-8";
|
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 sessionId = WebSocketHandler.BeginSession (context.HttpContext, element);
|
||||||
var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title);
|
var initialHtml = element.OuterHtml;
|
||||||
|
var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml);
|
||||||
var htmlBytes = Encoding.UTF8.GetBytes (html);
|
var htmlBytes = Encoding.UTF8.GetBytes (html);
|
||||||
response.ContentLength = htmlBytes.Length;
|
response.ContentLength = htmlBytes.Length;
|
||||||
using (var s = response.Body) {
|
using (var s = response.Body) {
|
||||||
await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ namespace Ooui.Forms.Renderers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual bool ManageNativeControlLifetime => true;
|
protected virtual bool ManageNativeControlLifetime => true;
|
||||||
|
|
||||||
|
protected override bool HtmlNeedsFullEndElement => TagName == "div";
|
||||||
|
|
||||||
public ViewRenderer (string tagName = "div")
|
public ViewRenderer (string tagName = "div")
|
||||||
: base (tagName)
|
: base (tagName)
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace Ooui.Forms.Renderers
|
||||||
|
|
||||||
if (_iframe != null)
|
if (_iframe != null)
|
||||||
{
|
{
|
||||||
_iframe.Src = html;
|
_iframe.Source = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -47,7 +47,7 @@ namespace Ooui.Forms.Renderers
|
||||||
|
|
||||||
if (_iframe != null)
|
if (_iframe != null)
|
||||||
{
|
{
|
||||||
_iframe.Src = url;
|
_iframe.Source = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
@ -62,6 +62,8 @@ namespace Ooui.Forms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool HtmlNeedsFullEndElement => TagName == "div";
|
||||||
|
|
||||||
public VisualElementRenderer (string tagName = "div") : base (tagName)
|
public VisualElementRenderer (string tagName = "div") : base (tagName)
|
||||||
{
|
{
|
||||||
_propertyChangedHandler = OnElementPropertyChanged;
|
_propertyChangedHandler = OnElementPropertyChanged;
|
||||||
|
|
|
@ -4,10 +4,9 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Anchor : Element
|
public class Anchor : Element
|
||||||
{
|
{
|
||||||
string href = "";
|
|
||||||
public string HRef {
|
public string HRef {
|
||||||
get => href;
|
get => GetStringAttribute ("href", "");
|
||||||
set => SetProperty (ref href, value ?? "", "href");
|
set => SetAttributeProperty ("href", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Anchor ()
|
public Anchor ()
|
||||||
|
|
|
@ -9,8 +9,8 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
ButtonType typ = ButtonType.Submit;
|
ButtonType typ = ButtonType.Submit;
|
||||||
public ButtonType Type {
|
public ButtonType Type {
|
||||||
get => typ;
|
get => GetAttribute ("type", ButtonType.Submit);
|
||||||
set => SetProperty (ref typ, value, "type");
|
set => SetAttributeProperty ("type", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button ()
|
public Button ()
|
||||||
|
|
|
@ -7,16 +7,14 @@ namespace Ooui
|
||||||
CanvasRenderingContext2D context2d = new CanvasRenderingContext2D ();
|
CanvasRenderingContext2D context2d = new CanvasRenderingContext2D ();
|
||||||
int gotContext2d = 0;
|
int gotContext2d = 0;
|
||||||
|
|
||||||
int width = 300;
|
|
||||||
public int Width {
|
public int Width {
|
||||||
get => width;
|
get => GetAttribute ("width", 300);
|
||||||
set => SetProperty (ref width, value <= 0 ? 150 : value, "width");
|
set => SetAttributeProperty ("width", value < 0 ? 0 : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
int height = 150;
|
|
||||||
public int Height {
|
public int Height {
|
||||||
get => height;
|
get => GetAttribute ("height", 150);
|
||||||
set => SetProperty (ref height, value <= 0 ? 150 : value, "height");
|
set => SetAttributeProperty ("height", value < 0 ? 0 : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Canvas ()
|
public Canvas ()
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
var debug = false;
|
var debug = false;
|
||||||
|
|
||||||
const nodes = {};
|
const nodes = {};
|
||||||
|
const hasText = {};
|
||||||
|
|
||||||
let socket = null;
|
let socket = null;
|
||||||
|
|
||||||
|
@ -35,10 +36,27 @@ 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
|
// Main entrypoint
|
||||||
function ooui (rootElementPath) {
|
function ooui (rootElementPath) {
|
||||||
|
|
||||||
var initialSize = getSize ();
|
var initialSize = getSize ();
|
||||||
|
saveSize (initialSize);
|
||||||
|
|
||||||
var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") +
|
var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") +
|
||||||
"w=" + initialSize.width + "&h=" + initialSize.height;
|
"w=" + initialSize.width + "&h=" + initialSize.height;
|
||||||
|
|
||||||
|
@ -123,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) {
|
function msgCreate (m) {
|
||||||
const id = m.id;
|
const id = m.id;
|
||||||
const tagName = m.k;
|
const tagName = m.k;
|
||||||
const node = tagName === "#text" ?
|
const node = tagName === "#text" ?
|
||||||
document.createTextNode ("") :
|
document.createTextNode ("") :
|
||||||
document.createElement (tagName);
|
getOrCreateElement (id, tagName);
|
||||||
if (tagName !== "#text")
|
if (tagName !== "#text")
|
||||||
node.id = id;
|
node.id = id;
|
||||||
nodes[id] = node;
|
nodes[id] = node;
|
||||||
|
@ -164,6 +192,17 @@ function msgSetAttr (m) {
|
||||||
if (debug) console.log ("SetAttr", node, m.k, m.v);
|
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) {
|
function msgCall (m) {
|
||||||
const id = m.id;
|
const id = m.id;
|
||||||
const node = getNode (id);
|
const node = getNode (id);
|
||||||
|
@ -173,6 +212,12 @@ function msgCall (m) {
|
||||||
}
|
}
|
||||||
const isJQuery = m.k.startsWith ("$.");
|
const isJQuery = m.k.startsWith ("$.");
|
||||||
const target = isJQuery ? $(node) : node;
|
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];
|
const f = isJQuery ? target[m.k.slice(2)] : target[m.k];
|
||||||
if (debug) console.log ("Call", node, f, m.v);
|
if (debug) console.log ("Call", node, f, m.v);
|
||||||
const r = f.apply (target, m.v);
|
const r = f.apply (target, m.v);
|
||||||
|
@ -227,6 +272,9 @@ function processMessage (m) {
|
||||||
case "setAttr":
|
case "setAttr":
|
||||||
msgSetAttr (m);
|
msgSetAttr (m);
|
||||||
break;
|
break;
|
||||||
|
case "remAttr":
|
||||||
|
msgRemAttr (m);
|
||||||
|
break;
|
||||||
case "call":
|
case "call":
|
||||||
msgCall (m);
|
msgCall (m);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -5,6 +5,8 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Div : Element
|
public class Div : Element
|
||||||
{
|
{
|
||||||
|
protected override bool HtmlNeedsFullEndElement => true;
|
||||||
|
|
||||||
public Div ()
|
public Div ()
|
||||||
: base ("div")
|
: base ("div")
|
||||||
{
|
{
|
||||||
|
|
164
Ooui/Element.cs
164
Ooui/Element.cs
|
@ -1,28 +1,29 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace Ooui
|
namespace Ooui
|
||||||
{
|
{
|
||||||
public abstract class Element : Node
|
public abstract class Element : Node
|
||||||
{
|
{
|
||||||
string className = "";
|
readonly Dictionary<string, object> attributes = new Dictionary<string, object> ();
|
||||||
|
|
||||||
public string ClassName {
|
public string ClassName {
|
||||||
get => className;
|
get => GetStringAttribute ("class", "");
|
||||||
set => SetProperty (ref className, value, "className");
|
set => SetAttributeProperty ("class", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Style Style { get; private set; } = new Style ();
|
public Style Style { get; private set; } = new Style ();
|
||||||
|
|
||||||
string title = "";
|
|
||||||
public string Title {
|
public string Title {
|
||||||
get => title;
|
get => GetStringAttribute ("title", "");
|
||||||
set => SetProperty (ref title, value, "title");
|
set => SetAttributeProperty ("title", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hidden = false;
|
bool hidden = false;
|
||||||
public bool IsHidden {
|
public bool IsHidden {
|
||||||
get => hidden;
|
get => GetBooleanAttribute ("hidden");
|
||||||
set => SetProperty (ref hidden, value, "hidden");
|
set => SetBooleanAttributeProperty ("hidden", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event TargetEventHandler Click {
|
public event TargetEventHandler Click {
|
||||||
|
@ -102,8 +103,64 @@ namespace Ooui
|
||||||
Style.PropertyChanged += HandleStylePropertyChanged;
|
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 {
|
Send (new Message {
|
||||||
MessageType = MessageType.SetAttribute,
|
MessageType = MessageType.SetAttribute,
|
||||||
TargetId = Id,
|
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<T> (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)
|
void HandleStylePropertyChanged (object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]);
|
SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]);
|
||||||
|
@ -130,5 +243,38 @@ namespace Ooui
|
||||||
return base.SaveStateMessageIfNeeded (message);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,12 +83,12 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool SetProperty<T> (ref T backingStore, T newValue, string attributeName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
protected bool SetProperty<T> (ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
||||||
{
|
{
|
||||||
if (EqualityComparer<T>.Default.Equals (backingStore, newValue))
|
if (EqualityComparer<T>.Default.Equals (backingStore, newValue))
|
||||||
return false;
|
return false;
|
||||||
backingStore = newValue;
|
backingStore = newValue;
|
||||||
SendSet (attributeName, newValue);
|
SendSet (jsPropertyName, newValue);
|
||||||
OnPropertyChanged (propertyName);
|
OnPropertyChanged (propertyName);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -120,12 +120,12 @@ namespace Ooui
|
||||||
Send (Message.Call (Id, methodName, args));
|
Send (Message.Call (Id, methodName, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SendSet (string attributeName, object value)
|
protected void SendSet (string jsPropertyName, object value)
|
||||||
{
|
{
|
||||||
Send (new Message {
|
Send (new Message {
|
||||||
MessageType = MessageType.Set,
|
MessageType = MessageType.Set,
|
||||||
TargetId = Id,
|
TargetId = Id,
|
||||||
Key = attributeName,
|
Key = jsPropertyName,
|
||||||
Value = value,
|
Value = value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -169,6 +169,11 @@ namespace Ooui
|
||||||
state.Add (message);
|
state.Add (message);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case MessageType.RemoveAttribute:
|
||||||
|
this.UpdateStateMessages (state => {
|
||||||
|
state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
case MessageType.Listen:
|
case MessageType.Listen:
|
||||||
AddStateMessage (message);
|
AddStateMessage (message);
|
||||||
break;
|
break;
|
||||||
|
|
14
Ooui/Form.cs
14
Ooui/Form.cs
|
@ -6,20 +6,18 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
string action = "";
|
string action = "";
|
||||||
public string Action {
|
public string Action {
|
||||||
get => action;
|
get => GetStringAttribute ("action", "");
|
||||||
set => SetProperty (ref action, value ?? "", "action");
|
set => SetAttributeProperty ("action", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
string method = "GET";
|
|
||||||
public string Method {
|
public string Method {
|
||||||
get => method;
|
get => GetStringAttribute ("method", "GET");
|
||||||
set => SetProperty (ref method, value ?? "", "method");
|
set => SetAttributeProperty ("method", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
string enctype = "application/x-www-form-urlencoded";
|
|
||||||
public string EncodingType {
|
public string EncodingType {
|
||||||
get => enctype;
|
get => GetStringAttribute ("enctype", "application/x-www-form-urlencoded");
|
||||||
set => SetProperty (ref enctype, value ?? "", "enctype");
|
set => SetAttributeProperty ("enctype", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public event TargetEventHandler Submit {
|
public event TargetEventHandler Submit {
|
||||||
|
|
|
@ -4,16 +4,15 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public abstract class FormControl : Element
|
public abstract class FormControl : Element
|
||||||
{
|
{
|
||||||
string name = "";
|
|
||||||
public string Name {
|
public string Name {
|
||||||
get => name;
|
get => GetStringAttribute ("name", "");
|
||||||
set => SetProperty (ref name, value, "name");
|
set => SetAttributeProperty ("name", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDisabled = false;
|
bool isDisabled = false;
|
||||||
public bool IsDisabled {
|
public bool IsDisabled {
|
||||||
get => isDisabled;
|
get => GetBooleanAttribute ("disabled");
|
||||||
set => SetProperty (ref isDisabled, value, "disabled");
|
set => SetBooleanAttributeProperty ("disabled", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FormControl (string tagName)
|
public FormControl (string tagName)
|
||||||
|
|
|
@ -2,17 +2,15 @@
|
||||||
{
|
{
|
||||||
public class Iframe : Element
|
public class Iframe : Element
|
||||||
{
|
{
|
||||||
public Iframe()
|
public string Source
|
||||||
: base("iframe")
|
|
||||||
{
|
{
|
||||||
|
get => GetStringAttribute ("src", null);
|
||||||
|
set => SetAttributeProperty ("src", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
string src = null;
|
public Iframe ()
|
||||||
public string Src
|
: base ("iframe")
|
||||||
{
|
{
|
||||||
get => src;
|
|
||||||
set => SetProperty(ref src, value, "src");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Image : Element
|
public class Image : Element
|
||||||
{
|
{
|
||||||
string src = "";
|
public string Source
|
||||||
public string Source {
|
{
|
||||||
get => src;
|
get => GetStringAttribute ("src", null);
|
||||||
set => SetProperty (ref src, value ?? "", "src");
|
set => SetAttributeProperty ("src", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image ()
|
public Image ()
|
||||||
|
|
|
@ -7,16 +7,14 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Input : FormControl
|
public class Input : FormControl
|
||||||
{
|
{
|
||||||
InputType typ = InputType.Text;
|
|
||||||
public InputType Type {
|
public InputType Type {
|
||||||
get => typ;
|
get => GetAttribute ("type", InputType.Text);
|
||||||
set => SetProperty (ref typ, value, "type");
|
set => SetAttributeProperty ("type", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
string val = "";
|
|
||||||
public string Value {
|
public string Value {
|
||||||
get => val;
|
get => GetStringAttribute ("value", "");
|
||||||
set => SetProperty (ref val, value ?? "", "value");
|
set => SetAttributeProperty ("value", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public double NumberValue {
|
public double NumberValue {
|
||||||
|
@ -35,37 +33,33 @@ namespace Ooui
|
||||||
remove => RemoveEventListener ("change", value);
|
remove => RemoveEventListener ("change", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
string placeholder = "";
|
|
||||||
public string Placeholder {
|
public string Placeholder {
|
||||||
get => placeholder;
|
get => GetStringAttribute ("placeholder", "");
|
||||||
set => SetProperty (ref placeholder, value, "placeholder");
|
set => SetAttributeProperty ("placeholder", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isChecked = false;
|
|
||||||
public bool IsChecked {
|
public bool IsChecked {
|
||||||
get => isChecked;
|
get => GetBooleanAttribute ("checked");
|
||||||
set {
|
set {
|
||||||
SetProperty (ref isChecked, value, "checked");
|
if (SetBooleanAttributeProperty ("checked", value)) {
|
||||||
TriggerEventFromMessage (Message.Event (Id, "change", isChecked));
|
TriggerEventFromMessage (Message.Event (Id, "change", IsChecked));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double minimum = 0;
|
|
||||||
public double Minimum {
|
public double Minimum {
|
||||||
get => minimum;
|
get => GetAttribute ("min", 0.0);
|
||||||
set => SetProperty (ref minimum, value, "min");
|
set => SetAttributeProperty ("min", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
double maximum = 100;
|
|
||||||
public double Maximum {
|
public double Maximum {
|
||||||
get => maximum;
|
get => GetAttribute ("max", 100.0);
|
||||||
set => SetProperty (ref maximum, value, "max");
|
set => SetAttributeProperty ("max", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
double step = 1;
|
|
||||||
public double Step {
|
public double Step {
|
||||||
get => step;
|
get => GetAttribute ("step", 1.0);
|
||||||
set => SetProperty (ref step, value, "step");
|
set => SetAttributeProperty ("step", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Input ()
|
public Input ()
|
||||||
|
@ -86,10 +80,10 @@ namespace Ooui
|
||||||
if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) {
|
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
|
// Don't need to notify here because the base implementation will fire the event
|
||||||
if (Type == InputType.Checkbox) {
|
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 {
|
else {
|
||||||
val = message.Value != null ? Convert.ToString (message.Value) : "";
|
UpdateAttributeProperty ("value", message.Value != null ? Convert.ToString (message.Value) : "", "Value");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return base.TriggerEventFromMessage (message);
|
return base.TriggerEventFromMessage (message);
|
||||||
|
|
|
@ -4,10 +4,9 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Label : Element
|
public class Label : Element
|
||||||
{
|
{
|
||||||
Element htmlFor = null;
|
|
||||||
public Element For {
|
public Element For {
|
||||||
get => htmlFor;
|
get => GetAttribute<Element> ("for", null);
|
||||||
set => SetProperty (ref htmlFor, value, "htmlFor");
|
set => SetAttributeProperty ("for", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Label ()
|
public Label ()
|
||||||
|
|
|
@ -48,6 +48,8 @@ namespace Ooui
|
||||||
Set,
|
Set,
|
||||||
[EnumMember (Value = "setAttr")]
|
[EnumMember (Value = "setAttr")]
|
||||||
SetAttribute,
|
SetAttribute,
|
||||||
|
[EnumMember(Value = "remAttr")]
|
||||||
|
RemoveAttribute,
|
||||||
[EnumMember(Value = "call")]
|
[EnumMember(Value = "call")]
|
||||||
Call,
|
Call,
|
||||||
[EnumMember(Value = "listen")]
|
[EnumMember(Value = "listen")]
|
||||||
|
|
19
Ooui/Node.cs
19
Ooui/Node.cs
|
@ -180,5 +180,24 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
return false;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,19 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Option : Element
|
public class Option : Element
|
||||||
{
|
{
|
||||||
string val = "";
|
|
||||||
public string Value {
|
public string Value {
|
||||||
get => val;
|
get => GetStringAttribute ("value", "");
|
||||||
set => SetProperty (ref val, value ?? "", "value");
|
set => SetAttributeProperty ("value", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
string label = "";
|
|
||||||
public string Label {
|
public string Label {
|
||||||
get => label;
|
get => GetStringAttribute ("label", "");
|
||||||
set => SetProperty (ref label, value ?? "", "label");
|
set => SetAttributeProperty ("label", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool defaultSelected = false;
|
|
||||||
public bool DefaultSelected {
|
public bool DefaultSelected {
|
||||||
get => defaultSelected;
|
get => GetBooleanAttribute ("selected");
|
||||||
set => SetProperty (ref defaultSelected, value, "defaultSelected");
|
set => SetBooleanAttributeProperty ("selected", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option ()
|
public Option ()
|
||||||
|
|
|
@ -4,10 +4,9 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Select : FormControl
|
public class Select : FormControl
|
||||||
{
|
{
|
||||||
string val = "";
|
|
||||||
public string Value {
|
public string Value {
|
||||||
get => val;
|
get => GetStringAttribute ("value", "");
|
||||||
set => SetProperty (ref val, value ?? "", "value");
|
set => SetAttributeProperty ("value", value ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public event TargetEventHandler Change {
|
public event TargetEventHandler Change {
|
||||||
|
@ -35,6 +34,7 @@ namespace Ooui
|
||||||
protected override void OnChildInsertedBefore (Node newChild, Node referenceChild)
|
protected override void OnChildInsertedBefore (Node newChild, Node referenceChild)
|
||||||
{
|
{
|
||||||
base.OnChildInsertedBefore (newChild, referenceChild);
|
base.OnChildInsertedBefore (newChild, referenceChild);
|
||||||
|
var val = Value;
|
||||||
if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) {
|
if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) {
|
||||||
val = o.Value;
|
val = o.Value;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,8 @@ namespace Ooui
|
||||||
protected override bool TriggerEventFromMessage (Message message)
|
protected override bool TriggerEventFromMessage (Message message)
|
||||||
{
|
{
|
||||||
if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) {
|
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);
|
return base.TriggerEventFromMessage (message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -399,7 +399,7 @@ namespace Ooui
|
||||||
o.Append (head);
|
o.Append (head);
|
||||||
o.Append (p.Key);
|
o.Append (p.Key);
|
||||||
o.Append (":");
|
o.Append (":");
|
||||||
o.Append (String.Format (System.Globalization.CultureInfo.InvariantCulture, "{0}", p.Value));
|
o.Append (Convert.ToString (p.Value, System.Globalization.CultureInfo.InvariantCulture));
|
||||||
head = ";";
|
head = ";";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,18 +20,19 @@ namespace Ooui
|
||||||
set => SetProperty (ref val, value ?? "", "value");
|
set => SetProperty (ref val, value ?? "", "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
int rows = 2;
|
|
||||||
public int Rows {
|
public int Rows {
|
||||||
get => rows;
|
get => GetAttribute ("rows", 2);
|
||||||
set => SetProperty (ref rows, value, "rows");
|
set => SetAttributeProperty ("rows", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
int cols = 20;
|
int cols = 20;
|
||||||
public int Columns {
|
public int Columns {
|
||||||
get => cols;
|
get => GetAttribute ("cols", 20);
|
||||||
set => SetProperty (ref cols, value, "cols");
|
set => SetAttributeProperty ("cols", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool HtmlNeedsFullEndElement => true;
|
||||||
|
|
||||||
public TextArea ()
|
public TextArea ()
|
||||||
: base ("textarea")
|
: base ("textarea")
|
||||||
{
|
{
|
||||||
|
@ -53,5 +54,10 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
return base.TriggerEventFromMessage (message);
|
return base.TriggerEventFromMessage (message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void WriteInnerHtml (System.Xml.XmlWriter w)
|
||||||
|
{
|
||||||
|
w.WriteString (val ?? "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,5 +20,10 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
Text = text;
|
Text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void WriteOuterHtml (System.Xml.XmlWriter w)
|
||||||
|
{
|
||||||
|
w.WriteString (text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,10 @@ namespace Ooui
|
||||||
<style>@Styles</style>
|
<style>@Styles</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id=""ooui-body"" class=""container-fluid""></div>
|
|
||||||
|
<div id=""ooui-body"" class=""container-fluid"">
|
||||||
|
@InitialHtml
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type=""text/javascript"" src=""https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js""></script>
|
<script type=""text/javascript"" src=""https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js""></script>
|
||||||
<script type=""text/javascript"" src=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js""></script>
|
<script type=""text/javascript"" src=""https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js""></script>
|
||||||
|
@ -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
|
class DataHandler : RequestHandler
|
||||||
|
|
|
@ -58,8 +58,8 @@ namespace Tests
|
||||||
Assert.AreEqual (480, c.Height);
|
Assert.AreEqual (480, c.Height);
|
||||||
c.Width = 0;
|
c.Width = 0;
|
||||||
c.Height = -100;
|
c.Height = -100;
|
||||||
Assert.AreEqual (150, c.Width);
|
Assert.AreEqual (0, c.Width);
|
||||||
Assert.AreEqual (150, c.Height);
|
Assert.AreEqual (0, c.Height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ("<textarea style=\"background-color:#18f\">Hello World!</textarea>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TextAreaEmptyStyled ()
|
||||||
|
{
|
||||||
|
var e = new TextArea ();
|
||||||
|
e.Style.BackgroundColor = "#18f";
|
||||||
|
Assert.AreEqual ("<textarea style=\"background-color:#18f\"></textarea>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Style ()
|
||||||
|
{
|
||||||
|
var e = new Div ();
|
||||||
|
e.Style.BackgroundColor = "#18f";
|
||||||
|
Assert.AreEqual ("<div style=\"background-color:#18f\"></div>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TwoGrandChildren ()
|
||||||
|
{
|
||||||
|
var e = new Div (new Div (new Anchor (), new Anchor ()), new Paragraph ());
|
||||||
|
Assert.AreEqual ("<div><div><a /><a /></div><p /></div>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Child ()
|
||||||
|
{
|
||||||
|
var e = new Div (new Anchor ());
|
||||||
|
Assert.AreEqual ("<div><a /></div>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TextChild ()
|
||||||
|
{
|
||||||
|
var e = new Paragraph ("Hello world!");
|
||||||
|
Assert.AreEqual ("<p>Hello world!</p>", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void IdIsFirst ()
|
||||||
|
{
|
||||||
|
var e = new Anchor ();
|
||||||
|
Assert.IsTrue (e.OuterHtml.StartsWith ("<a id=\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void EmptyElement ()
|
||||||
|
{
|
||||||
|
var e = new Anchor ();
|
||||||
|
Assert.AreEqual ("<a />", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void AnchorHRef ()
|
||||||
|
{
|
||||||
|
var e = new Anchor {
|
||||||
|
HRef = "http://google.com"
|
||||||
|
};
|
||||||
|
Assert.AreEqual ("<a href=\"http://google.com\" />", OuterHtmlWithoutIds (e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue