| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112 | using System;using System.Collections.Generic;using System.Drawing;using System.Drawing.Imaging;using System.IO;using System.Linq;using System.Linq.Expressions;using System.Reflection;using System.Text;using System.Text.RegularExpressions;using Comal.Classes;using InABox.Clients;using InABox.Core;using PRSClasses;using PRSServer.Properties;using RazorEngine.Templating;using RazorEngine.Text;using Syncfusion.Pdf.Parsing;using Color = System.Windows.Media.Color;using ColorConverter = System.Windows.Media.ColorConverter;using Encoder = System.Drawing.Imaging.Encoder;namespace PRSServer{    /// <summary>    ///     Provides utility functions for writing Razor template pages    /// </summary>    public class WebUtils    {        #region Entity Editor        /// <summary>        ///     Builds an editor for a given entity.        /// </summary>        /// <remarks>To get reasonable formatting, import <see cref="WebUtils.DigitalFormViewerStyle" /> in a <style> tag</remarks>        /// <typeparam name="T">The type of <paramref name="entity" /></typeparam>        /// <param name="entity">The entity which contains the data to be pre-inserted into the editor</param>        /// <param name="id">An optional HTML id to add to root <div> of the generated item.</param>        /// <param name="classList">An optional list of HTML classes to add to root <div> of the generated item.</param>        /// <returns>A string containing raw HTML markup</returns>        public static string BuildEntityEditor<T>(T entity, string? id = null, string[]? classList = null) where T : Entity        {            var builder = new StringBuilder();            builder.AppendFormat(                @"<div class=""prs-web_utils-entity_editor{0}""{1}>",                classList != null ? " " + string.Join(" ", classList) : "",                id != null ? " id=\"" + id + "\"" : ""            );            var layout = DFLayout.GenerateEntityLayout<T>();            var data = WebHandler.SerializeEntityForEditing(typeof(T), entity);            var markup = BuildDigitalFormViewer(layout, data, false, submitLabel: "Save", saveLabel: null);            builder.Append(markup).Append("</div>");            return builder.ToString();        }        #endregion        #region CoreTable Viewer        /// <summary>        ///     Used for determining which columns to show in an HTML representation of a CoreTable.        ///     EntityTableColumns.ALL means to show every column        ///     EntityTableColumns.VISIBLE means only show columns which do not have their visiblity set to hidden        ///     EntityTableColumns.REQUIRED means only show columns which have their visibility set to visible.        ///     EntityTableColumns.LOOK_UP means to use the Lookup associated with the entity to generate the columns.        /// </summary>        public enum EntityTableColumns        {            ALL,            VISIBLE,            LOOKUP,            REQUIRED        }        private static List<string> GetColumnsForTable<T>(EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity        {            if (showType == EntityTableColumns.LOOKUP) return LookupFactory.DefineColumns(typeof(T)).ColumnNames().ToList<string>();            var columns = new List<string>();            var properties = CoreUtils.PropertyInfoList(                typeof(T),                x => x.GetCustomAttribute<DoNotPersist>() == null &&                     x.GetCustomAttribute<DoNotSerialize>() == null &&                     x.PropertyType != typeof(UserProperties), true);            switch (showType)            {                case EntityTableColumns.VISIBLE:                    foreach (var property in properties)                    {                        var editor = property.Value.GetEditor();                        if (editor != null && editor.Visible != Visible.Hidden) columns.Add(property.Key);                    }                    break;                case EntityTableColumns.REQUIRED:                    foreach (var property in properties)                    {                        var editor = property.Value.GetEditor();                        if (editor != null && editor.Visible == Visible.Default) columns.Add(property.Key);                    }                    break;                case EntityTableColumns.ALL:                    foreach (var property in properties.Keys) columns.Add(property);                    break;            }            return columns;        }        /// <summary>        ///     Returns a string containing an HTML table representing the data present in a CoreTable.        /// </summary>        /// <typeparam name="T">The type of the entity that the table represents.</typeparam>        /// <param name="table">The table to be converted to HTML</param>        /// <param name="showType">A flag to determine which columns to show.</param>        /// <returns>The HTML string</returns>        public static string BuildHTMLTableFromCoreTable<T>(CoreTable table, EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity        {            var columns = GetColumnsForTable<T>(showType);            var html = new List<string>();            html.Add("<table class=\"prs-web_utils-entity_table entity-" + typeof(T).Name + "\"><caption>");            html.Add(typeof(T).Name);            html.Add("</caption><thead><tr>");            foreach (var column in columns)            {                html.Add("<th>");                html.Add(column);                html.Add("</th>");            }            html.Add("</tr></thead>");            foreach (var row in table.Rows)            {                html.Add("<tr>");                foreach (var column in columns)                {                    html.Add("<td>");                    html.Add(row.Get<object?>(column)?.ToString() ?? "NULL");                    html.Add("</td>");                }                html.Add("</tr>");            }            html.Add("</table>");            html.Add("</table>");            return string.Concat(html);        }        /// <summary>        ///     Returns a string containing an HTML table representing the data present in a CoreTable.        /// </summary>        /// <param name="entityType">The type of the entity that the table represents</param>        /// <param name="table">The table to be converted to HTML</param>        /// <param name="showType">A flag to determine which columns to show.</param>        /// <returns>The HTML string</returns>        public static string BuildHTMLTableFromCoreTable(Type entityType, CoreTable table, EntityTableColumns showType = EntityTableColumns.ALL)        {            var fnc = typeof(WebUtils).GetMethod(nameof(BuildHTMLTableFromCoreTable), 1, new[] { typeof(CoreTable), typeof(EntityTableColumns) })!                .MakeGenericMethod(entityType);            return (fnc.Invoke(null, new object[] { table, showType }) as string)!;        }        #endregion        #region Extra Utilities        public static string ConvertStringToPlainHTML(string text)        {            return text.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).ReplaceLineEndings("<br/>");        }        public enum ImageEncoding        {            JPEG        }        private static ImageCodecInfo? GetEncoder(ImageFormat format)        {            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();            foreach (ImageCodecInfo codec in codecs)            {                if (codec.FormatID == format.Guid)                {                    return codec;                }            }            return null;        }        public static List<byte[]> RenderPDFToImages(byte[] pdfData, ImageEncoding encoding = ImageEncoding.JPEG)        {            var rendered = new List<byte[]>();            PdfLoadedDocument loadeddoc = new PdfLoadedDocument(pdfData);            Bitmap[] images = loadeddoc.ExportAsImage(0, loadeddoc.Pages.Count - 1);            var jpgEncoder = GetEncoder(ImageFormat.Jpeg)!;            var quality = Encoder.Quality;            var encodeParams = new EncoderParameters(1);            encodeParams.Param[0] = new EncoderParameter(quality, 100L);            if (images != null)                foreach (var image in images)                {                    using(var data = new MemoryStream())                    {                        image.Save(data, jpgEncoder, encodeParams);                        rendered.Add(data.ToArray());                    }                }            return rendered;        }        private static readonly ImageConverter converter = new();        public static string WebDocumentToDataURL(string code)        {            var document = new Client<Document>().Query(                new Filter<Document>(x => x.ID)                    .InQuery(new SubQuery<WebDocument>(                        new Filter<WebDocument>(x => x.Code).IsEqualTo(code),                        new Column<WebDocument>(x => x.Document.ID))),                Columns.None<Document>().Add(x => x.Data));            var data = (byte[]?)document.Rows.FirstOrDefault()?["Data"];            return "data:;base64," + (data != null ? Convert.ToBase64String(data) : "");        }        public static Guid? WebDocumentID(string code)        {            var document = new Client<Document>().Query(                new Filter<Document>(x => x.ID)                    .InQuery(new SubQuery<WebDocument>(                        new Filter<WebDocument>(x => x.Code).IsEqualTo(code),                        new Column<WebDocument>(x => x.Document.ID))),                Columns.None<Document>().Add(x => x.Data));            return (Guid?)document.Rows.FirstOrDefault()?["ID"];        }        public static string ResourceToDataURL(Bitmap resource)        {            return "data:;base64," + Convert.ToBase64String((byte[])converter.ConvertTo(resource, typeof(byte[]))!);        }        #endregion        #region Security        public static bool UserCanAccess<T>(IDataModel model) where T : ISecurityDescriptor, new()        {            var userTable = model.GetTable<User>();            if (userTable.Rows.Count == 0) return false;            var user = userTable.Rows[0].ToObject<User>();            return Security.IsAllowed<T>(user.ID, user.SecurityGroup.ID);        }        public static bool IsLoggedIn(IDataModel model)        {            var userTable = model.GetTable<User>();            return userTable.Rows.Count > 0;        }        public static User? GetUser(IDataModel model)        {            var userTable = model.GetTable<User>();            if (userTable.Rows.Count == 0) return null;            return userTable.Rows[0].ToObject<User>();        }        #endregion        #region Directory Viewer        public static string DirectoryViewerStyle { get; } = @"<style>.prs-web_utils-file_manager {    width: 30em;    height: 40em;    padding: 0.5em;     text-align: left;    overflow: auto;    border: 1px solid black;    background-color: white;    box-sizing: border-box;}.prs-web_utils-file_manager * {    box-sizing: border-box;}.prs-web_utils-file_manager-file,.prs-web_utils-file_manager-directory_head {    font-size: 1em;    color: black;    width: 100%;    overflow: hidden;    white-space: nowrap;    text-overflow: ellipsis;        text-align: left;        padding-left: 1.5em;    background-position: left;    background-repeat: no-repeat;    background-size: contain;}.prs-web_utils-file_manager-file {    border: #0000 solid 1px;    background-image: url(" + ResourceToDataURL(Resources.file) + @");}.prs-web_utils-file_manager-directory_head {    background-color: white;    background-image: url(" + ResourceToDataURL(Resources.folder) + @");}.prs-web_utils-file_manager-pdf_type {    background-image: url(" + ResourceToDataURL(Resources.pdf) + @");}.prs-web_utils-file_manager-png_type {    background-image: url(" + ResourceToDataURL(Resources.png) + @");}.prs-web_utils-file_manager-jpg_type {    background-image: url(" + ResourceToDataURL(Resources.jpg) + @");}.prs-web_utils-file_manager-directory_head:active {    cursor: pointer;    background-color: #CCCCCC}@media (hover:hover) {    .prs-web_utils-file_manager-directory_head:hover {        cursor: pointer;        background-color: #DDDDDD    }    .prs-web_utils-file_manager-file:hover {        cursor: pointer;        background-color: #b8d9e7;        border: #79c4e5 solid 1px;    }}.prs-web_utils-file_manager-download {    display: inline-block;    width: 1.5em;    height: 1.5em;    padding: 0.5em;    float: right;    border: solid 1px black;    background-image: url(" + ResourceToDataURL(Resources.download) + @");    background-size: 100% 100%;}.prs-web_utils-file_manager-directory {    width: 100%;    padding-left: 0.5em;    border-left: dashed 1px #AAA;    border-bottom: dashed 1px #AAA;}.prs-web_utils-file_manager-collapsed {    display: none;}</style>";        private interface IFileItem        {            public string FilePath { get; }            public string FileName { get; }        }        private class FileItem : IFileItem        {            public FileItem(string filePath, string fileName)            {                FilePath = filePath;                FileName = fileName;            }            public string FilePath { get; }            public string FileName { get; }        }        private class DirectoryItem : IFileItem        {            public readonly List<DirectoryItem> Directories = new();            public readonly List<FileItem> Files = new();            public DirectoryItem(string filePath, string fileName)            {                FilePath = filePath;                FileName = fileName;            }            public string FilePath { get; }            public string FileName { get; }        }        private static DirectoryItem? GetDirectoryStructure(string path, Func<string, bool>? fileFilter, Func<string, bool>? directoryFilter,            string? name = null, int depth = 2)        {            var fileName = name ?? path;            if (depth == 0) return new DirectoryItem(path, fileName);            if (!Directory.Exists(path)) return null;            var directory = new DirectoryItem(path, fileName);            foreach (var item in Directory.GetDirectories(path))                if (directoryFilter?.Invoke(item) != false)                    try                    {                        var subDirectory = GetDirectoryStructure(item, fileFilter, directoryFilter, Path.GetFileName(item), depth - 1);                        if (subDirectory != null) directory.Directories.Add(subDirectory);                    }                    catch (UnauthorizedAccessException)                    {                    }            foreach (var item in Directory.GetFiles(path))                if (fileFilter?.Invoke(item) != false)                    directory.Files.Add(new FileItem(item, Path.GetFileName(item)));            return directory;        }        private static DirectoryItem? GetDirectoryStructure(string path, string? fileFilter, string? directoryFilter, string? name = null,            int depth = 2)        {            var fileFilterRgx = fileFilter != null ? new Regex(fileFilter) : null;            var dirFilterRgx = directoryFilter != null ? new Regex(directoryFilter) : null;            return GetDirectoryStructure(                path,                fileFilterRgx != null ? x => fileFilterRgx.IsMatch(x) : null,                dirFilterRgx != null ? x => dirFilterRgx.IsMatch(x) : null,                name, depth            );        }        private static readonly string DirectoryViewerScript = @"<script>function headClickEventListener(e){    var next = e.target.nextSibling;    next.classList.toggle(""prs-web_utils-file_manager-collapsed"");}var heads = document.getElementsByClassName(""prs-web_utils-file_manager-directory_head"");for(var head of heads){    head.addEventListener(""click"", headClickEventListener);}</script>";        private static void BuildDirectory(StringBuilder builder, DirectoryItem directory)        {            builder.Append(@"<div class=""prs-web_utils-file_manager-directory_head"">");            builder.Append(directory.FileName);            builder.Append(@"</div>");            builder.Append(@"<div class=""prs-web_utils-file_manager-directory prs-web_utils-file_manager-collapsed""/>");            if (directory.Directories.Count == 0 && directory.Files.Count == 0)            {                builder.Append(@"Folder is empty");            }            else            {                foreach (var subDirectory in directory.Directories) BuildDirectory(builder, subDirectory);                foreach (var file in directory.Files)                {                    var typeClass = Path.GetExtension(file.FileName) switch                    {                        ".png" => "prs-web_utils-file_manager-png_type",                        ".pdf" => "prs-web_utils-file_manager-pdf_type",                        ".jpg" => "prs-web_utils-file_manager-jpg_type",                        ".jpeg" => "prs-web_utils-file_manager-jpg_type",                        _ => null                    };                    builder.Append(@"<div class=""prs-web_utils-file_manager-file");                    if (typeClass != null) builder.Append(" " + typeClass);                    builder.Append(string.Format(@""" data-file=""{0}"">", file.FilePath));                    builder.Append(@"<div class=""prs-web_utils-file_manager-download""></div>");                    builder.Append(file.FileName);                    builder.Append(@"</div>");                }            }            builder.Append(@"</div>");        }        /// <summary>        /// </summary>        /// <param name="path">The path to the folder in which to initialise the directory viewer</param>        /// <param name="fileFilter">A file filter which is compiled to a Regex. Regex.IsMatch is used to filter files</param>        /// <param name="directoryFilter">        ///     A directory filter which is compiled to a Regex. Regex.IsMatch is used to filter        ///     directories        /// </param>        /// <param name="alias">The name to call the root folder. If null, defaults to the directory path.</param>        /// <param name="id">An HTML id attribute for the viewer</param>        /// <param name="classList">An HTML class list for the viewer</param>        /// <param name="depth">The depth of directories to search</param>        /// <returns></returns>        public static string BuildDirectoryViewer(string path, Func<string, bool> fileFilter, Func<string, bool>? directoryFilter = null,            string? alias = null, string? id = null, string[]? classList = null, int depth = 2)        {            var stringBuilder = new StringBuilder();            stringBuilder.Append(@"<div ");            if (id != null) stringBuilder.Append("id=\"" + id + "\" ");            stringBuilder.Append(@"class=""prs-web_utils-file_manager");            if (classList != null)                foreach (var className in classList)                    stringBuilder.Append(" " + className);            stringBuilder.Append(@""">");            var structure = GetDirectoryStructure(path, fileFilter, directoryFilter, alias ?? path, depth);            if (structure != null) BuildDirectory(stringBuilder, structure);            stringBuilder.Append(@"</div>");            stringBuilder.Append(DirectoryViewerScript);            return stringBuilder.ToString();        }        public static string BuildDirectoryViewer(string path, string fileFilter, string? directoryFilter = null, string? alias = null,            string? id = null,            string[]? classList = null, int depth = 2)        {            var fileFilterRgx = new Regex(fileFilter);            var dirFilterRgx = directoryFilter != null ? new Regex(directoryFilter) : null;            return BuildDirectoryViewer(                path,                x => fileFilterRgx.IsMatch(x),                dirFilterRgx != null ? x => dirFilterRgx.IsMatch(x) : null,                alias, id, classList, depth            );        }        #endregion        #region JSON Entities        public static Dictionary<string, object> SerializeEntity(Type entityType, Entity entity)        {            var data = new Dictionary<string, object>();            foreach (var property in DatabaseSchema.Properties(entityType))            {                data[property.Name] = CoreUtils.GetPropertyValue(entity, property.Name);            }            return data;        }        /// <summary>        /// Generates a JSON object for javascript        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="entity"></param>        /// <returns></returns>        public static string EntityToJSON<T>(T entity) where T : Entity        {            var serializedEntity = SerializeEntity(typeof(T), entity);            var serializedJSON = Serialization.Serialize(serializedEntity);            return serializedJSON;        }        public static string CreateJSONEntity<T>() where T : Entity, new()        {            var serializedEntity = SerializeEntity(typeof(T), new T());            var serializedJSON = Serialization.Serialize(serializedEntity);            return serializedJSON;        }        /// <summary>        /// Generate code for setting a property of a JSON entity        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="entityExpr">A Javascript expression that evaluates to the entity JSON</param>        /// <param name="expression"></param>        /// <returns></returns>        public static string JSONSetProperty<T>(string entityExpr, Expression<Func<T, object>> expression, string value) where T : Entity        {            var propName = CoreUtils.GetFullPropertyName(expression, ".");            return $"({entityExpr})[\"{propName}\"]={value}";        }        /// <summary>        /// Generate code for getting a property of a JSON entity        /// </summary>        /// <typeparam name="T"></typeparam>        /// <param name="entityExpr">A Javascript expression that evaluates to the entity JSON</param>        /// <param name="expression"></param>        /// <returns></returns>        public static string JSONGetProperty<T>(string entityExpr, Expression<Func<T, object>> expression) where T : Entity        {            var propName = CoreUtils.GetFullPropertyName(expression, ".");            return $"({entityExpr})[\"{propName}\"]";        }        #endregion        #region Digital Form Viewer        private const string _multiImageStyle = @".prs-web_utils-digital_form-multi_image {    }.prs-web_utils-digital_form-multi_image_cont {    overflow: auto;    position: relative;    height: 10em;    background: #999;    box-shadow: black inset 0 0 4px;}.prs-web_utils-digital_form-multi_image_cont > div {    position: absolute;    top: 0;    bottom: 0;    display: flex;    gap: 1em;    padding: 0.4em;}.prs-web_utils-digital_form-multi_image_cont img {    max-height: 100%;}.prs-web_utils-digital_form-multi_image_cont img.selected {    border: dashed 2px black;}.prs-web_utils-digital_form-multi_image_btns {    display: flex;    gap: 0.2em;}.prs-web_utils-digital_form-multi_image_btns .prs-button {    flex: 1;    height: 4em;    display: flex;    justify-content: center;    align-items: center;}";        private static string _digitalFormViewerStyle = @".prs-web_utils-digital_form {    display: grid;    column-gap: 0.4em;    row-gap: 0.4em;}.prs-web_utils-digital_form-field {    min-height: 2em;}@media (pointer:coarse), (pointer:none) {    .prs-web_utils-digital_form-field {        min-height: 4em;    }}.prs-web_utils-digital_form * {    box-sizing: border-box;}.prs-web_utils-digital_form-label {    text-align: left;    padding: 1.5em 0.3em;    align-self: center;    overflow-wrap: anywhere;}.prs-web_utils-digital_form-image {    max-width: 100%;    max-height: 100%;}.prs-web_utils-digital_form-color,.prs-web_utils-digital_form-integer,.prs-web_utils-digital_form-text,.prs-web_utils-digital_form-string,.prs-web_utils-digital_form-url,.prs-web_utils-digital_form-password,.prs-web_utils-digital_form-date,.prs-web_utils-digital_form-datetime {    background-color: white;}.prs-web_utils-digital_form-time {    display: flex;    align-items: center;    width: min-content;        background: white;    border: solid 1px gray;}.prs-web_utils-digital_form-time_hours {    text-align: right;}.prs-web_utils-digital_form-time_mins {    text-align: left;}.prs-web_utils-digital_form-time span {    margin: 0.1em;}.prs-web_utils-digital_form-time input {    height: 100%;    width: 3em;    font-family: monospace;    border: none;    background: none;}.prs-web_utils-digital_form-time input:active {    background: #ddd;}.prs-web_utils-digital_form-timestamp {    display: flex;    justify-content: space-between;    align-items: center;    gap: 0.4em;}.prs-web_utils-digital_form-timestamp input {    flex: 1;    height: 100%;}.prs-web_utils-digital_form-timestamp .prs-button {    height: 100%;    padding: 1em;    display: flex;    align-items: center;}.prs-web_utils-digital_form-pin {    display: flex;    justify-content: space-between;    align-items: center;    gap: 0.4em;}.prs-web_utils-digital_form-pin input {    flex: 1;    height: 100%;}.prs-web_utils-digital_form-pin .prs-button {    height: 100%;    padding: 1em;    display: flex;    align-items: center;}.prs-web_utils-digital_form-document {    display: flex;    justify-content: space-between;    align-items: center;    gap: 0.4em;}.prs-web_utils-digital_form-document input {    flex: 1;    height: 100%;}.prs-web_utils-digital_form-document .prs-button {    height: 100%;    padding: 1em;    display: flex;    align-items: center;}.prs-web_utils-digital_form-notes {    display: flex;    gap: 0.2em;}.prs-web_utils-digital_form-note_cont {    flex: 1;        display: flex;    flex-direction: column;    gap: 0.2em;}.prs-web_utils-digital_form-note {    resize: none;    width: 100%;    min-height: 6em;        background-color: #cdcdcd;}.prs-web_utils-digital_form-note.active {    background-color: lightgoldenrodyellow;}.prs-web_utils-digital_form-note_error {    color: black;    text-align: left;}.prs-web_utils-digital_form-note_add {    display: flex;    justify-content: center;    align-items: center;    text-align: center;    background-color: #dedede;    border: solid 1px black;    border-radius: 3px;    user-select: none;        width: 2em;}@media (hover:hover) {    .prs-web_utils-digital_form-note_add:hover {        cursor: pointer;        background-color: #cacaca;    }}.prs-web_utils-digital_form-note_add:active {    background-color: #ccc;}.prs-web_utils-digital_form-embedded_image, .prs-web_utils-digital_form-signature {    max-width: 100%;    border-radius: 3px;    min-height: 2em;    display: flex;    flex-direction: column;}.prs-web_utils-digital_form-img_cont {    flex: 1;    overflow: hidden;}.prs-web_utils-digital_form-clear_img {    height: 4em;    display: flex;    justify-content: center;    align-items: center;}.prs-web_utils-digital_form-img {    max-width: 100%;    max-height: 100%;}div.prs-web_utils-digital_form-boolean {    display: flex;    align-items: center;    gap: 0.2em;}.prs-web_utils-digital_form-boolean label {    flex: 1;    text-align: left;}input.prs-web_utils-digital_form-boolean {    width: min-content;    height: min-content;    margin: auto;}.prs-web_utils-digital_form-lookup, .prs-web_utils-digital_form-option {    min-width: 0;    min-height: 2em;    background-color: white;}.prs-web_utils-digital_form-option_fake {    visibility:hidden;    margin-left:1em;    overflow:hidden;    min-height: 2em;}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button {    display: inline-block;    flex: 1;    height: 100%;    position: relative;    height: 2em;        background: none;    border: none;}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input {    appearance: none;    background-color: #a7a7a7;    border-radius: 3px;    border: dashed 1px black;    margin: 0;        width: 100%;    height: 100%;}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked {    background-color: #e7e7e7;    border: solid 1px black;}@media (hover:hover) {    .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:hover {        cursor:pointer;        background-color: #a3a3a3;    }    .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked:hover {        background-color: #e0e0e0;    }}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:active {    background-color: #979797;}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked:active {    background-color: #c6c6c6;}.prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button span {    position: absolute;    left: 0;    right: 0;    top: 0;    bottom: 0;    pointer-events: none;        line-height: 2em;}.prs-web_utils-digital_form-radio_cont {    display: flex;    justify-content: stretch;    align-items: center;}.prs-web_utils-digital_form-radio_cont label {    flex: 1;    text-align: left;}.prs-web_utils-digital_form-save_cont {    margin-top: 2em;    display: flex;    gap: 0.2em;}.prs-web_utils-digital_form-save_cont .prs-button {    height: 4em;    flex: 1;    display: flex;    align-items: center;    justify-content: center;}.prs-web_utils-digital_form-add_task {    display: flex;    gap: 0.4em;}.prs-web_utils-digital_form-add_task .prs-button {    height: 4em;    padding: 0 1em;        display: flex;    align-items: center;    justify-content: center;}.prs-web_utils-digital_form-add_task input {    flex: 1;}.prs-web_utils-digital_form input:invalid {    background-color: #FFDDDD;}.prs-web_utils-digital_form-field:required {    border: solid 1px #edaf58;}.prs-web_utils-digital_form-validate {    position: absolute;    top: 1em;    left: 1em;    height: 3em;    line-height: 3em;    padding: 0 1em;    background: white;    border: black solid 1px;    border-radius: 3px;    box-shadow: grey 0 0 10px;    transition: opacity 1s linear 0s;}" + _multiImageStyle;        /// <summary>        ///     A string which contains a stylesheet used for formatting digital forms. This is not necessary, but can be useful as        ///     a base.<br />        ///     Include within the <style> tag in your document head.        /// </summary>        public static string DigitalFormViewerStyle => _digitalFormViewerStyle;        private const string _submitScript = @"var submitButtons = document.getElementsByClassName(""prs-web_utils-digital_form-submit"");var saveButtons = document.getElementsByClassName(""prs-web_utils-digital_form-save"");function getData(element){    if(element.classList.contains(""prs-web_utils-digital_form-double"")            || element.classList.contains(""prs-web_utils-digital_form-integer"")){        var res = element.valueAsNumber;        if(Number.isNaN(res)){            return null;        }        return res;    } else if(element.classList.contains(""prs-web_utils-digital_form-color"")){        return ""#FF"" + element.value.substring(1).toUpperCase();    } else if(element.classList.contains(""prs-web_utils-digital_form-multi_image"")){        let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];        let imgContDiv = imgCont.children[0];                let imgs = [];        for(let img of imgContDiv.children){            imgs.push(img.src.replace(/^data:(.*,)?/, ''));        }        return imgs;    } else if(element.classList.contains(""prs-web_utils-digital_form-notes"")){        let cont = element.getElementsByClassName(""prs-web_utils-digital_form-note_cont"")[0];        let retVal = [];        for(let note of cont.children){            retVal.push(note.value);        }        if(!(/\S/.test(retVal[retVal.length - 1]))){            retVal.pop();        }        return retVal;    } else if(element.classList.contains(""prs-web_utils-digital_form-time"")){        let hours = element.getElementsByClassName(""prs-web_utils-digital_form-time_hours"")[0];        let mins = element.getElementsByClassName(""prs-web_utils-digital_form-time_mins"")[0];        return hours.value + "":"" + mins.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-date"")            || element.classList.contains(""prs-web_utils-digital_form-datetime"")){        return element.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-pin"")){        let pinText = element.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];        return pinText.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-timestamp"")){        let dateText = element.getElementsByClassName(""prs-web_utils-digital_form-timestamp_date"")[0];        return dateText.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-text"")            || element.classList.contains(""prs-web_utils-digital_form-string"")            || element.classList.contains(""prs-web_utils-digital_form-password"")            || element.classList.contains(""prs-web_utils-digital_form-url"")            || element.classList.contains(""prs-web_utils-digital_form-code"")            || element.classList.contains(""prs-web_utils-digital_form-option"")){        return element.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-radio_cont"")){        var options = element.getElementsByTagName(""input"");        for(let option of options){            if(option.checked){                return option.value;            }        }        return null;    } else if(element.classList.contains(""prs-web_utils-digital_form-lookup"")){        return element.value;    } else if(element.classList.contains(""prs-web_utils-digital_form-embedded_image"")            || element.classList.contains(""prs-web_utils-digital_form-signature"")){        var imgs = element.getElementsByClassName(""prs-web_utils-digital_form-img"");        if(imgs.length == 0){            return null;        } else {            var img = imgs[0];            var src = img.getAttribute(""src"");            var match = /data:[^;]*;base64,/d.exec(src);            if(match){                return src.substring(match.indices[0][1]);            }            return null;        }    } else if(element.classList.contains(""prs-web_utils-digital_form-document"")){        return element.getAttribute(""data-id"");    } else if(element.classList.contains(""prs-web_utils-digital_form-boolean"")){        if(element.tagName == ""INPUT""){            return element.checked;        } else if(element.tagName == ""SELECT""){            if(element.selectedOptions.length > 0){                return element.selectedOptions[0].hasAttribute(""data-true"");            }            return null;        } else if(element.tagName == ""DIV""){            for(var element of element.getElementsByTagName(""input"")){                if(element.checked){                    return element.hasAttribute(""data-true"");                }            }            return null;        }    } else if(element.classList.contains(""prs-web_utils-digital_form-add_task"")){        var input = element.getElementsByTagName(""input"")[0];        return input.value;    }}function validate(element){    let valid = true;    let err = """";    if(element.classList.contains(""prs-web_utils-digital_form-embedded_image"")            || element.classList.contains(""prs-web_utils-digital_form-signature"")){        let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];        let imgs = imgCont.getElementsByTagName(""img"");        if(imgs.length == 0){            valid = false;            err = ""Please provide an image"";        }    } else if(element.classList.contains(""prs-web_utils-digital_form-multi_image"")){        let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];        let imgContDiv = imgCont.children[0];        if(imgContDiv.children.length == 0){            valid = false;            err = ""Please provide at least one image"";        }    }    return [valid, err];}function doValidationCheck(form){    var validations = form.getElementsByClassName(""prs-web_utils-digital_form-validate"");    for(let validation of validations){        validation.remove();    }    if(!form.reportValidity()){ return false; }        let valid = true;    var elements = form.getElementsByClassName(""prs-web_utils-digital_form-field"");    for(var element of elements){        if(element.hasAttribute(""required"")){            var [elValid, err] = validate(element);            valid = elValid && valid;            if(!elValid){                element.scrollIntoView();                let validation = document.createElement(""div"");                validation.classList.add(""prs-web_utils-digital_form-validate"");                validation.textContent = err;                element.style.position = ""relative"";                element.appendChild(validation);                setTimeout(() => {validation.style.opacity = 0;}, 5000);            }        }    }    return valid;}function submitForm(form, save, validate = true){    if(!validate || doValidationCheck(form)){        var formData = {};            var elements = form.getElementsByClassName(""prs-web_utils-digital_form-field"");        for(var element of elements){            formData[element.getAttribute(""name"")] = getData(element);            if(element.classList.contains(""prs-web_utils-digital_form-lookup"")){                for(var option of element.getElementsByTagName(""option"")){                    if(option.value == element.value){                        formData[element.getAttribute(""name"") + ""$ID""] = option.getAttribute(""data-id"");                        break;                    }                }            }        }        let eventName = save ? ""prs-web_utils-df_save"" : ""prs-web_utils-df_submit"";        const submitFormEvent = new CustomEvent(eventName, { detail: { formData: formData } });        form.dispatchEvent(submitFormEvent);    }}for(var btn of submitButtons){    btn.addEventListener(""click"", function(e) {        var form = e.target.parentNode.parentNode;        submitForm(form, false, true);    });}for(var btn of saveButtons){    btn.addEventListener(""click"", function(e) {        var form = e.target.parentNode.parentNode;        submitForm(form, true, false);    });}";        private static string GetDigitalFormViewerScript()        {            return @"<script>function checkDisabled(target, fnc){    target.addEventListener(""click"", function(e){        return !target.hasAttribute(""disabled"") ? fnc(e) : null;    });}var integerFields = document.getElementsByClassName(""prs-web_utils-digital_form-integer"");function integerValidator(e){    e.target.value = Math.floor(e.target.value);}for(var field of integerFields){    field.addEventListener(""change"", integerValidator);}function changeSelect(e){    var parent = e.target.parentNode;    var label = parent.getElementsByTagName(""div"")[0];    label.textContent = e.target.value;}var optionCont = document.getElementsByClassName(""prs-web_utils-digital_form-option_cont"");for(var option of optionCont){    var select = option.getElementsByTagName(""select"")[0];    select.setAttribute(""style"", ""position: absolute;top:0;left:0;right:0;bottom:0;"");    select.addEventListener(""change"", changeSelect);}function saveDocument(filename, data, onResponse){    var xhr = new XMLHttpRequest();    xhr.open(""POST"", ""/save/Document"");    xhr.setRequestHeader(""Content-Type"", ""application/json"");    xhr.responseType = ""json"";    xhr.onreadystatechange = function(){        if(xhr.readyState === 4){            onResponse(xhr.response);        }    }    xhr.send(JSON.stringify({        ""FileName"": filename,        ""Data"": data    }));}function selectDocument(e){    var editor = e.target.parentNode;    var filename = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];    var accept = filename.getAttribute(""accept"");        var input = document.createElement(""input"");    input.type = ""file"";    input.setAttribute(""accept"", accept);    input.onchange = e => {        var file = e.target.files[0];        var reader = new FileReader();        reader.onloadend = (re) => {            saveDocument(file.name, reader.result.replace(/^data:(.*,)?/, ''), function(data){                filename.value = data[""FileName""];                editor.setAttribute(""data-id"", data[""ID""]);            });        };        reader.readAsDataURL(file);    };    input.click();}function clearDocument(e){    var editor = e.target.parentNode;    editor.removeAttribute(""data-id"");    var filenameEl = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];    filenameEl.value = """";}function viewDocument(e){    var editor = e.target.parentNode;    if(editor.hasAttribute(""data-id"")){        var id = editor.getAttribute(""data-id"");        var filenameEl = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];        console.log(filenameEl.value);        const viewDocEvent = new CustomEvent(""prs-web_utils-df_view_doc"", { detail: { id: id, filename: filenameEl.value } });        editor.dispatchEvent(viewDocEvent);    }}var docSelectBtns = document.getElementsByClassName(""prs-web_utils-digital_form-select_doc"");var docClearBtns = document.getElementsByClassName(""prs-web_utils-digital_form-clear_doc"");var docViewBtns = document.getElementsByClassName(""prs-web_utils-digital_form-view_doc"");for(var docSelect of docSelectBtns){    checkDisabled(docSelect, selectDocument);}for(var docClear of docClearBtns){    checkDisabled(docClear, clearDocument);}for(var docView of docViewBtns){    checkDisabled(docView, viewDocument);}function createPIN(e){    var editor = e.target.parentNode;       var length = parseInt(editor.getAttribute(""data-length""), 10);    var pinEl = editor.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];    pinEl.value = """";    for(let i = 0; i < length; i++){        pinEl.value += Math.floor(Math.random() * 10).toString();    }}function clearPIN(e){    var editor = e.target.parentNode;    var pinEl = editor.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];    pinEl.value = """";}var pinCreateBtns = document.getElementsByClassName(""prs-web_utils-digital_form-create_pin"");var pinClearBtns = document.getElementsByClassName(""prs-web_utils-digital_form-clear_pin"");for(var pinCreate of pinCreateBtns){    checkDisabled(pinCreate, createPIN);}for(var pinClear of pinClearBtns){    checkDisabled(pinClear, clearPIN);}function toggleStamp(e){    var editor = e.target.parentNode;    var stampEl = editor.getElementsByClassName(""prs-web_utils-digital_form-timestamp_date"")[0];    if(/\S/.test(stampEl.value)){        stampEl.value = """";        e.target.textContent = ""Set"";    } else {        var dateString = (new Date()).toISOString();        stampEl.value = dateString.substring(0, dateString.length - 1);        e.target.textContent = ""Clear"";    }}var pinToggleBtns = document.getElementsByClassName(""prs-web_utils-digital_form-toggle_stamp"");for(var pinToggle of pinToggleBtns){    checkDisabled(pinToggle, toggleStamp);}var embedImageFields = document.getElementsByClassName(""prs-web_utils-digital_form-embedded_image"");var signatureFields = document.getElementsByClassName(""prs-web_utils-digital_form-signature"");function fileSelector(e){    var selectedFile = e.target.files[0];    var target = e.target;    var parent = e.target.parentNode;    let clearButton = parent.nextSibling;        var img = document.createElement(""img"");    img.classList.add(""prs-web_utils-digital_form-img"");    const reader = new FileReader();    reader.onload = (e) => {        img.src = e.target.result;        clearButton.removeAttribute(""disabled"");    }    reader.readAsDataURL(selectedFile);        parent.replaceChildren(img, target);}function fileButton(field, fileElement){    field.addEventListener(""click"", (e) => {        fileElement.click();    });}function clearEmbeddedImage(e){    let imgField = e.target.parentNode;    let imgCont = imgField.children[0];    let span = document.createElement(""span"");    span.textContent = ""Choose a file"";    imgCont.replaceChildren(span, imgCont.children[1]);    e.target.setAttribute(""disabled"", """");}var clearButtons = [];for(var field of embedImageFields){    var imgElement = field.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];    var fileElement = field.getElementsByTagName(""input"")[0];    fileButton(imgElement, fileElement);    fileElement.addEventListener(""change"", fileSelector);    clearButtons.push(field.getElementsByClassName(""prs-web_utils-digital_form-clear_img"")[0]);}for(var field of signatureFields){    var imgElement = field.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];    var fileElement = field.getElementsByTagName(""input"")[0];    fileButton(imgElement, fileElement);    fileElement.addEventListener(""change"", fileSelector);    clearButtons.push(field.getElementsByClassName(""prs-web_utils-digital_form-clear_img"")[0]);}for(let clearButton of clearButtons){    checkDisabled(clearButton, clearEmbeddedImage);}function enableMultiImageRem(multiImage, enabled = true){    if(!multiImage.hasAttribute(""disabled"")){        var btn = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_remove"")[0];        if(enabled){            btn.removeAttribute(""disabled"");        } else {            btn.setAttribute(""disabled"", """");        }    }}function multiImageClick(e){    var multiImage = e.target.parentNode.parentNode.parentNode;    for(let img of e.target.parentNode.children){        img.classList.remove(""selected"");    }    e.target.classList.add(""selected"");    enableMultiImageRem(multiImage);}var multiImages = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image"");for(let multiImg of multiImages){    var imgs = multiImg.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0].getElementsByTagName(""img"");    for(let img of imgs){        img.addEventListener(""click"", multiImageClick);    }}function multiImageRemove(e){    let multiImage = e.target.parentNode.parentNode;    let imgList = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0].children[0];    for(let img of imgList.children){        if(img.classList.contains(""selected"")){            imgList.removeChild(img);        }    }    enableMultiImageRem(multiImage, false);}function multiImageAdd(e){    let multiImage = e.target.parentNode.parentNode;    let imgCont = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];    let imgContDiv = imgCont.children[0];    var input = document.createElement(""input"");    input.type = ""file"";    input.setAttribute(""accept"", ""image/*"");    input.onchange = e => {        var file = e.target.files[0];        var reader = new FileReader();        reader.onloadend = (re) => {            var img = document.createElement(""img"");            img.src = reader.result;            img.addEventListener(""click"", multiImageClick);            imgContDiv.appendChild(img);        };        reader.readAsDataURL(file);    };    input.click();}var multiImageAddBtns = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image_add"");var multiImageRemBtns = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image_remove"");for(var multiImageBtn of multiImageAddBtns){    checkDisabled(multiImageBtn, multiImageAdd);}for(var multiImageBtn of multiImageRemBtns){    checkDisabled(multiImageBtn, multiImageRemove);}function addNote(e){    let notesCont = e.target.parentNode.getElementsByClassName(""prs-web_utils-digital_form-note_cont"")[0];    let noteChildren = notesCont.getElementsByClassName(""prs-web_utils-digital_form-note"");    let lastChild = noteChildren[noteChildren.length - 1];    let error = notesCont.getElementsByClassName(""prs-web_utils-digital_form-note_error"")[0];    if(/\S/.test(lastChild.value)){        lastChild.classList.remove(""active"");        lastChild.setAttribute(""disabled"", """");                    let newNotes = document.createElement(""textarea"");        newNotes.classList.add(""prs-web_utils-digital_form-note"");        newNotes.classList.add(""active"");        notesCont.insertBefore(newNotes, error);                    error.textContent = """";    } else {        error.textContent = ""Please enter a note!"";    }}var notesAddButtons = document.getElementsByClassName(""prs-web_utils-digital_form-note_add"");for(let noteAdd of notesAddButtons){    checkDisabled(noteAdd, addNote);}function addTask(e){    let field = e.target.parentNode;    let kanbanType = field.getAttribute(""data-tasktype"");    let kanban = " + WebUtils.CreateJSONEntity<Kanban>() + ";" +     WebUtils.JSONSetProperty<Kanban>("kanban", x => x.Type.ID, "kanbanType") + @";    let eventName = ""prs-web_utils-df_add_task"";    const addTaskEvent = new CustomEvent(eventName, { detail: {        kanban: kanban,        setTaskNumber: function(number){" +            WebUtils.JSONSetProperty<Kanban>("kanban", x => x.Number, "number") + @";            var input = field.getElementsByTagName(""input"")[0];            input.value = number.toString();            var btn = field.getElementsByClassName(""prs-button"")[0];            btn.setAttribute(""disabled"", """");            submitForm(field.closest("".prs-web_utils-digital_form""), true, false);        }    }});    form.dispatchEvent(addTaskEvent);}var addTaskFields = document.getElementsByClassName(""prs-web_utils-digital_form-add_task"");for(let addTaskField of addTaskFields){    let addTaskBtn = addTaskField.getElementsByClassName(""prs-button"")[0];    checkDisabled(addTaskBtn, addTask);}" + _submitScript + "</script>";        }        public static string DigitalFormViewerScript => GetDigitalFormViewerScript();        private static string BuildLookupField(            IEnumerable<Tuple<string, string?>> options,            string? selectedString,            string style,            string className,            bool readOnly,            bool required,            string name        )        {            var element = new StringBuilder(                string.Format(@"<div class=""prs-web_utils-digital_form-option_cont"" style=""{0}position: relative;font-size: 1.2em;"">", style));            var childOptions = new StringBuilder();            var display = selectedString ?? "";            foreach (var option in options)            {                var idString = option.Item2 != null ? $" data-id=\"{option.Item2}\"" : "";                childOptions.Append($"<option{(option.Item1 == selectedString ? " selected" : "")} value=\"{option.Item1}\"{idString}>{option.Item1}</option>");            }            element.AppendFormat(                @"<div class=""prs-web_utils-digital_form-option_fake"">{0}</div><select name=""{1}"" class=""{2}""{3}{4}><option></option>",                display,                name,                className,                readOnly ? " disabled" : "",                required ? " required" : ""            ).Append(childOptions).Append("</select></div>");            return element.ToString();        }        private static string BuildRadioButtons(            IEnumerable<string> options,            string? selectedString,            string style,            string className,            bool readOnly,            bool required,            string name        )        {            var element = new StringBuilder(                string.Format(@"<div name=""{0}"" class=""prs-web_utils-digital_form-radio_cont {1}"" style=""{2}position: relative;font-size: 1.2em;"">", name, className, style));            var childOptions = new StringBuilder();            var radioOptions = string.Format(                @"name=""{0}"" type=""radio""{1}{2}",                name,                required ? " required" : "",                readOnly ? " disabled" : ""            );            foreach (var option in options)            {                var selected = option == selectedString;                var valueStr = $" value=\"{option}\"";                var optionID = Guid.NewGuid();                childOptions.Append($"<input id=\"{optionID}\"{valueStr} {radioOptions} {(selected ? " checked" : "")}/>"                    + $"<label for=\"{optionID}\">{option}</label>");            }            element.Append(childOptions).Append("</div>");            return element.ToString();        }        public static string BuildDigitalFormViewer(            DFLayout layout,            Dictionary<string, object>? data,            bool readOnly,            string? id = null, string[]? classList = null,            string submitLabel = "Submit Form", string? saveLabel = "Save Form"        )        {            StringBuilder stringBuilder = new();            stringBuilder.Append("<form ");            if (id != null) stringBuilder.Append("id=\"" + id + "\" ");            stringBuilder.Append(@"class=""prs-web_utils-digital_form");            if (classList != null)                foreach (var className in classList)                    stringBuilder.Append(" " + className);            stringBuilder.Append(@""" style=""grid-template-columns:");            foreach (var width in layout.ColumnWidths)                if (width == "Auto")                {                    stringBuilder.Append("auto ");                }                else if (width == "*")                {                    stringBuilder.Append("1fr ");                }                else                {                    stringBuilder.Append(width);                    stringBuilder.Append("px ");                }            stringBuilder.Append(@";grid-template-rows:");            foreach (var height in layout.RowHeights)                if (height == "Auto")                {                    stringBuilder.Append("auto ");                }                else if (height == "*")                {                    stringBuilder.Append("1fr ");                }                else                {                    stringBuilder.Append(height);                    stringBuilder.Append("px ");                }            stringBuilder.Append(@"min-content;"">");            var lastRowNum = layout.RowHeights.Count + 1;            foreach (var element in layout.Elements)            {                var style = string.Format("grid-column: {0} / span {1};grid-row: {2} / span {3};", element.Column, element.ColumnSpan, element.Row,                    element.RowSpan);                if (element is DFLayoutField fieldElement)                {                    var required = fieldElement.GetPropertyValue<bool>("Required");                    var fieldReadOnly = readOnly || fieldElement.GetPropertyValue<bool>("Secure");                    object? fieldData = null;                    data?.TryGetValue(fieldElement.Name, out fieldData);                    var tagType = "input";                    var classes = "";                    string? value = null;                    var properties = "";                    string? children = null;                    // TODO: DocumentField, MultiSignaturePad                    if(element is DFLayoutAddTaskField addTaskField)                    {                        var kanbanType = addTaskField.Properties.TaskType;                        var kanbanNumber = (int?)fieldData;                        tagType = "div";                        classes = "prs-web_utils-digital_form-add_task";                        properties = $"data-tasktype=\"{kanbanType.ID}\"";                        children = $"<input type=\"number\" step=\"1\" disabled{(kanbanNumber != null ? $" value=\"{kanbanNumber}\"" : "")}/>" +                            $"<div class=\"prs-button\"{(fieldReadOnly || kanbanNumber != null ? " disabled" : "")}>Create Task</div>";                    }                    else if (element is DFLayoutBooleanField)                    {                        var fieldType = fieldElement.GetPropertyValue<DesignBooleanFieldType>("Type");                        var trueValue = fieldElement.GetPropertyValue<string>("TrueValue");                        var falseValue = fieldElement.GetPropertyValue<string>("FalseValue");                        bool checkTrue = (bool?)fieldData == true;                        bool checkFalse = (bool?)fieldData == false;                        if (fieldType == DesignBooleanFieldType.Checkbox)                        {                            tagType = "div";                            classes = "prs-web_utils-digital_form-boolean";                            var options = string.Format(                                @"name=""{0}"" type=""radio""{1}{2}",                                fieldElement.Name,                                required ? " required" : "",                                fieldReadOnly ? " disabled" : ""                            );                            var disabled = fieldReadOnly ? " disabled" : "";                            var idTrue = Guid.NewGuid();                            var idFalse = Guid.NewGuid();                            children =                                $"<input value=\"{trueValue}\" id=\"{idTrue}\" {(checkTrue ? " checked" : "")} data-true {options}/>" +                                    $"<label for=\"{idTrue}\">{trueValue}</label>" +                                $"<input value=\"{falseValue}\" id=\"{idFalse}\" \"{(checkFalse ? " checked" : "")} data-false {options}/>" +                                    $"<label for=\"{idFalse}\">{falseValue}</label>";                        }                        else if (fieldType == DesignBooleanFieldType.ComboBox)                        {                            classes = "prs-web_utils-digital_form-boolean";                            tagType = "select";                            children = string.Format(                                @"<option{0} data-true>{1}</option><option{2} data-false>{3}</option>",                                checkTrue ? " selected" : "",                                trueValue,                                checkFalse ? " selected" : "",                                falseValue                            );                        }                        else if (fieldType == DesignBooleanFieldType.Buttons)                        {                            classes = "prs-web_utils-digital_form-boolean";                            tagType = "div";                            var options = string.Format(                                @"name=""{0}"" type=""radio""{1}{2}",                                fieldElement.Name,                                required ? " required" : "",                                fieldReadOnly ? " disabled" : ""                            );                            children = string.Format(                                @"<div class=""prs-web_utils-digital_form-button""><input {0} value=""{1}""{3} data-true></input><span>{1}</span></div>" +                                @"<div class=""prs-web_utils-digital_form-button""><input {0} value=""{2}""{4} data-false></input><span>{2}</span></div>",                                options,                                trueValue,                                falseValue,                                checkTrue ? " checked" : "",                                checkFalse ? " checked" : ""                            );                        }                    }                    else if (element is DFLayoutCodeField)                    {                        properties = @"type=""text"" pattern=""[^a-z]+""";                        classes = "prs-web_utils-digital_form-code";                        value = fieldData?.ToString() ?? "";                    }                    else if (element is DFLayoutColorField)                    {                        properties = @"type=""color""";                        classes = "prs-web_utils-digital_form-color";                        if (fieldData != null)                            try                            {                                var color =                                    (Color)ColorConverter.ConvertFromString((string)fieldData);                                value = string.Format("#{0:x}{1:x}{2:x}", color.R, color.G, color.B);                            }                            catch (FormatException)                            {                            }                    }                    else if (element is DFLayoutDateField)                    {                        properties = @"type=""date""";                        classes = "prs-web_utils-digital_form-date";                        value = ((DateTime?)fieldData)?.ToString("yyyy-MM-dd") ?? "";                    }                    else if (element is DFLayoutDateTimeField)                    {                        properties = @"type=""datetime-local""";                        classes = "prs-web_utils-digital_form-datetime";                        value = fieldData != null ? string.Format("{0:yyyy-MM-dd}T{0:HH:mm}", fieldData) : "";                    }                    else if (element is DFLayoutDoubleField)                    {                        properties = @"type=""number""";                        classes = "prs-web_utils-digital_form-double";                        value = fieldData?.ToString() ?? "";                    }                    else if (element is DFLayoutIntegerField)                    {                        properties = @"type=""number"" step=""1""";                        classes = "prs-web_utils-digital_form-integer";                        value = fieldData?.ToString() ?? "";                    }                    else if (element is DFLayoutLookupField)                    {                        var type = CoreUtils.GetEntityOrNull((element as DFLayoutLookupField).Properties.LookupType);                        if(type is not null)                        {                            var table = ClientFactory.CreateClient(type).Query(                                LookupFactory.DefineFilter(type),                                LookupFactory.DefineColumns(type),                                LookupFactory.DefineSort(type)                            );                            data.TryGetValue(fieldElement.Name + "$ID", out var fieldID);                            data.TryGetValue(fieldElement.Name, out var fieldFormat);                            var options = table.Rows.Select(x => new Tuple<string, string?>(                                LookupFactory.FormatLookup(type, x.ToDictionary(new[] { "ID" }), Array.Empty<string>()),                                x["ID"].ToString())).ToList();                            fieldFormat ??= options.Where(x => x.Item2 == fieldID?.ToString()).FirstOrDefault()?.Item1;                            tagType = null;                            stringBuilder.Append(BuildLookupField(                                options,                                fieldFormat?.ToString(),                                style,                                "prs-web_utils-digital_form-lookup prs-web_utils-digital_form-field",                                fieldReadOnly,                                required,                                fieldElement.Name                            ));                        }                    }                    else if (element is DFLayoutMultiImage)                    {                        tagType = "div";                        classes = "prs-web_utils-digital_form-multi_image";                        var childBuild = new StringBuilder();                        childBuild.Append(@"<div class=""prs-web_utils-digital_form-multi_image_cont""><div>");                        if (fieldData is List<byte[]> images)                            foreach (var image in images)                            {                                var imgStr = Convert.ToBase64String(image);                                childBuild.AppendFormat(@"<img src=""data:;base64,{0}""/>", imgStr);                            }                        childBuild.AppendFormat(@"</div></div><div class=""prs-web_utils-digital_form-multi_image_btns"">" +                                                @"<div class=""prs-button prs-web_utils-digital_form-multi_image_add""{0}>Add Image</div>" +                                                @"<div class=""prs-button prs-web_utils-digital_form-multi_image_remove"" disabled>Remove Image</div>" +                                                @"</div>",                            fieldReadOnly ? " disabled" : ""                        );                        children = childBuild.ToString();                    }                    else if (element is DFLayoutNotesField)                    {                        tagType = "div";                        classes = "prs-web_utils-digital_form-notes";                        var notes = (string[]?)fieldData ?? Array.Empty<string>();                        var childrenBuilder = new StringBuilder(@"<div class=""prs-web_utils-digital_form-note_cont"">");                        foreach (var note in notes)                            childrenBuilder.AppendFormat(@"<textarea class=""prs-web_utils-digital_form-note"" disabled>{0}</textarea>", note);                        var options = fieldReadOnly ? "disabled" : "";                        childrenBuilder.Append(                            @"<textarea class=""prs-web_utils-digital_form-note active""></textarea><div class=""prs-web_utils-digital_form-note_error""></div></div>"                            + $"<div class=\"prs-web_utils-digital_form-note_add prs-button\" {options}>+</div>");                        children = childrenBuilder.ToString();                    }                    else if (element is DFLayoutOptionField optionField)                    {                        var optionType = optionField.Properties.OptionType;                        tagType = null;                        if (optionType == DFLayoutOptionType.Radio)                        {                            stringBuilder.Append(BuildRadioButtons(                                optionField.Properties.Options.Split(','),                                fieldData?.ToString(),                                style,                                "prs-web_utils-digital_form-field",                                fieldReadOnly,                                required,                                fieldElement.Name                            ));                        }                        else                        {                            stringBuilder.Append(BuildLookupField(                                optionField.Properties.Options.Split(',').Select(x => new Tuple<string, string?>(x, null)),                                fieldData?.ToString(),                                style,                                "prs-web_utils-digital_form-option prs-web_utils-digital_form-field",                                fieldReadOnly,                                required,                                fieldElement.Name                            ));                        }                    }                    else if (element is DFLayoutPasswordField)                    {                        properties = @"type=""password""";                        classes = "prs-web_utils-digital_form-password";                        value = fieldData?.ToString() ?? "";                    }                    else if (element is DFLayoutPINField)                    {                        tagType = "div";                        classes = "prs-web_utils-digital_form-pin";                        var pinLength = fieldElement.GetPropertyValue<int>("Length");                        properties += $" data-length=\"{pinLength}\"";                        var options = fieldReadOnly ? "disabled" : "";                        children +=                            $"<input type=\"text\" disabled value=\"{fieldData?.ToString() ?? ""}\" class=\"prs-web_utils-digital_form-pin_text\"{(required ? " required" : "")}/>"                            + $"<div class=\"prs-button prs-web_utils-digital_form-create_pin\" {options}>Create</div>"                            + $"<div class=\"prs-button prs-web_utils-digital_form-clear_pin\" {options}>Clear</div>";                    }                    else if (element is DFLayoutStringField)                    {                        properties = @"type=""text""";                        classes = "prs-web_utils-digital_form-string";                        value = fieldData?.ToString() ?? "";                    }                    else if (element is DFLayoutTextField)                    {                        tagType = "textarea";                        classes = "prs-web_utils-digital_form-text";                        style += "resize:none;";                        children += fieldData?.ToString();                    }                    else if (element is DFLayoutTimeField)                    {                        tagType = "div";                        classes = "prs-web_utils-digital_form-time";                        var timeSpan = (TimeSpan?)fieldData;                        var hours = Math.Truncate(timeSpan?.TotalHours ?? 0.0);                        var mins = timeSpan?.Minutes ?? 0.0;                        var requiredStr = required ? " required" : "";                        children = $"<input class=\"prs-web_utils-digital_form-time_hours\" type=\"number\" min=\"0\" step=\"1\" value=\"{hours}\"{requiredStr}/>" +                            @"<span>:</span>"                            + $"<input class=\"prs-web_utils-digital_form-time_mins\" type=\"number\" min=\"0\" step=\"1\" max=\"60\" value=\"{mins}\"{requiredStr}/>";                    }                    else if (element is DFLayoutTimeStampField)                    {                        tagType = "div";                        classes = "prs-web_utils-digital_form-timestamp";                        var exists = fieldData != null && (DateTime)fieldData != DateTime.MinValue;                        var valueStr = exists ? string.Format("{0:yyyy-MM-dd}T{0:HH:mm}", fieldData) : "";                        var btnStr = exists ? "Clear" : "Set";                        var disabled = fieldReadOnly ? " disabled" : "";                        children +=                            $"<input type=\"datetime-local\" disabled value=\"{valueStr}\" class=\"prs-web_utils-digital_form-timestamp_date\"{(required ? " required" : "")}/>"                            + $"<div class=\"prs-button prs-web_utils-digital_form-toggle_stamp\"{disabled}>{btnStr}</div>";                    }                    else if (element is DFLayoutURLField)                    {                        properties = @"type=""url""";                        classes = "prs-web_utils-digital_form-url";                        value = fieldData?.ToString();                    }                    else if (element is DFLayoutEmbeddedImage || element is DFLayoutSignaturePad)                    {                        // TODO: Perform validation for images                        tagType = "div";                        classes = element is DFLayoutEmbeddedImage                            ? "prs-web_utils-digital_form-embedded_image"                            : "prs-web_utils-digital_form-signature";                        var content = "";                        var hasContent = fieldData != null && ((byte[])fieldData).Length > 0;                        if (hasContent)                        {                            content = string.Format(                                @"<img class=""prs-web_utils-digital_form-img"" src=""data:;base64,{0}"">",                                Convert.ToBase64String((byte[])fieldData)                            );                        }                        else                        {                            if (fieldReadOnly)                                content = "<span>No image</span>";                            else                                content = "<span>Choose a file</span>";                        }                        children = String.Format(@"<div class=""prs-button prs-web_utils-digital_form-img_cont""{1}>{0}<input type=""file"" accept=""image/*"" style=""display: none"" {1}></div><div class=""prs-button prs-web_utils-digital_form-clear_img""{2}>Clear Image</div>",                            content,                            fieldReadOnly ? " disabled" : "",                            (hasContent && !fieldReadOnly) ? "" : " disabled"                        );                    }                                        if (tagType != null)                        stringBuilder.Append(string.Format(                            @"<{0} style=""{1}"" class=""{2} prs-web_utils-digital_form-field""{3}{4}{5} name=""{6}"" {7}>{8}</{0}>",                            tagType,                            style,                            classes,                            required ? " required" : "",                            fieldReadOnly ? " disabled" : "",                            value != null ? " value=\"" + value + "\"" : "",                            fieldElement.Name,                            properties,                            children ?? ""                        ));                }                else if (element is DFLayoutLabel)                {                    stringBuilder.Append(string.Format(@"<label class=""prs-web_utils-digital_form-label"" style=""{0}"">{1}</label>", style,                        (element as DFLayoutLabel).Caption));                }                else if (element is DFLayoutImage)                {                    stringBuilder.Append(string.Format(@"<img src=""/document?id={0}"" class=""prs-web_utils-digital_form-image"" style=""{1}"">",                        (element as DFLayoutImage).Image.ID, style));                }            }            if (!readOnly)            {                stringBuilder.Append($"<div class=\"prs-web_utils-digital_form-save_cont\" style=\"grid-column: 1 / -1; grid-row: {lastRowNum}\">");                if (saveLabel != null) stringBuilder.Append($"<div class=\"prs-button prs-web_utils-digital_form-save\">{saveLabel}</div>");                stringBuilder.Append($"<div class=\"prs-button prs-web_utils-digital_form-submit\">{submitLabel}</div>");                stringBuilder.Append(@"</div>");            }            stringBuilder.Append("</form>");            stringBuilder.Append(DigitalFormViewerScript);            return stringBuilder.ToString();        }        /// <summary>        ///     Builds a layout for viewing and optionally editing digital forms.        /// </summary>        /// <note>        ///     <para>        ///         In addition to outputting an HTML representation of the form, a script tag is outputted, which provides form        ///         submission functionality and some extra layouting functionality.        ///         The submit form button has the class of "prs-web_utils-digital_form-submit", and when clicked calls an event        ///         named "prs-web_utils-df_submit" on the root form tag itself.        ///         This event is a JavaScript CustomEvent object with detail.formData set to a JSON object containing data which        ///         can be sent directly to www.domain.com/form_submission/XXXXForm?id=ID, allowing for the form to be submitted.        ///     </para>        /// </note>        /// <param name="form">The instance of the form</param>        /// <param name="entity">The entity associated with the form</param>        /// <param name="readOnly">If set to true, causes all elements to be disabled and the Submit Form button to be omitted</param>        /// <param name="id">An optional HTML id to add to root <div> of the generated item.</param>        /// <param name="classList">An optional list of HTML classes to add to root <div> of the generated item.</param>        /// <returns>A string containing raw HTML markup</returns>        public static string BuildDigitalFormViewer(ICoreDigitalFormInstance form, Entity entity, bool readOnly, string? id = null,            string[]? classList = null)        {            var formLayoutTable = new Client<DigitalFormLayout>().Query(                new Filter<DigitalFormLayout>(x => x.Form.ID).IsEqualTo(form.Form.ID),                Columns.None<DigitalFormLayout>().Add(x => x.Type).Add(x => x.Layout)            );            var variables = new Client<DigitalFormVariable>().Load(new Filter<DigitalFormVariable>(x => x.Form.ID).IsEqualTo(form.Form.ID));            var digitalFormLayout =                (formLayoutTable.Rows.FirstOrDefault(x => (DFLayoutType)x["Type"] == DFLayoutType.Mobile)                 ?? formLayoutTable.Rows.FirstOrDefault(x => (DFLayoutType)x["Type"] == DFLayoutType.Desktop))?.ToObject<DigitalFormLayout>();            var layout = digitalFormLayout != null                ? DFLayout.FromLayoutString(digitalFormLayout.Layout)                : DFLayout.GenerateAutoMobileLayout(variables);            layout.LoadVariables(variables);            var data = DigitalForm.ParseFormData(form.FormData, variables, entity);            return BuildDigitalFormViewer(layout, data, readOnly, id, classList);        }        #endregion    }    public abstract class WebTemplateBase<T> : TemplateBase<T>    {        public IEncodedString JSON<U>() where U : Entity, new()        {            return new RawString(WebUtils.CreateJSONEntity<U>());        }        public IEncodedString JSON<U>(U entity) where U : Entity        {            return new RawString(WebUtils.EntityToJSON(entity));        }        public IEncodedString Get<U>(string entity, Expression<Func<U, object>> expression) where U : Entity        {            return new RawString(WebUtils.JSONGetProperty(entity, expression));        }        public IEncodedString Set<U>(string entity, Expression<Func<U, object>> expression, string value) where U : Entity        {            return new RawString(WebUtils.JSONSetProperty(entity, expression, value));        }    }}
 |