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 { /// /// Provides utility functions for writing Razor template pages /// public class WebUtils { #region Entity Editor /// /// Builds an editor for a given entity. /// /// To get reasonable formatting, import in a <style> tag /// The type of /// The entity which contains the data to be pre-inserted into the editor /// An optional HTML id to add to root <div> of the generated item. /// An optional list of HTML classes to add to root <div> of the generated item. /// A string containing raw HTML markup public static string BuildEntityEditor(T entity, string? id = null, string[]? classList = null) where T : Entity { var builder = new StringBuilder(); builder.AppendFormat( @"
", classList != null ? " " + string.Join(" ", classList) : "", id != null ? " id=\"" + id + "\"" : "" ); var layout = DFLayout.GenerateEntityLayout(); var data = WebHandler.SerializeEntityForEditing(typeof(T), entity); var markup = BuildDigitalFormViewer(layout, data, false, submitLabel: "Save", saveLabel: null); builder.Append(markup).Append("
"); return builder.ToString(); } #endregion #region CoreTable Viewer /// /// 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. /// public enum EntityTableColumns { ALL, VISIBLE, LOOKUP, REQUIRED } private static List GetColumnsForTable(EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity { if (showType == EntityTableColumns.LOOKUP) return LookupFactory.DefineColumns(typeof(T)).ColumnNames().ToList(); var columns = new List(); var properties = CoreUtils.PropertyInfoList( typeof(T), x => x.GetCustomAttribute() == null && x.GetCustomAttribute() == 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; } /// /// Returns a string containing an HTML table representing the data present in a CoreTable. /// /// The type of the entity that the table represents. /// The table to be converted to HTML /// A flag to determine which columns to show. /// The HTML string public static string BuildHTMLTableFromCoreTable(CoreTable table, EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity { var columns = GetColumnsForTable(showType); var html = new List(); html.Add(""); foreach (var column in columns) { html.Add(""); } html.Add(""); foreach (var row in table.Rows) { html.Add(""); foreach (var column in columns) { html.Add(""); } html.Add(""); } html.Add("
"); html.Add(typeof(T).Name); html.Add("
"); html.Add(column); html.Add("
"); html.Add(row.Get(column)?.ToString() ?? "NULL"); html.Add("
"); html.Add(""); return string.Concat(html); } /// /// Returns a string containing an HTML table representing the data present in a CoreTable. /// /// The type of the entity that the table represents /// The table to be converted to HTML /// A flag to determine which columns to show. /// The HTML string 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("
"); } 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 RenderPDFToImages(byte[] pdfData, ImageEncoding encoding = ImageEncoding.JPEG) { var rendered = new List(); 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().Query( new Filter(x => x.ID) .InQuery(new SubQuery( new Filter(x => x.Code).IsEqualTo(code), new Column(x => x.Document.ID))), new Columns(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().Query( new Filter(x => x.ID) .InQuery(new SubQuery( new Filter(x => x.Code).IsEqualTo(code), new Column(x => x.Document.ID))), new Columns(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(IDataModel model) where T : ISecurityDescriptor, new() { var userTable = model.GetTable(); if (userTable.Rows.Count == 0) return false; var user = userTable.Rows[0].ToObject(); return Security.IsAllowed(user.ID, user.SecurityGroup.ID); } public static bool IsLoggedIn(IDataModel model) { var userTable = model.GetTable(); return userTable.Rows.Count > 0; } public static User? GetUser(IDataModel model) { var userTable = model.GetTable(); if (userTable.Rows.Count == 0) return null; return userTable.Rows[0].ToObject(); } #endregion #region Directory Viewer public static string DirectoryViewerStyle { get; } = @""; 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 Directories = new(); public readonly List 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? fileFilter, Func? 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 = @""; private static void BuildDirectory(StringBuilder builder, DirectoryItem directory) { builder.Append(@"
"); builder.Append(directory.FileName); builder.Append(@"
"); builder.Append(@"
"); 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(@"
", file.FilePath)); builder.Append(@"
"); builder.Append(file.FileName); builder.Append(@"
"); } } builder.Append(@"
"); } /// /// /// The path to the folder in which to initialise the directory viewer /// A file filter which is compiled to a Regex. Regex.IsMatch is used to filter files /// /// A directory filter which is compiled to a Regex. Regex.IsMatch is used to filter /// directories /// /// The name to call the root folder. If null, defaults to the directory path. /// An HTML id attribute for the viewer /// An HTML class list for the viewer /// The depth of directories to search /// public static string BuildDirectoryViewer(string path, Func fileFilter, Func? directoryFilter = null, string? alias = null, string? id = null, string[]? classList = null, int depth = 2) { var stringBuilder = new StringBuilder(); stringBuilder.Append(@"
"); var structure = GetDirectoryStructure(path, fileFilter, directoryFilter, alias ?? path, depth); if (structure != null) BuildDirectory(stringBuilder, structure); stringBuilder.Append(@"
"); 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 SerializeEntity(Type entityType, Entity entity) { var data = new Dictionary(); foreach (var property in DatabaseSchema.Properties(entityType)) { data[property.Name] = CoreUtils.GetPropertyValue(entity, property.Name); } return data; } /// /// Generates a JSON object for javascript /// /// /// /// public static string EntityToJSON(T entity) where T : Entity { var serializedEntity = SerializeEntity(typeof(T), entity); var serializedJSON = Serialization.Serialize(serializedEntity); return serializedJSON; } public static string CreateJSONEntity() where T : Entity, new() { var serializedEntity = SerializeEntity(typeof(T), new T()); var serializedJSON = Serialization.Serialize(serializedEntity); return serializedJSON; } /// /// Generate code for setting a property of a JSON entity /// /// /// A Javascript expression that evaluates to the entity JSON /// /// public static string JSONSetProperty(string entityExpr, Expression> expression, string value) where T : Entity { var propName = CoreUtils.GetFullPropertyName(expression, "."); return $"({entityExpr})[\"{propName}\"]={value}"; } /// /// Generate code for getting a property of a JSON entity /// /// /// A Javascript expression that evaluates to the entity JSON /// /// public static string JSONGetProperty(string entityExpr, Expression> 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; /// /// A string which contains a stylesheet used for formatting digital forms. This is not necessary, but can be useful as /// a base.
/// Include within the <style> tag in your document head. ///
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 @""; } public static string DigitalFormViewerScript => GetDigitalFormViewerScript(); private static string BuildLookupField( IEnumerable> options, string? selectedString, string style, string className, bool readOnly, bool required, string name ) { var element = new StringBuilder( string.Format(@"
", 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.Item1}"); } element.AppendFormat( @"
{0}
"); return element.ToString(); } private static string BuildRadioButtons( IEnumerable options, string? selectedString, string style, string className, bool readOnly, bool required, string name ) { var element = new StringBuilder( string.Format(@"
", 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($"" + $""); } element.Append(childOptions).Append("
"); return element.ToString(); } public static string BuildDigitalFormViewer( DFLayout layout, Dictionary? data, bool readOnly, string? id = null, string[]? classList = null, string submitLabel = "Submit Form", string? saveLabel = "Save Form" ) { StringBuilder stringBuilder = new(); stringBuilder.Append("
"); 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("Required"); var fieldReadOnly = readOnly || fieldElement.GetPropertyValue("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 = $"" + $"
Create Task
"; } else if (element is DFLayoutBooleanField) { var fieldType = fieldElement.GetPropertyValue("Type"); var trueValue = fieldElement.GetPropertyValue("TrueValue"); var falseValue = fieldElement.GetPropertyValue("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 = $"" + $"" + $"" + $""; } else if (fieldType == DesignBooleanFieldType.ComboBox) { classes = "prs-web_utils-digital_form-boolean"; tagType = "select"; children = string.Format( @"{1}{3}", 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( @"
{1}
" + @"
{2}
", 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( LookupFactory.FormatLookup(type, x.ToDictionary(new[] { "ID" }), Array.Empty()), 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(@"
"); if (fieldData is List images) foreach (var image in images) { var imgStr = Convert.ToBase64String(image); childBuild.AppendFormat(@"", imgStr); } childBuild.AppendFormat(@"
" + @"
Add Image
" + @"
Remove Image
" + @"
", 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(); var childrenBuilder = new StringBuilder(@"
"); foreach (var note in notes) childrenBuilder.AppendFormat(@"", note); var options = fieldReadOnly ? "disabled" : ""; childrenBuilder.Append( @"
" + $"
+
"); 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(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("Length"); properties += $" data-length=\"{pinLength}\""; var options = fieldReadOnly ? "disabled" : ""; children += $"" + $"
Create
" + $"
Clear
"; } 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 = $"" + @":" + $""; } 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 += $"" + $"
{btnStr}
"; } 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( @"", Convert.ToBase64String((byte[])fieldData) ); } else { if (fieldReadOnly) content = "No image"; else content = "Choose a file"; } children = String.Format(@"
{0}
Clear Image
", 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}", tagType, style, classes, required ? " required" : "", fieldReadOnly ? " disabled" : "", value != null ? " value=\"" + value + "\"" : "", fieldElement.Name, properties, children ?? "" )); } else if (element is DFLayoutLabel) { stringBuilder.Append(string.Format(@"", style, (element as DFLayoutLabel).Caption)); } else if (element is DFLayoutImage) { stringBuilder.Append(string.Format(@"", (element as DFLayoutImage).Image.ID, style)); } } if (!readOnly) { stringBuilder.Append($"
"); if (saveLabel != null) stringBuilder.Append($"
{saveLabel}
"); stringBuilder.Append($"
{submitLabel}
"); stringBuilder.Append(@"
"); } stringBuilder.Append(""); stringBuilder.Append(DigitalFormViewerScript); return stringBuilder.ToString(); } /// /// Builds a layout for viewing and optionally editing digital forms. /// /// /// /// 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. /// /// /// The instance of the form /// The entity associated with the form /// If set to true, causes all elements to be disabled and the Submit Form button to be omitted /// An optional HTML id to add to root <div> of the generated item. /// An optional list of HTML classes to add to root <div> of the generated item. /// A string containing raw HTML markup public static string BuildDigitalFormViewer(IBaseDigitalFormInstance form, Entity entity, bool readOnly, string? id = null, string[]? classList = null) { var formLayoutTable = new Client().Query( new Filter(x => x.Form.ID).IsEqualTo(form.Form.ID), new Columns(x => x.Type).Add(x => x.Layout) ); var variables = new Client().Load(new Filter(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(); 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 : TemplateBase { public IEncodedString JSON() where U : Entity, new() { return new RawString(WebUtils.CreateJSONEntity()); } public IEncodedString JSON(U entity) where U : Entity { return new RawString(WebUtils.EntityToJSON(entity)); } public IEncodedString Get(string entity, Expression> expression) where U : Entity { return new RawString(WebUtils.JSONGetProperty(entity, expression)); } public IEncodedString Set(string entity, Expression> expression, string value) where U : Entity { return new RawString(WebUtils.JSONSetProperty(entity, expression, value)); } } }