WebUtils.cs 79 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Drawing.Imaging;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Linq.Expressions;
  8. using System.Reflection;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11. using Comal.Classes;
  12. using InABox.Clients;
  13. using InABox.Core;
  14. using PRSClasses;
  15. using PRSServer.Properties;
  16. using RazorEngine.Templating;
  17. using RazorEngine.Text;
  18. using Syncfusion.Pdf.Parsing;
  19. using Color = System.Windows.Media.Color;
  20. using ColorConverter = System.Windows.Media.ColorConverter;
  21. using Encoder = System.Drawing.Imaging.Encoder;
  22. namespace PRSServer
  23. {
  24. /// <summary>
  25. /// Provides utility functions for writing Razor template pages
  26. /// </summary>
  27. public class WebUtils
  28. {
  29. #region Entity Editor
  30. /// <summary>
  31. /// Builds an editor for a given entity.
  32. /// </summary>
  33. /// <remarks>To get reasonable formatting, import <see cref="WebUtils.DigitalFormViewerStyle" /> in a &lt;style&gt; tag</remarks>
  34. /// <typeparam name="T">The type of <paramref name="entity" /></typeparam>
  35. /// <param name="entity">The entity which contains the data to be pre-inserted into the editor</param>
  36. /// <param name="id">An optional HTML id to add to root &lt;div&gt; of the generated item.</param>
  37. /// <param name="classList">An optional list of HTML classes to add to root &lt;div&gt; of the generated item.</param>
  38. /// <returns>A string containing raw HTML markup</returns>
  39. public static string BuildEntityEditor<T>(T entity, string? id = null, string[]? classList = null) where T : Entity
  40. {
  41. var builder = new StringBuilder();
  42. builder.AppendFormat(
  43. @"<div class=""prs-web_utils-entity_editor{0}""{1}>",
  44. classList != null ? " " + string.Join(" ", classList) : "",
  45. id != null ? " id=\"" + id + "\"" : ""
  46. );
  47. var layout = DFLayout.GenerateEntityLayout<T>();
  48. var data = WebHandler.SerializeEntityForEditing(typeof(T), entity);
  49. var markup = BuildDigitalFormViewer(layout, data, false, submitLabel: "Save", saveLabel: null);
  50. builder.Append(markup).Append("</div>");
  51. return builder.ToString();
  52. }
  53. #endregion
  54. #region CoreTable Viewer
  55. /// <summary>
  56. /// Used for determining which columns to show in an HTML representation of a CoreTable.
  57. /// EntityTableColumns.ALL means to show every column
  58. /// EntityTableColumns.VISIBLE means only show columns which do not have their visiblity set to hidden
  59. /// EntityTableColumns.REQUIRED means only show columns which have their visibility set to visible.
  60. /// EntityTableColumns.LOOK_UP means to use the Lookup associated with the entity to generate the columns.
  61. /// </summary>
  62. public enum EntityTableColumns
  63. {
  64. ALL,
  65. VISIBLE,
  66. LOOKUP,
  67. REQUIRED
  68. }
  69. private static List<string> GetColumnsForTable<T>(EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity
  70. {
  71. if (showType == EntityTableColumns.LOOKUP) return LookupFactory.DefineColumns(typeof(T)).ColumnNames().ToList<string>();
  72. var columns = new List<string>();
  73. var properties = CoreUtils.PropertyInfoList(
  74. typeof(T),
  75. x => x.GetCustomAttribute<DoNotPersist>() == null &&
  76. x.GetCustomAttribute<DoNotSerialize>() == null &&
  77. x.PropertyType != typeof(UserProperties), true);
  78. switch (showType)
  79. {
  80. case EntityTableColumns.VISIBLE:
  81. foreach (var property in properties)
  82. {
  83. var editor = property.Value.GetEditor();
  84. if (editor != null && editor.Visible != Visible.Hidden) columns.Add(property.Key);
  85. }
  86. break;
  87. case EntityTableColumns.REQUIRED:
  88. foreach (var property in properties)
  89. {
  90. var editor = property.Value.GetEditor();
  91. if (editor != null && editor.Visible == Visible.Default) columns.Add(property.Key);
  92. }
  93. break;
  94. case EntityTableColumns.ALL:
  95. foreach (var property in properties.Keys) columns.Add(property);
  96. break;
  97. }
  98. return columns;
  99. }
  100. /// <summary>
  101. /// Returns a string containing an HTML table representing the data present in a CoreTable.
  102. /// </summary>
  103. /// <typeparam name="T">The type of the entity that the table represents.</typeparam>
  104. /// <param name="table">The table to be converted to HTML</param>
  105. /// <param name="showType">A flag to determine which columns to show.</param>
  106. /// <returns>The HTML string</returns>
  107. public static string BuildHTMLTableFromCoreTable<T>(CoreTable table, EntityTableColumns showType = EntityTableColumns.ALL) where T : Entity
  108. {
  109. var columns = GetColumnsForTable<T>(showType);
  110. var html = new List<string>();
  111. html.Add("<table class=\"prs-web_utils-entity_table entity-" + typeof(T).Name + "\"><caption>");
  112. html.Add(typeof(T).Name);
  113. html.Add("</caption><thead><tr>");
  114. foreach (var column in columns)
  115. {
  116. html.Add("<th>");
  117. html.Add(column);
  118. html.Add("</th>");
  119. }
  120. html.Add("</tr></thead>");
  121. foreach (var row in table.Rows)
  122. {
  123. html.Add("<tr>");
  124. foreach (var column in columns)
  125. {
  126. html.Add("<td>");
  127. html.Add(row.Get<object?>(column)?.ToString() ?? "NULL");
  128. html.Add("</td>");
  129. }
  130. html.Add("</tr>");
  131. }
  132. html.Add("</table>");
  133. html.Add("</table>");
  134. return string.Concat(html);
  135. }
  136. /// <summary>
  137. /// Returns a string containing an HTML table representing the data present in a CoreTable.
  138. /// </summary>
  139. /// <param name="entityType">The type of the entity that the table represents</param>
  140. /// <param name="table">The table to be converted to HTML</param>
  141. /// <param name="showType">A flag to determine which columns to show.</param>
  142. /// <returns>The HTML string</returns>
  143. public static string BuildHTMLTableFromCoreTable(Type entityType, CoreTable table, EntityTableColumns showType = EntityTableColumns.ALL)
  144. {
  145. var fnc = typeof(WebUtils).GetMethod(nameof(BuildHTMLTableFromCoreTable), 1, new[] { typeof(CoreTable), typeof(EntityTableColumns) })!
  146. .MakeGenericMethod(entityType);
  147. return (fnc.Invoke(null, new object[] { table, showType }) as string)!;
  148. }
  149. #endregion
  150. #region Extra Utilities
  151. public static string ConvertStringToPlainHTML(string text)
  152. {
  153. return text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;").ReplaceLineEndings("<br/>");
  154. }
  155. public enum ImageEncoding
  156. {
  157. JPEG
  158. }
  159. private static ImageCodecInfo? GetEncoder(ImageFormat format)
  160. {
  161. ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
  162. foreach (ImageCodecInfo codec in codecs)
  163. {
  164. if (codec.FormatID == format.Guid)
  165. {
  166. return codec;
  167. }
  168. }
  169. return null;
  170. }
  171. public static List<byte[]> RenderPDFToImages(byte[] pdfData, ImageEncoding encoding = ImageEncoding.JPEG)
  172. {
  173. var rendered = new List<byte[]>();
  174. PdfLoadedDocument loadeddoc = new PdfLoadedDocument(pdfData);
  175. Bitmap[] images = loadeddoc.ExportAsImage(0, loadeddoc.Pages.Count - 1);
  176. var jpgEncoder = GetEncoder(ImageFormat.Jpeg)!;
  177. var quality = Encoder.Quality;
  178. var encodeParams = new EncoderParameters(1);
  179. encodeParams.Param[0] = new EncoderParameter(quality, 100L);
  180. if (images != null)
  181. foreach (var image in images)
  182. {
  183. using(var data = new MemoryStream())
  184. {
  185. image.Save(data, jpgEncoder, encodeParams);
  186. rendered.Add(data.ToArray());
  187. }
  188. }
  189. return rendered;
  190. }
  191. private static readonly ImageConverter converter = new();
  192. public static string WebDocumentToDataURL(string code)
  193. {
  194. var document = new Client<Document>().Query(
  195. new Filter<Document>(x => x.ID)
  196. .InQuery(new SubQuery<WebDocument>(
  197. new Filter<WebDocument>(x => x.Code).IsEqualTo(code),
  198. new Column<WebDocument>(x => x.Document.ID))),
  199. new Columns<Document>(x => x.Data));
  200. var data = (byte[]?)document.Rows.FirstOrDefault()?["Data"];
  201. return "data:;base64," + (data != null ? Convert.ToBase64String(data) : "");
  202. }
  203. public static Guid? WebDocumentID(string code)
  204. {
  205. var document = new Client<Document>().Query(
  206. new Filter<Document>(x => x.ID)
  207. .InQuery(new SubQuery<WebDocument>(
  208. new Filter<WebDocument>(x => x.Code).IsEqualTo(code),
  209. new Column<WebDocument>(x => x.Document.ID))),
  210. new Columns<Document>(x => x.Data));
  211. return (Guid?)document.Rows.FirstOrDefault()?["ID"];
  212. }
  213. public static string ResourceToDataURL(Bitmap resource)
  214. {
  215. return "data:;base64," + Convert.ToBase64String((byte[])converter.ConvertTo(resource, typeof(byte[]))!);
  216. }
  217. #endregion
  218. #region Security
  219. public static bool UserCanAccess<T>(IDataModel model) where T : ISecurityDescriptor, new()
  220. {
  221. var userTable = model.GetTable<User>();
  222. if (userTable.Rows.Count == 0) return false;
  223. var user = userTable.Rows[0].ToObject<User>();
  224. return Security.IsAllowed<T>(user.ID, user.SecurityGroup.ID);
  225. }
  226. public static bool IsLoggedIn(IDataModel model)
  227. {
  228. var userTable = model.GetTable<User>();
  229. return userTable.Rows.Count > 0;
  230. }
  231. public static User? GetUser(IDataModel model)
  232. {
  233. var userTable = model.GetTable<User>();
  234. if (userTable.Rows.Count == 0) return null;
  235. return userTable.Rows[0].ToObject<User>();
  236. }
  237. #endregion
  238. #region Directory Viewer
  239. public static string DirectoryViewerStyle { get; } = @"<style>
  240. .prs-web_utils-file_manager {
  241. width: 30em;
  242. height: 40em;
  243. padding: 0.5em;
  244. text-align: left;
  245. overflow: auto;
  246. border: 1px solid black;
  247. background-color: white;
  248. box-sizing: border-box;
  249. }
  250. .prs-web_utils-file_manager * {
  251. box-sizing: border-box;
  252. }
  253. .prs-web_utils-file_manager-file,.prs-web_utils-file_manager-directory_head {
  254. font-size: 1em;
  255. color: black;
  256. width: 100%;
  257. overflow: hidden;
  258. white-space: nowrap;
  259. text-overflow: ellipsis;
  260. text-align: left;
  261. padding-left: 1.5em;
  262. background-position: left;
  263. background-repeat: no-repeat;
  264. background-size: contain;
  265. }
  266. .prs-web_utils-file_manager-file {
  267. border: #0000 solid 1px;
  268. background-image: url(" + ResourceToDataURL(Resources.file) + @");
  269. }
  270. .prs-web_utils-file_manager-directory_head {
  271. background-color: white;
  272. background-image: url(" + ResourceToDataURL(Resources.folder) + @");
  273. }
  274. .prs-web_utils-file_manager-pdf_type {
  275. background-image: url(" + ResourceToDataURL(Resources.pdf) + @");
  276. }
  277. .prs-web_utils-file_manager-png_type {
  278. background-image: url(" + ResourceToDataURL(Resources.png) + @");
  279. }
  280. .prs-web_utils-file_manager-jpg_type {
  281. background-image: url(" + ResourceToDataURL(Resources.jpg) + @");
  282. }
  283. .prs-web_utils-file_manager-directory_head:active {
  284. cursor: pointer;
  285. background-color: #CCCCCC
  286. }
  287. @media (hover:hover) {
  288. .prs-web_utils-file_manager-directory_head:hover {
  289. cursor: pointer;
  290. background-color: #DDDDDD
  291. }
  292. .prs-web_utils-file_manager-file:hover {
  293. cursor: pointer;
  294. background-color: #b8d9e7;
  295. border: #79c4e5 solid 1px;
  296. }
  297. }
  298. .prs-web_utils-file_manager-download {
  299. display: inline-block;
  300. width: 1.5em;
  301. height: 1.5em;
  302. padding: 0.5em;
  303. float: right;
  304. border: solid 1px black;
  305. background-image: url(" + ResourceToDataURL(Resources.download) + @");
  306. background-size: 100% 100%;
  307. }
  308. .prs-web_utils-file_manager-directory {
  309. width: 100%;
  310. padding-left: 0.5em;
  311. border-left: dashed 1px #AAA;
  312. border-bottom: dashed 1px #AAA;
  313. }
  314. .prs-web_utils-file_manager-collapsed {
  315. display: none;
  316. }
  317. </style>";
  318. private interface IFileItem
  319. {
  320. public string FilePath { get; }
  321. public string FileName { get; }
  322. }
  323. private class FileItem : IFileItem
  324. {
  325. public FileItem(string filePath, string fileName)
  326. {
  327. FilePath = filePath;
  328. FileName = fileName;
  329. }
  330. public string FilePath { get; }
  331. public string FileName { get; }
  332. }
  333. private class DirectoryItem : IFileItem
  334. {
  335. public readonly List<DirectoryItem> Directories = new();
  336. public readonly List<FileItem> Files = new();
  337. public DirectoryItem(string filePath, string fileName)
  338. {
  339. FilePath = filePath;
  340. FileName = fileName;
  341. }
  342. public string FilePath { get; }
  343. public string FileName { get; }
  344. }
  345. private static DirectoryItem? GetDirectoryStructure(string path, Func<string, bool>? fileFilter, Func<string, bool>? directoryFilter,
  346. string? name = null, int depth = 2)
  347. {
  348. var fileName = name ?? path;
  349. if (depth == 0) return new DirectoryItem(path, fileName);
  350. if (!Directory.Exists(path)) return null;
  351. var directory = new DirectoryItem(path, fileName);
  352. foreach (var item in Directory.GetDirectories(path))
  353. if (directoryFilter?.Invoke(item) != false)
  354. try
  355. {
  356. var subDirectory = GetDirectoryStructure(item, fileFilter, directoryFilter, Path.GetFileName(item), depth - 1);
  357. if (subDirectory != null) directory.Directories.Add(subDirectory);
  358. }
  359. catch (UnauthorizedAccessException)
  360. {
  361. }
  362. foreach (var item in Directory.GetFiles(path))
  363. if (fileFilter?.Invoke(item) != false)
  364. directory.Files.Add(new FileItem(item, Path.GetFileName(item)));
  365. return directory;
  366. }
  367. private static DirectoryItem? GetDirectoryStructure(string path, string? fileFilter, string? directoryFilter, string? name = null,
  368. int depth = 2)
  369. {
  370. var fileFilterRgx = fileFilter != null ? new Regex(fileFilter) : null;
  371. var dirFilterRgx = directoryFilter != null ? new Regex(directoryFilter) : null;
  372. return GetDirectoryStructure(
  373. path,
  374. fileFilterRgx != null ? x => fileFilterRgx.IsMatch(x) : null,
  375. dirFilterRgx != null ? x => dirFilterRgx.IsMatch(x) : null,
  376. name, depth
  377. );
  378. }
  379. private static readonly string DirectoryViewerScript = @"<script>
  380. function headClickEventListener(e){
  381. var next = e.target.nextSibling;
  382. next.classList.toggle(""prs-web_utils-file_manager-collapsed"");
  383. }
  384. var heads = document.getElementsByClassName(""prs-web_utils-file_manager-directory_head"");
  385. for(var head of heads){
  386. head.addEventListener(""click"", headClickEventListener);
  387. }
  388. </script>";
  389. private static void BuildDirectory(StringBuilder builder, DirectoryItem directory)
  390. {
  391. builder.Append(@"<div class=""prs-web_utils-file_manager-directory_head"">");
  392. builder.Append(directory.FileName);
  393. builder.Append(@"</div>");
  394. builder.Append(@"<div class=""prs-web_utils-file_manager-directory prs-web_utils-file_manager-collapsed""/>");
  395. if (directory.Directories.Count == 0 && directory.Files.Count == 0)
  396. {
  397. builder.Append(@"Folder is empty");
  398. }
  399. else
  400. {
  401. foreach (var subDirectory in directory.Directories) BuildDirectory(builder, subDirectory);
  402. foreach (var file in directory.Files)
  403. {
  404. var typeClass = Path.GetExtension(file.FileName) switch
  405. {
  406. ".png" => "prs-web_utils-file_manager-png_type",
  407. ".pdf" => "prs-web_utils-file_manager-pdf_type",
  408. ".jpg" => "prs-web_utils-file_manager-jpg_type",
  409. ".jpeg" => "prs-web_utils-file_manager-jpg_type",
  410. _ => null
  411. };
  412. builder.Append(@"<div class=""prs-web_utils-file_manager-file");
  413. if (typeClass != null) builder.Append(" " + typeClass);
  414. builder.Append(string.Format(@""" data-file=""{0}"">", file.FilePath));
  415. builder.Append(@"<div class=""prs-web_utils-file_manager-download""></div>");
  416. builder.Append(file.FileName);
  417. builder.Append(@"</div>");
  418. }
  419. }
  420. builder.Append(@"</div>");
  421. }
  422. /// <summary>
  423. /// </summary>
  424. /// <param name="path">The path to the folder in which to initialise the directory viewer</param>
  425. /// <param name="fileFilter">A file filter which is compiled to a Regex. Regex.IsMatch is used to filter files</param>
  426. /// <param name="directoryFilter">
  427. /// A directory filter which is compiled to a Regex. Regex.IsMatch is used to filter
  428. /// directories
  429. /// </param>
  430. /// <param name="alias">The name to call the root folder. If null, defaults to the directory path.</param>
  431. /// <param name="id">An HTML id attribute for the viewer</param>
  432. /// <param name="classList">An HTML class list for the viewer</param>
  433. /// <param name="depth">The depth of directories to search</param>
  434. /// <returns></returns>
  435. public static string BuildDirectoryViewer(string path, Func<string, bool> fileFilter, Func<string, bool>? directoryFilter = null,
  436. string? alias = null, string? id = null, string[]? classList = null, int depth = 2)
  437. {
  438. var stringBuilder = new StringBuilder();
  439. stringBuilder.Append(@"<div ");
  440. if (id != null) stringBuilder.Append("id=\"" + id + "\" ");
  441. stringBuilder.Append(@"class=""prs-web_utils-file_manager");
  442. if (classList != null)
  443. foreach (var className in classList)
  444. stringBuilder.Append(" " + className);
  445. stringBuilder.Append(@""">");
  446. var structure = GetDirectoryStructure(path, fileFilter, directoryFilter, alias ?? path, depth);
  447. if (structure != null) BuildDirectory(stringBuilder, structure);
  448. stringBuilder.Append(@"</div>");
  449. stringBuilder.Append(DirectoryViewerScript);
  450. return stringBuilder.ToString();
  451. }
  452. public static string BuildDirectoryViewer(string path, string fileFilter, string? directoryFilter = null, string? alias = null,
  453. string? id = null,
  454. string[]? classList = null, int depth = 2)
  455. {
  456. var fileFilterRgx = new Regex(fileFilter);
  457. var dirFilterRgx = directoryFilter != null ? new Regex(directoryFilter) : null;
  458. return BuildDirectoryViewer(
  459. path,
  460. x => fileFilterRgx.IsMatch(x),
  461. dirFilterRgx != null ? x => dirFilterRgx.IsMatch(x) : null,
  462. alias, id, classList, depth
  463. );
  464. }
  465. #endregion
  466. #region JSON Entities
  467. public static Dictionary<string, object> SerializeEntity(Type entityType, Entity entity)
  468. {
  469. var data = new Dictionary<string, object>();
  470. foreach (var property in DatabaseSchema.Properties(entityType))
  471. {
  472. data[property.Name] = CoreUtils.GetPropertyValue(entity, property.Name);
  473. }
  474. return data;
  475. }
  476. /// <summary>
  477. /// Generates a JSON object for javascript
  478. /// </summary>
  479. /// <typeparam name="T"></typeparam>
  480. /// <param name="entity"></param>
  481. /// <returns></returns>
  482. public static string EntityToJSON<T>(T entity) where T : Entity
  483. {
  484. var serializedEntity = SerializeEntity(typeof(T), entity);
  485. var serializedJSON = Serialization.Serialize(serializedEntity);
  486. return serializedJSON;
  487. }
  488. public static string CreateJSONEntity<T>() where T : Entity, new()
  489. {
  490. var serializedEntity = SerializeEntity(typeof(T), new T());
  491. var serializedJSON = Serialization.Serialize(serializedEntity);
  492. return serializedJSON;
  493. }
  494. /// <summary>
  495. /// Generate code for setting a property of a JSON entity
  496. /// </summary>
  497. /// <typeparam name="T"></typeparam>
  498. /// <param name="entityExpr">A Javascript expression that evaluates to the entity JSON</param>
  499. /// <param name="expression"></param>
  500. /// <returns></returns>
  501. public static string JSONSetProperty<T>(string entityExpr, Expression<Func<T, object>> expression, string value) where T : Entity
  502. {
  503. var propName = CoreUtils.GetFullPropertyName(expression, ".");
  504. return $"({entityExpr})[\"{propName}\"]={value}";
  505. }
  506. /// <summary>
  507. /// Generate code for getting a property of a JSON entity
  508. /// </summary>
  509. /// <typeparam name="T"></typeparam>
  510. /// <param name="entityExpr">A Javascript expression that evaluates to the entity JSON</param>
  511. /// <param name="expression"></param>
  512. /// <returns></returns>
  513. public static string JSONGetProperty<T>(string entityExpr, Expression<Func<T, object>> expression) where T : Entity
  514. {
  515. var propName = CoreUtils.GetFullPropertyName(expression, ".");
  516. return $"({entityExpr})[\"{propName}\"]";
  517. }
  518. #endregion
  519. #region Digital Form Viewer
  520. private const string _multiImageStyle = @"
  521. .prs-web_utils-digital_form-multi_image {
  522. }
  523. .prs-web_utils-digital_form-multi_image_cont {
  524. overflow: auto;
  525. position: relative;
  526. height: 10em;
  527. background: #999;
  528. box-shadow: black inset 0 0 4px;
  529. }
  530. .prs-web_utils-digital_form-multi_image_cont > div {
  531. position: absolute;
  532. top: 0;
  533. bottom: 0;
  534. display: flex;
  535. gap: 1em;
  536. padding: 0.4em;
  537. }
  538. .prs-web_utils-digital_form-multi_image_cont img {
  539. max-height: 100%;
  540. }
  541. .prs-web_utils-digital_form-multi_image_cont img.selected {
  542. border: dashed 2px black;
  543. }
  544. .prs-web_utils-digital_form-multi_image_btns {
  545. display: flex;
  546. gap: 0.2em;
  547. }
  548. .prs-web_utils-digital_form-multi_image_btns .prs-button {
  549. flex: 1;
  550. height: 4em;
  551. display: flex;
  552. justify-content: center;
  553. align-items: center;
  554. }
  555. ";
  556. private static string _digitalFormViewerStyle = @"
  557. .prs-web_utils-digital_form {
  558. display: grid;
  559. column-gap: 0.4em;
  560. row-gap: 0.4em;
  561. }
  562. .prs-web_utils-digital_form-field {
  563. min-height: 2em;
  564. }
  565. @media (pointer:coarse), (pointer:none) {
  566. .prs-web_utils-digital_form-field {
  567. min-height: 4em;
  568. }
  569. }
  570. .prs-web_utils-digital_form * {
  571. box-sizing: border-box;
  572. }
  573. .prs-web_utils-digital_form-label {
  574. text-align: left;
  575. padding: 1.5em 0.3em;
  576. align-self: center;
  577. overflow-wrap: anywhere;
  578. }
  579. .prs-web_utils-digital_form-image {
  580. max-width: 100%;
  581. max-height: 100%;
  582. }
  583. .prs-web_utils-digital_form-color,
  584. .prs-web_utils-digital_form-integer,
  585. .prs-web_utils-digital_form-text,
  586. .prs-web_utils-digital_form-string,
  587. .prs-web_utils-digital_form-url,
  588. .prs-web_utils-digital_form-password,
  589. .prs-web_utils-digital_form-date,
  590. .prs-web_utils-digital_form-datetime {
  591. background-color: white;
  592. }
  593. .prs-web_utils-digital_form-time {
  594. display: flex;
  595. align-items: center;
  596. width: min-content;
  597. background: white;
  598. border: solid 1px gray;
  599. }
  600. .prs-web_utils-digital_form-time_hours {
  601. text-align: right;
  602. }
  603. .prs-web_utils-digital_form-time_mins {
  604. text-align: left;
  605. }
  606. .prs-web_utils-digital_form-time span {
  607. margin: 0.1em;
  608. }
  609. .prs-web_utils-digital_form-time input {
  610. height: 100%;
  611. width: 3em;
  612. font-family: monospace;
  613. border: none;
  614. background: none;
  615. }
  616. .prs-web_utils-digital_form-time input:active {
  617. background: #ddd;
  618. }
  619. .prs-web_utils-digital_form-timestamp {
  620. display: flex;
  621. justify-content: space-between;
  622. align-items: center;
  623. gap: 0.4em;
  624. }
  625. .prs-web_utils-digital_form-timestamp input {
  626. flex: 1;
  627. height: 100%;
  628. }
  629. .prs-web_utils-digital_form-timestamp .prs-button {
  630. height: 100%;
  631. padding: 1em;
  632. display: flex;
  633. align-items: center;
  634. }
  635. .prs-web_utils-digital_form-pin {
  636. display: flex;
  637. justify-content: space-between;
  638. align-items: center;
  639. gap: 0.4em;
  640. }
  641. .prs-web_utils-digital_form-pin input {
  642. flex: 1;
  643. height: 100%;
  644. }
  645. .prs-web_utils-digital_form-pin .prs-button {
  646. height: 100%;
  647. padding: 1em;
  648. display: flex;
  649. align-items: center;
  650. }
  651. .prs-web_utils-digital_form-document {
  652. display: flex;
  653. justify-content: space-between;
  654. align-items: center;
  655. gap: 0.4em;
  656. }
  657. .prs-web_utils-digital_form-document input {
  658. flex: 1;
  659. height: 100%;
  660. }
  661. .prs-web_utils-digital_form-document .prs-button {
  662. height: 100%;
  663. padding: 1em;
  664. display: flex;
  665. align-items: center;
  666. }
  667. .prs-web_utils-digital_form-notes {
  668. display: flex;
  669. gap: 0.2em;
  670. }
  671. .prs-web_utils-digital_form-note_cont {
  672. flex: 1;
  673. display: flex;
  674. flex-direction: column;
  675. gap: 0.2em;
  676. }
  677. .prs-web_utils-digital_form-note {
  678. resize: none;
  679. width: 100%;
  680. min-height: 6em;
  681. background-color: #cdcdcd;
  682. }
  683. .prs-web_utils-digital_form-note.active {
  684. background-color: lightgoldenrodyellow;
  685. }
  686. .prs-web_utils-digital_form-note_error {
  687. color: black;
  688. text-align: left;
  689. }
  690. .prs-web_utils-digital_form-note_add {
  691. display: flex;
  692. justify-content: center;
  693. align-items: center;
  694. text-align: center;
  695. background-color: #dedede;
  696. border: solid 1px black;
  697. border-radius: 3px;
  698. user-select: none;
  699. width: 2em;
  700. }
  701. @media (hover:hover) {
  702. .prs-web_utils-digital_form-note_add:hover {
  703. cursor: pointer;
  704. background-color: #cacaca;
  705. }
  706. }
  707. .prs-web_utils-digital_form-note_add:active {
  708. background-color: #ccc;
  709. }
  710. .prs-web_utils-digital_form-embedded_image, .prs-web_utils-digital_form-signature {
  711. max-width: 100%;
  712. border-radius: 3px;
  713. min-height: 2em;
  714. display: flex;
  715. flex-direction: column;
  716. }
  717. .prs-web_utils-digital_form-img_cont {
  718. flex: 1;
  719. overflow: hidden;
  720. }
  721. .prs-web_utils-digital_form-clear_img {
  722. height: 4em;
  723. display: flex;
  724. justify-content: center;
  725. align-items: center;
  726. }
  727. .prs-web_utils-digital_form-img {
  728. max-width: 100%;
  729. max-height: 100%;
  730. }
  731. div.prs-web_utils-digital_form-boolean {
  732. display: flex;
  733. align-items: center;
  734. gap: 0.2em;
  735. }
  736. .prs-web_utils-digital_form-boolean label {
  737. flex: 1;
  738. text-align: left;
  739. }
  740. input.prs-web_utils-digital_form-boolean {
  741. width: min-content;
  742. height: min-content;
  743. margin: auto;
  744. }
  745. .prs-web_utils-digital_form-lookup, .prs-web_utils-digital_form-option {
  746. min-width: 0;
  747. min-height: 2em;
  748. background-color: white;
  749. }
  750. .prs-web_utils-digital_form-option_fake {
  751. visibility:hidden;
  752. margin-left:1em;
  753. overflow:hidden;
  754. min-height: 2em;
  755. }
  756. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button {
  757. display: inline-block;
  758. flex: 1;
  759. height: 100%;
  760. position: relative;
  761. height: 2em;
  762. background: none;
  763. border: none;
  764. }
  765. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input {
  766. appearance: none;
  767. background-color: #a7a7a7;
  768. border-radius: 3px;
  769. border: dashed 1px black;
  770. margin: 0;
  771. width: 100%;
  772. height: 100%;
  773. }
  774. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked {
  775. background-color: #e7e7e7;
  776. border: solid 1px black;
  777. }
  778. @media (hover:hover) {
  779. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:hover {
  780. cursor:pointer;
  781. background-color: #a3a3a3;
  782. }
  783. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked:hover {
  784. background-color: #e0e0e0;
  785. }
  786. }
  787. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:active {
  788. background-color: #979797;
  789. }
  790. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button input:checked:active {
  791. background-color: #c6c6c6;
  792. }
  793. .prs-web_utils-digital_form-boolean .prs-web_utils-digital_form-button span {
  794. position: absolute;
  795. left: 0;
  796. right: 0;
  797. top: 0;
  798. bottom: 0;
  799. pointer-events: none;
  800. line-height: 2em;
  801. }
  802. .prs-web_utils-digital_form-radio_cont {
  803. display: flex;
  804. justify-content: stretch;
  805. align-items: center;
  806. }
  807. .prs-web_utils-digital_form-radio_cont label {
  808. flex: 1;
  809. text-align: left;
  810. }
  811. .prs-web_utils-digital_form-save_cont {
  812. margin-top: 2em;
  813. display: flex;
  814. gap: 0.2em;
  815. }
  816. .prs-web_utils-digital_form-save_cont .prs-button {
  817. height: 4em;
  818. flex: 1;
  819. display: flex;
  820. align-items: center;
  821. justify-content: center;
  822. }
  823. .prs-web_utils-digital_form-add_task {
  824. display: flex;
  825. gap: 0.4em;
  826. }
  827. .prs-web_utils-digital_form-add_task .prs-button {
  828. height: 4em;
  829. padding: 0 1em;
  830. display: flex;
  831. align-items: center;
  832. justify-content: center;
  833. }
  834. .prs-web_utils-digital_form-add_task input {
  835. flex: 1;
  836. }
  837. .prs-web_utils-digital_form input:invalid {
  838. background-color: #FFDDDD;
  839. }
  840. .prs-web_utils-digital_form-field:required {
  841. border: solid 1px #edaf58;
  842. }
  843. .prs-web_utils-digital_form-validate {
  844. position: absolute;
  845. top: 1em;
  846. left: 1em;
  847. height: 3em;
  848. line-height: 3em;
  849. padding: 0 1em;
  850. background: white;
  851. border: black solid 1px;
  852. border-radius: 3px;
  853. box-shadow: grey 0 0 10px;
  854. transition: opacity 1s linear 0s;
  855. }
  856. " + _multiImageStyle;
  857. /// <summary>
  858. /// A string which contains a stylesheet used for formatting digital forms. This is not necessary, but can be useful as
  859. /// a base.<br />
  860. /// Include within the &lt;style&gt; tag in your document head.
  861. /// </summary>
  862. public static string DigitalFormViewerStyle => _digitalFormViewerStyle;
  863. private const string _submitScript = @"
  864. var submitButtons = document.getElementsByClassName(""prs-web_utils-digital_form-submit"");
  865. var saveButtons = document.getElementsByClassName(""prs-web_utils-digital_form-save"");
  866. function getData(element){
  867. if(element.classList.contains(""prs-web_utils-digital_form-double"")
  868. || element.classList.contains(""prs-web_utils-digital_form-integer"")){
  869. var res = element.valueAsNumber;
  870. if(Number.isNaN(res)){
  871. return null;
  872. }
  873. return res;
  874. } else if(element.classList.contains(""prs-web_utils-digital_form-color"")){
  875. return ""#FF"" + element.value.substring(1).toUpperCase();
  876. } else if(element.classList.contains(""prs-web_utils-digital_form-multi_image"")){
  877. let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];
  878. let imgContDiv = imgCont.children[0];
  879. let imgs = [];
  880. for(let img of imgContDiv.children){
  881. imgs.push(img.src.replace(/^data:(.*,)?/, ''));
  882. }
  883. return imgs;
  884. } else if(element.classList.contains(""prs-web_utils-digital_form-notes"")){
  885. let cont = element.getElementsByClassName(""prs-web_utils-digital_form-note_cont"")[0];
  886. let retVal = [];
  887. for(let note of cont.children){
  888. retVal.push(note.value);
  889. }
  890. if(!(/\S/.test(retVal[retVal.length - 1]))){
  891. retVal.pop();
  892. }
  893. return retVal;
  894. } else if(element.classList.contains(""prs-web_utils-digital_form-time"")){
  895. let hours = element.getElementsByClassName(""prs-web_utils-digital_form-time_hours"")[0];
  896. let mins = element.getElementsByClassName(""prs-web_utils-digital_form-time_mins"")[0];
  897. return hours.value + "":"" + mins.value;
  898. } else if(element.classList.contains(""prs-web_utils-digital_form-date"")
  899. || element.classList.contains(""prs-web_utils-digital_form-datetime"")){
  900. return element.value;
  901. } else if(element.classList.contains(""prs-web_utils-digital_form-pin"")){
  902. let pinText = element.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];
  903. return pinText.value;
  904. } else if(element.classList.contains(""prs-web_utils-digital_form-timestamp"")){
  905. let dateText = element.getElementsByClassName(""prs-web_utils-digital_form-timestamp_date"")[0];
  906. return dateText.value;
  907. } else if(element.classList.contains(""prs-web_utils-digital_form-text"")
  908. || element.classList.contains(""prs-web_utils-digital_form-string"")
  909. || element.classList.contains(""prs-web_utils-digital_form-password"")
  910. || element.classList.contains(""prs-web_utils-digital_form-url"")
  911. || element.classList.contains(""prs-web_utils-digital_form-code"")
  912. || element.classList.contains(""prs-web_utils-digital_form-option"")){
  913. return element.value;
  914. } else if(element.classList.contains(""prs-web_utils-digital_form-radio_cont"")){
  915. var options = element.getElementsByTagName(""input"");
  916. for(let option of options){
  917. if(option.checked){
  918. return option.value;
  919. }
  920. }
  921. return null;
  922. } else if(element.classList.contains(""prs-web_utils-digital_form-lookup"")){
  923. return element.value;
  924. } else if(element.classList.contains(""prs-web_utils-digital_form-embedded_image"")
  925. || element.classList.contains(""prs-web_utils-digital_form-signature"")){
  926. var imgs = element.getElementsByClassName(""prs-web_utils-digital_form-img"");
  927. if(imgs.length == 0){
  928. return null;
  929. } else {
  930. var img = imgs[0];
  931. var src = img.getAttribute(""src"");
  932. var match = /data:[^;]*;base64,/d.exec(src);
  933. if(match){
  934. return src.substring(match.indices[0][1]);
  935. }
  936. return null;
  937. }
  938. } else if(element.classList.contains(""prs-web_utils-digital_form-document"")){
  939. return element.getAttribute(""data-id"");
  940. } else if(element.classList.contains(""prs-web_utils-digital_form-boolean"")){
  941. if(element.tagName == ""INPUT""){
  942. return element.checked;
  943. } else if(element.tagName == ""SELECT""){
  944. if(element.selectedOptions.length > 0){
  945. return element.selectedOptions[0].hasAttribute(""data-true"");
  946. }
  947. return null;
  948. } else if(element.tagName == ""DIV""){
  949. for(var element of element.getElementsByTagName(""input"")){
  950. if(element.checked){
  951. return element.hasAttribute(""data-true"");
  952. }
  953. }
  954. return null;
  955. }
  956. } else if(element.classList.contains(""prs-web_utils-digital_form-add_task"")){
  957. var input = element.getElementsByTagName(""input"")[0];
  958. return input.value;
  959. }
  960. }
  961. function validate(element){
  962. let valid = true;
  963. let err = """";
  964. if(element.classList.contains(""prs-web_utils-digital_form-embedded_image"")
  965. || element.classList.contains(""prs-web_utils-digital_form-signature"")){
  966. let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];
  967. let imgs = imgCont.getElementsByTagName(""img"");
  968. if(imgs.length == 0){
  969. valid = false;
  970. err = ""Please provide an image"";
  971. }
  972. } else if(element.classList.contains(""prs-web_utils-digital_form-multi_image"")){
  973. let imgCont = element.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];
  974. let imgContDiv = imgCont.children[0];
  975. if(imgContDiv.children.length == 0){
  976. valid = false;
  977. err = ""Please provide at least one image"";
  978. }
  979. }
  980. return [valid, err];
  981. }
  982. function doValidationCheck(form){
  983. var validations = form.getElementsByClassName(""prs-web_utils-digital_form-validate"");
  984. for(let validation of validations){
  985. validation.remove();
  986. }
  987. if(!form.reportValidity()){ return false; }
  988. let valid = true;
  989. var elements = form.getElementsByClassName(""prs-web_utils-digital_form-field"");
  990. for(var element of elements){
  991. if(element.hasAttribute(""required"")){
  992. var [elValid, err] = validate(element);
  993. valid = elValid && valid;
  994. if(!elValid){
  995. element.scrollIntoView();
  996. let validation = document.createElement(""div"");
  997. validation.classList.add(""prs-web_utils-digital_form-validate"");
  998. validation.textContent = err;
  999. element.style.position = ""relative"";
  1000. element.appendChild(validation);
  1001. setTimeout(() => {validation.style.opacity = 0;}, 5000);
  1002. }
  1003. }
  1004. }
  1005. return valid;
  1006. }
  1007. function submitForm(form, save, validate = true){
  1008. if(!validate || doValidationCheck(form)){
  1009. var formData = {};
  1010. var elements = form.getElementsByClassName(""prs-web_utils-digital_form-field"");
  1011. for(var element of elements){
  1012. formData[element.getAttribute(""name"")] = getData(element);
  1013. if(element.classList.contains(""prs-web_utils-digital_form-lookup"")){
  1014. for(var option of element.getElementsByTagName(""option"")){
  1015. if(option.value == element.value){
  1016. formData[element.getAttribute(""name"") + ""$ID""] = option.getAttribute(""data-id"");
  1017. break;
  1018. }
  1019. }
  1020. }
  1021. }
  1022. let eventName = save ? ""prs-web_utils-df_save"" : ""prs-web_utils-df_submit"";
  1023. const submitFormEvent = new CustomEvent(eventName, { detail: { formData: formData } });
  1024. form.dispatchEvent(submitFormEvent);
  1025. }
  1026. }
  1027. for(var btn of submitButtons){
  1028. btn.addEventListener(""click"", function(e) {
  1029. var form = e.target.parentNode.parentNode;
  1030. submitForm(form, false, true);
  1031. });
  1032. }
  1033. for(var btn of saveButtons){
  1034. btn.addEventListener(""click"", function(e) {
  1035. var form = e.target.parentNode.parentNode;
  1036. submitForm(form, true, false);
  1037. });
  1038. }
  1039. ";
  1040. private static string GetDigitalFormViewerScript()
  1041. {
  1042. return @"<script>
  1043. function checkDisabled(target, fnc){
  1044. target.addEventListener(""click"", function(e){
  1045. return !target.hasAttribute(""disabled"") ? fnc(e) : null;
  1046. });
  1047. }
  1048. var integerFields = document.getElementsByClassName(""prs-web_utils-digital_form-integer"");
  1049. function integerValidator(e)
  1050. {
  1051. e.target.value = Math.floor(e.target.value);
  1052. }
  1053. for(var field of integerFields){
  1054. field.addEventListener(""change"", integerValidator);
  1055. }
  1056. function changeSelect(e){
  1057. var parent = e.target.parentNode;
  1058. var label = parent.getElementsByTagName(""div"")[0];
  1059. label.textContent = e.target.value;
  1060. }
  1061. var optionCont = document.getElementsByClassName(""prs-web_utils-digital_form-option_cont"");
  1062. for(var option of optionCont){
  1063. var select = option.getElementsByTagName(""select"")[0];
  1064. select.setAttribute(""style"", ""position: absolute;top:0;left:0;right:0;bottom:0;"");
  1065. select.addEventListener(""change"", changeSelect);
  1066. }
  1067. function saveDocument(filename, data, onResponse){
  1068. var xhr = new XMLHttpRequest();
  1069. xhr.open(""POST"", ""/save/Document"");
  1070. xhr.setRequestHeader(""Content-Type"", ""application/json"");
  1071. xhr.responseType = ""json"";
  1072. xhr.onreadystatechange = function(){
  1073. if(xhr.readyState === 4){
  1074. onResponse(xhr.response);
  1075. }
  1076. }
  1077. xhr.send(JSON.stringify({
  1078. ""FileName"": filename,
  1079. ""Data"": data
  1080. }));
  1081. }
  1082. function selectDocument(e){
  1083. var editor = e.target.parentNode;
  1084. var filename = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];
  1085. var accept = filename.getAttribute(""accept"");
  1086. var input = document.createElement(""input"");
  1087. input.type = ""file"";
  1088. input.setAttribute(""accept"", accept);
  1089. input.onchange = e => {
  1090. var file = e.target.files[0];
  1091. var reader = new FileReader();
  1092. reader.onloadend = (re) => {
  1093. saveDocument(file.name, reader.result.replace(/^data:(.*,)?/, ''), function(data){
  1094. filename.value = data[""FileName""];
  1095. editor.setAttribute(""data-id"", data[""ID""]);
  1096. });
  1097. };
  1098. reader.readAsDataURL(file);
  1099. };
  1100. input.click();
  1101. }
  1102. function clearDocument(e){
  1103. var editor = e.target.parentNode;
  1104. editor.removeAttribute(""data-id"");
  1105. var filenameEl = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];
  1106. filenameEl.value = """";
  1107. }
  1108. function viewDocument(e){
  1109. var editor = e.target.parentNode;
  1110. if(editor.hasAttribute(""data-id"")){
  1111. var id = editor.getAttribute(""data-id"");
  1112. var filenameEl = editor.getElementsByClassName(""prs-web_utils-digital_form-filename"")[0];
  1113. console.log(filenameEl.value);
  1114. const viewDocEvent = new CustomEvent(""prs-web_utils-df_view_doc"", { detail: { id: id, filename: filenameEl.value } });
  1115. editor.dispatchEvent(viewDocEvent);
  1116. }
  1117. }
  1118. var docSelectBtns = document.getElementsByClassName(""prs-web_utils-digital_form-select_doc"");
  1119. var docClearBtns = document.getElementsByClassName(""prs-web_utils-digital_form-clear_doc"");
  1120. var docViewBtns = document.getElementsByClassName(""prs-web_utils-digital_form-view_doc"");
  1121. for(var docSelect of docSelectBtns){
  1122. checkDisabled(docSelect, selectDocument);
  1123. }
  1124. for(var docClear of docClearBtns){
  1125. checkDisabled(docClear, clearDocument);
  1126. }
  1127. for(var docView of docViewBtns){
  1128. checkDisabled(docView, viewDocument);
  1129. }
  1130. function createPIN(e){
  1131. var editor = e.target.parentNode;
  1132. var length = parseInt(editor.getAttribute(""data-length""), 10);
  1133. var pinEl = editor.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];
  1134. pinEl.value = """";
  1135. for(let i = 0; i < length; i++){
  1136. pinEl.value += Math.floor(Math.random() * 10).toString();
  1137. }
  1138. }
  1139. function clearPIN(e){
  1140. var editor = e.target.parentNode;
  1141. var pinEl = editor.getElementsByClassName(""prs-web_utils-digital_form-pin_text"")[0];
  1142. pinEl.value = """";
  1143. }
  1144. var pinCreateBtns = document.getElementsByClassName(""prs-web_utils-digital_form-create_pin"");
  1145. var pinClearBtns = document.getElementsByClassName(""prs-web_utils-digital_form-clear_pin"");
  1146. for(var pinCreate of pinCreateBtns){
  1147. checkDisabled(pinCreate, createPIN);
  1148. }
  1149. for(var pinClear of pinClearBtns){
  1150. checkDisabled(pinClear, clearPIN);
  1151. }
  1152. function toggleStamp(e){
  1153. var editor = e.target.parentNode;
  1154. var stampEl = editor.getElementsByClassName(""prs-web_utils-digital_form-timestamp_date"")[0];
  1155. if(/\S/.test(stampEl.value)){
  1156. stampEl.value = """";
  1157. e.target.textContent = ""Set"";
  1158. } else {
  1159. var dateString = (new Date()).toISOString();
  1160. stampEl.value = dateString.substring(0, dateString.length - 1);
  1161. e.target.textContent = ""Clear"";
  1162. }
  1163. }
  1164. var pinToggleBtns = document.getElementsByClassName(""prs-web_utils-digital_form-toggle_stamp"");
  1165. for(var pinToggle of pinToggleBtns){
  1166. checkDisabled(pinToggle, toggleStamp);
  1167. }
  1168. var embedImageFields = document.getElementsByClassName(""prs-web_utils-digital_form-embedded_image"");
  1169. var signatureFields = document.getElementsByClassName(""prs-web_utils-digital_form-signature"");
  1170. function fileSelector(e)
  1171. {
  1172. var selectedFile = e.target.files[0];
  1173. var target = e.target;
  1174. var parent = e.target.parentNode;
  1175. let clearButton = parent.nextSibling;
  1176. var img = document.createElement(""img"");
  1177. img.classList.add(""prs-web_utils-digital_form-img"");
  1178. const reader = new FileReader();
  1179. reader.onload = (e) => {
  1180. img.src = e.target.result;
  1181. clearButton.removeAttribute(""disabled"");
  1182. }
  1183. reader.readAsDataURL(selectedFile);
  1184. parent.replaceChildren(img, target);
  1185. }
  1186. function fileButton(field, fileElement){
  1187. field.addEventListener(""click"", (e) => {
  1188. fileElement.click();
  1189. });
  1190. }
  1191. function clearEmbeddedImage(e){
  1192. let imgField = e.target.parentNode;
  1193. let imgCont = imgField.children[0];
  1194. let span = document.createElement(""span"");
  1195. span.textContent = ""Choose a file"";
  1196. imgCont.replaceChildren(span, imgCont.children[1]);
  1197. e.target.setAttribute(""disabled"", """");
  1198. }
  1199. var clearButtons = [];
  1200. for(var field of embedImageFields){
  1201. var imgElement = field.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];
  1202. var fileElement = field.getElementsByTagName(""input"")[0];
  1203. fileButton(imgElement, fileElement);
  1204. fileElement.addEventListener(""change"", fileSelector);
  1205. clearButtons.push(field.getElementsByClassName(""prs-web_utils-digital_form-clear_img"")[0]);
  1206. }
  1207. for(var field of signatureFields){
  1208. var imgElement = field.getElementsByClassName(""prs-web_utils-digital_form-img_cont"")[0];
  1209. var fileElement = field.getElementsByTagName(""input"")[0];
  1210. fileButton(imgElement, fileElement);
  1211. fileElement.addEventListener(""change"", fileSelector);
  1212. clearButtons.push(field.getElementsByClassName(""prs-web_utils-digital_form-clear_img"")[0]);
  1213. }
  1214. for(let clearButton of clearButtons){
  1215. checkDisabled(clearButton, clearEmbeddedImage);
  1216. }
  1217. function enableMultiImageRem(multiImage, enabled = true){
  1218. if(!multiImage.hasAttribute(""disabled"")){
  1219. var btn = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_remove"")[0];
  1220. if(enabled){
  1221. btn.removeAttribute(""disabled"");
  1222. } else {
  1223. btn.setAttribute(""disabled"", """");
  1224. }
  1225. }
  1226. }
  1227. function multiImageClick(e){
  1228. var multiImage = e.target.parentNode.parentNode.parentNode;
  1229. for(let img of e.target.parentNode.children){
  1230. img.classList.remove(""selected"");
  1231. }
  1232. e.target.classList.add(""selected"");
  1233. enableMultiImageRem(multiImage);
  1234. }
  1235. var multiImages = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image"");
  1236. for(let multiImg of multiImages){
  1237. var imgs = multiImg.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0].getElementsByTagName(""img"");
  1238. for(let img of imgs){
  1239. img.addEventListener(""click"", multiImageClick);
  1240. }
  1241. }
  1242. function multiImageRemove(e){
  1243. let multiImage = e.target.parentNode.parentNode;
  1244. let imgList = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0].children[0];
  1245. for(let img of imgList.children){
  1246. if(img.classList.contains(""selected"")){
  1247. imgList.removeChild(img);
  1248. }
  1249. }
  1250. enableMultiImageRem(multiImage, false);
  1251. }
  1252. function multiImageAdd(e){
  1253. let multiImage = e.target.parentNode.parentNode;
  1254. let imgCont = multiImage.getElementsByClassName(""prs-web_utils-digital_form-multi_image_cont"")[0];
  1255. let imgContDiv = imgCont.children[0];
  1256. var input = document.createElement(""input"");
  1257. input.type = ""file"";
  1258. input.setAttribute(""accept"", ""image/*"");
  1259. input.onchange = e => {
  1260. var file = e.target.files[0];
  1261. var reader = new FileReader();
  1262. reader.onloadend = (re) => {
  1263. var img = document.createElement(""img"");
  1264. img.src = reader.result;
  1265. img.addEventListener(""click"", multiImageClick);
  1266. imgContDiv.appendChild(img);
  1267. };
  1268. reader.readAsDataURL(file);
  1269. };
  1270. input.click();
  1271. }
  1272. var multiImageAddBtns = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image_add"");
  1273. var multiImageRemBtns = document.getElementsByClassName(""prs-web_utils-digital_form-multi_image_remove"");
  1274. for(var multiImageBtn of multiImageAddBtns){
  1275. checkDisabled(multiImageBtn, multiImageAdd);
  1276. }
  1277. for(var multiImageBtn of multiImageRemBtns){
  1278. checkDisabled(multiImageBtn, multiImageRemove);
  1279. }
  1280. function addNote(e){
  1281. let notesCont = e.target.parentNode.getElementsByClassName(""prs-web_utils-digital_form-note_cont"")[0];
  1282. let noteChildren = notesCont.getElementsByClassName(""prs-web_utils-digital_form-note"");
  1283. let lastChild = noteChildren[noteChildren.length - 1];
  1284. let error = notesCont.getElementsByClassName(""prs-web_utils-digital_form-note_error"")[0];
  1285. if(/\S/.test(lastChild.value)){
  1286. lastChild.classList.remove(""active"");
  1287. lastChild.setAttribute(""disabled"", """");
  1288. let newNotes = document.createElement(""textarea"");
  1289. newNotes.classList.add(""prs-web_utils-digital_form-note"");
  1290. newNotes.classList.add(""active"");
  1291. notesCont.insertBefore(newNotes, error);
  1292. error.textContent = """";
  1293. } else {
  1294. error.textContent = ""Please enter a note!"";
  1295. }
  1296. }
  1297. var notesAddButtons = document.getElementsByClassName(""prs-web_utils-digital_form-note_add"");
  1298. for(let noteAdd of notesAddButtons){
  1299. checkDisabled(noteAdd, addNote);
  1300. }
  1301. function addTask(e){
  1302. let field = e.target.parentNode;
  1303. let kanbanType = field.getAttribute(""data-tasktype"");
  1304. let kanban = " + WebUtils.CreateJSONEntity<Kanban>() + ";" +
  1305. WebUtils.JSONSetProperty<Kanban>("kanban", x => x.Type.ID, "kanbanType") + @";
  1306. let eventName = ""prs-web_utils-df_add_task"";
  1307. const addTaskEvent = new CustomEvent(eventName, { detail: {
  1308. kanban: kanban,
  1309. setTaskNumber: function(number){" +
  1310. WebUtils.JSONSetProperty<Kanban>("kanban", x => x.Number, "number") + @";
  1311. var input = field.getElementsByTagName(""input"")[0];
  1312. input.value = number.toString();
  1313. var btn = field.getElementsByClassName(""prs-button"")[0];
  1314. btn.setAttribute(""disabled"", """");
  1315. submitForm(field.closest("".prs-web_utils-digital_form""), true, false);
  1316. }
  1317. }});
  1318. form.dispatchEvent(addTaskEvent);
  1319. }
  1320. var addTaskFields = document.getElementsByClassName(""prs-web_utils-digital_form-add_task"");
  1321. for(let addTaskField of addTaskFields){
  1322. let addTaskBtn = addTaskField.getElementsByClassName(""prs-button"")[0];
  1323. checkDisabled(addTaskBtn, addTask);
  1324. }
  1325. " + _submitScript + "</script>";
  1326. }
  1327. public static string DigitalFormViewerScript => GetDigitalFormViewerScript();
  1328. private static string BuildLookupField(
  1329. IEnumerable<Tuple<string, string?>> options,
  1330. string? selectedString,
  1331. string style,
  1332. string className,
  1333. bool readOnly,
  1334. bool required,
  1335. string name
  1336. )
  1337. {
  1338. var element = new StringBuilder(
  1339. string.Format(@"<div class=""prs-web_utils-digital_form-option_cont"" style=""{0}position: relative;font-size: 1.2em;"">", style));
  1340. var childOptions = new StringBuilder();
  1341. var display = selectedString ?? "";
  1342. foreach (var option in options)
  1343. {
  1344. var idString = option.Item2 != null ? $" data-id=\"{option.Item2}\"" : "";
  1345. childOptions.Append($"<option{(option.Item1 == selectedString ? " selected" : "")} value=\"{option.Item1}\"{idString}>{option.Item1}</option>");
  1346. }
  1347. element.AppendFormat(
  1348. @"<div class=""prs-web_utils-digital_form-option_fake"">{0}</div><select name=""{1}"" class=""{2}""{3}{4}><option></option>",
  1349. display,
  1350. name,
  1351. className,
  1352. readOnly ? " disabled" : "",
  1353. required ? " required" : ""
  1354. ).Append(childOptions).Append("</select></div>");
  1355. return element.ToString();
  1356. }
  1357. private static string BuildRadioButtons(
  1358. IEnumerable<string> options,
  1359. string? selectedString,
  1360. string style,
  1361. string className,
  1362. bool readOnly,
  1363. bool required,
  1364. string name
  1365. )
  1366. {
  1367. var element = new StringBuilder(
  1368. 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));
  1369. var childOptions = new StringBuilder();
  1370. var radioOptions = string.Format(
  1371. @"name=""{0}"" type=""radio""{1}{2}",
  1372. name,
  1373. required ? " required" : "",
  1374. readOnly ? " disabled" : ""
  1375. );
  1376. foreach (var option in options)
  1377. {
  1378. var selected = option == selectedString;
  1379. var valueStr = $" value=\"{option}\"";
  1380. var optionID = Guid.NewGuid();
  1381. childOptions.Append($"<input id=\"{optionID}\"{valueStr} {radioOptions} {(selected ? " checked" : "")}/>"
  1382. + $"<label for=\"{optionID}\">{option}</label>");
  1383. }
  1384. element.Append(childOptions).Append("</div>");
  1385. return element.ToString();
  1386. }
  1387. public static string BuildDigitalFormViewer(
  1388. DFLayout layout,
  1389. Dictionary<string, object>? data,
  1390. bool readOnly,
  1391. string? id = null, string[]? classList = null,
  1392. string submitLabel = "Submit Form", string? saveLabel = "Save Form"
  1393. )
  1394. {
  1395. StringBuilder stringBuilder = new();
  1396. stringBuilder.Append("<form ");
  1397. if (id != null) stringBuilder.Append("id=\"" + id + "\" ");
  1398. stringBuilder.Append(@"class=""prs-web_utils-digital_form");
  1399. if (classList != null)
  1400. foreach (var className in classList)
  1401. stringBuilder.Append(" " + className);
  1402. stringBuilder.Append(@""" style=""grid-template-columns:");
  1403. foreach (var width in layout.ColumnWidths)
  1404. if (width == "Auto")
  1405. {
  1406. stringBuilder.Append("auto ");
  1407. }
  1408. else if (width == "*")
  1409. {
  1410. stringBuilder.Append("1fr ");
  1411. }
  1412. else
  1413. {
  1414. stringBuilder.Append(width);
  1415. stringBuilder.Append("px ");
  1416. }
  1417. stringBuilder.Append(@";grid-template-rows:");
  1418. foreach (var height in layout.RowHeights)
  1419. if (height == "Auto")
  1420. {
  1421. stringBuilder.Append("auto ");
  1422. }
  1423. else if (height == "*")
  1424. {
  1425. stringBuilder.Append("1fr ");
  1426. }
  1427. else
  1428. {
  1429. stringBuilder.Append(height);
  1430. stringBuilder.Append("px ");
  1431. }
  1432. stringBuilder.Append(@"min-content;"">");
  1433. var lastRowNum = layout.RowHeights.Count + 1;
  1434. foreach (var element in layout.Elements)
  1435. {
  1436. var style = string.Format("grid-column: {0} / span {1};grid-row: {2} / span {3};", element.Column, element.ColumnSpan, element.Row,
  1437. element.RowSpan);
  1438. if (element is DFLayoutField fieldElement)
  1439. {
  1440. var required = fieldElement.GetPropertyValue<bool>("Required");
  1441. var fieldReadOnly = readOnly || fieldElement.GetPropertyValue<bool>("Secure");
  1442. object? fieldData = null;
  1443. data?.TryGetValue(fieldElement.Name, out fieldData);
  1444. var tagType = "input";
  1445. var classes = "";
  1446. string? value = null;
  1447. var properties = "";
  1448. string? children = null;
  1449. // TODO: DocumentField, MultiSignaturePad
  1450. if(element is DFLayoutAddTaskField addTaskField)
  1451. {
  1452. var kanbanType = addTaskField.Properties.TaskType;
  1453. var kanbanNumber = (int?)fieldData;
  1454. tagType = "div";
  1455. classes = "prs-web_utils-digital_form-add_task";
  1456. properties = $"data-tasktype=\"{kanbanType.ID}\"";
  1457. children = $"<input type=\"number\" step=\"1\" disabled{(kanbanNumber != null ? $" value=\"{kanbanNumber}\"" : "")}/>" +
  1458. $"<div class=\"prs-button\"{(fieldReadOnly || kanbanNumber != null ? " disabled" : "")}>Create Task</div>";
  1459. }
  1460. else if (element is DFLayoutBooleanField)
  1461. {
  1462. var fieldType = fieldElement.GetPropertyValue<DesignBooleanFieldType>("Type");
  1463. var trueValue = fieldElement.GetPropertyValue<string>("TrueValue");
  1464. var falseValue = fieldElement.GetPropertyValue<string>("FalseValue");
  1465. bool checkTrue = (bool?)fieldData == true;
  1466. bool checkFalse = (bool?)fieldData == false;
  1467. if (fieldType == DesignBooleanFieldType.Checkbox)
  1468. {
  1469. tagType = "div";
  1470. classes = "prs-web_utils-digital_form-boolean";
  1471. var options = string.Format(
  1472. @"name=""{0}"" type=""radio""{1}{2}",
  1473. fieldElement.Name,
  1474. required ? " required" : "",
  1475. fieldReadOnly ? " disabled" : ""
  1476. );
  1477. var disabled = fieldReadOnly ? " disabled" : "";
  1478. var idTrue = Guid.NewGuid();
  1479. var idFalse = Guid.NewGuid();
  1480. children =
  1481. $"<input value=\"{trueValue}\" id=\"{idTrue}\" {(checkTrue ? " checked" : "")} data-true {options}/>" +
  1482. $"<label for=\"{idTrue}\">{trueValue}</label>" +
  1483. $"<input value=\"{falseValue}\" id=\"{idFalse}\" \"{(checkFalse ? " checked" : "")} data-false {options}/>" +
  1484. $"<label for=\"{idFalse}\">{falseValue}</label>";
  1485. }
  1486. else if (fieldType == DesignBooleanFieldType.ComboBox)
  1487. {
  1488. classes = "prs-web_utils-digital_form-boolean";
  1489. tagType = "select";
  1490. children = string.Format(
  1491. @"<option{0} data-true>{1}</option><option{2} data-false>{3}</option>",
  1492. checkTrue ? " selected" : "",
  1493. trueValue,
  1494. checkFalse ? " selected" : "",
  1495. falseValue
  1496. );
  1497. }
  1498. else if (fieldType == DesignBooleanFieldType.Buttons)
  1499. {
  1500. classes = "prs-web_utils-digital_form-boolean";
  1501. tagType = "div";
  1502. var options = string.Format(
  1503. @"name=""{0}"" type=""radio""{1}{2}",
  1504. fieldElement.Name,
  1505. required ? " required" : "",
  1506. fieldReadOnly ? " disabled" : ""
  1507. );
  1508. children = string.Format(
  1509. @"<div class=""prs-web_utils-digital_form-button""><input {0} value=""{1}""{3} data-true></input><span>{1}</span></div>" +
  1510. @"<div class=""prs-web_utils-digital_form-button""><input {0} value=""{2}""{4} data-false></input><span>{2}</span></div>",
  1511. options,
  1512. trueValue,
  1513. falseValue,
  1514. checkTrue ? " checked" : "",
  1515. checkFalse ? " checked" : ""
  1516. );
  1517. }
  1518. }
  1519. else if (element is DFLayoutCodeField)
  1520. {
  1521. properties = @"type=""text"" pattern=""[^a-z]+""";
  1522. classes = "prs-web_utils-digital_form-code";
  1523. value = fieldData?.ToString() ?? "";
  1524. }
  1525. else if (element is DFLayoutColorField)
  1526. {
  1527. properties = @"type=""color""";
  1528. classes = "prs-web_utils-digital_form-color";
  1529. if (fieldData != null)
  1530. try
  1531. {
  1532. var color =
  1533. (Color)ColorConverter.ConvertFromString((string)fieldData);
  1534. value = string.Format("#{0:x}{1:x}{2:x}", color.R, color.G, color.B);
  1535. }
  1536. catch (FormatException)
  1537. {
  1538. }
  1539. }
  1540. else if (element is DFLayoutDateField)
  1541. {
  1542. properties = @"type=""date""";
  1543. classes = "prs-web_utils-digital_form-date";
  1544. value = ((DateTime?)fieldData)?.ToString("yyyy-MM-dd") ?? "";
  1545. }
  1546. else if (element is DFLayoutDateTimeField)
  1547. {
  1548. properties = @"type=""datetime-local""";
  1549. classes = "prs-web_utils-digital_form-datetime";
  1550. value = fieldData != null ? string.Format("{0:yyyy-MM-dd}T{0:HH:mm}", fieldData) : "";
  1551. }
  1552. else if (element is DFLayoutDoubleField)
  1553. {
  1554. properties = @"type=""number""";
  1555. classes = "prs-web_utils-digital_form-double";
  1556. value = fieldData?.ToString() ?? "";
  1557. }
  1558. else if (element is DFLayoutIntegerField)
  1559. {
  1560. properties = @"type=""number"" step=""1""";
  1561. classes = "prs-web_utils-digital_form-integer";
  1562. value = fieldData?.ToString() ?? "";
  1563. }
  1564. else if (element is DFLayoutLookupField)
  1565. {
  1566. var type = CoreUtils.GetEntityOrNull((element as DFLayoutLookupField).Properties.LookupType);
  1567. if(type is not null)
  1568. {
  1569. var table = ClientFactory.CreateClient(type).Query(
  1570. LookupFactory.DefineFilter(type),
  1571. LookupFactory.DefineColumns(type),
  1572. LookupFactory.DefineSort(type)
  1573. );
  1574. data.TryGetValue(fieldElement.Name + "$ID", out var fieldID);
  1575. data.TryGetValue(fieldElement.Name, out var fieldFormat);
  1576. var options = table.Rows.Select(x => new Tuple<string, string?>(
  1577. LookupFactory.FormatLookup(type, x.ToDictionary(new[] { "ID" }), Array.Empty<string>()),
  1578. x["ID"].ToString())).ToList();
  1579. fieldFormat ??= options.Where(x => x.Item2 == fieldID?.ToString()).FirstOrDefault()?.Item1;
  1580. tagType = null;
  1581. stringBuilder.Append(BuildLookupField(
  1582. options,
  1583. fieldFormat?.ToString(),
  1584. style,
  1585. "prs-web_utils-digital_form-lookup prs-web_utils-digital_form-field",
  1586. fieldReadOnly,
  1587. required,
  1588. fieldElement.Name
  1589. ));
  1590. }
  1591. }
  1592. else if (element is DFLayoutMultiImage)
  1593. {
  1594. tagType = "div";
  1595. classes = "prs-web_utils-digital_form-multi_image";
  1596. var childBuild = new StringBuilder();
  1597. childBuild.Append(@"<div class=""prs-web_utils-digital_form-multi_image_cont""><div>");
  1598. if (fieldData is List<byte[]> images)
  1599. foreach (var image in images)
  1600. {
  1601. var imgStr = Convert.ToBase64String(image);
  1602. childBuild.AppendFormat(@"<img src=""data:;base64,{0}""/>", imgStr);
  1603. }
  1604. childBuild.AppendFormat(@"</div></div><div class=""prs-web_utils-digital_form-multi_image_btns"">" +
  1605. @"<div class=""prs-button prs-web_utils-digital_form-multi_image_add""{0}>Add Image</div>" +
  1606. @"<div class=""prs-button prs-web_utils-digital_form-multi_image_remove"" disabled>Remove Image</div>" +
  1607. @"</div>",
  1608. fieldReadOnly ? " disabled" : ""
  1609. );
  1610. children = childBuild.ToString();
  1611. }
  1612. else if (element is DFLayoutNotesField)
  1613. {
  1614. tagType = "div";
  1615. classes = "prs-web_utils-digital_form-notes";
  1616. var notes = (string[]?)fieldData ?? Array.Empty<string>();
  1617. var childrenBuilder = new StringBuilder(@"<div class=""prs-web_utils-digital_form-note_cont"">");
  1618. foreach (var note in notes)
  1619. childrenBuilder.AppendFormat(@"<textarea class=""prs-web_utils-digital_form-note"" disabled>{0}</textarea>", note);
  1620. var options = fieldReadOnly ? "disabled" : "";
  1621. childrenBuilder.Append(
  1622. @"<textarea class=""prs-web_utils-digital_form-note active""></textarea><div class=""prs-web_utils-digital_form-note_error""></div></div>"
  1623. + $"<div class=\"prs-web_utils-digital_form-note_add prs-button\" {options}>+</div>");
  1624. children = childrenBuilder.ToString();
  1625. }
  1626. else if (element is DFLayoutOptionField optionField)
  1627. {
  1628. var optionType = optionField.Properties.OptionType;
  1629. tagType = null;
  1630. if (optionType == DFLayoutOptionType.Radio)
  1631. {
  1632. stringBuilder.Append(BuildRadioButtons(
  1633. optionField.Properties.Options.Split(','),
  1634. fieldData?.ToString(),
  1635. style,
  1636. "prs-web_utils-digital_form-field",
  1637. fieldReadOnly,
  1638. required,
  1639. fieldElement.Name
  1640. ));
  1641. }
  1642. else
  1643. {
  1644. stringBuilder.Append(BuildLookupField(
  1645. optionField.Properties.Options.Split(',').Select(x => new Tuple<string, string?>(x, null)),
  1646. fieldData?.ToString(),
  1647. style,
  1648. "prs-web_utils-digital_form-option prs-web_utils-digital_form-field",
  1649. fieldReadOnly,
  1650. required,
  1651. fieldElement.Name
  1652. ));
  1653. }
  1654. }
  1655. else if (element is DFLayoutPasswordField)
  1656. {
  1657. properties = @"type=""password""";
  1658. classes = "prs-web_utils-digital_form-password";
  1659. value = fieldData?.ToString() ?? "";
  1660. }
  1661. else if (element is DFLayoutPINField)
  1662. {
  1663. tagType = "div";
  1664. classes = "prs-web_utils-digital_form-pin";
  1665. var pinLength = fieldElement.GetPropertyValue<int>("Length");
  1666. properties += $" data-length=\"{pinLength}\"";
  1667. var options = fieldReadOnly ? "disabled" : "";
  1668. children +=
  1669. $"<input type=\"text\" disabled value=\"{fieldData?.ToString() ?? ""}\" class=\"prs-web_utils-digital_form-pin_text\"{(required ? " required" : "")}/>"
  1670. + $"<div class=\"prs-button prs-web_utils-digital_form-create_pin\" {options}>Create</div>"
  1671. + $"<div class=\"prs-button prs-web_utils-digital_form-clear_pin\" {options}>Clear</div>";
  1672. }
  1673. else if (element is DFLayoutStringField)
  1674. {
  1675. properties = @"type=""text""";
  1676. classes = "prs-web_utils-digital_form-string";
  1677. value = fieldData?.ToString() ?? "";
  1678. }
  1679. else if (element is DFLayoutTextField)
  1680. {
  1681. tagType = "textarea";
  1682. classes = "prs-web_utils-digital_form-text";
  1683. style += "resize:none;";
  1684. children += fieldData?.ToString();
  1685. }
  1686. else if (element is DFLayoutTimeField)
  1687. {
  1688. tagType = "div";
  1689. classes = "prs-web_utils-digital_form-time";
  1690. var timeSpan = (TimeSpan?)fieldData;
  1691. var hours = Math.Truncate(timeSpan?.TotalHours ?? 0.0);
  1692. var mins = timeSpan?.Minutes ?? 0.0;
  1693. var requiredStr = required ? " required" : "";
  1694. children = $"<input class=\"prs-web_utils-digital_form-time_hours\" type=\"number\" min=\"0\" step=\"1\" value=\"{hours}\"{requiredStr}/>" +
  1695. @"<span>:</span>"
  1696. + $"<input class=\"prs-web_utils-digital_form-time_mins\" type=\"number\" min=\"0\" step=\"1\" max=\"60\" value=\"{mins}\"{requiredStr}/>";
  1697. }
  1698. else if (element is DFLayoutTimeStampField)
  1699. {
  1700. tagType = "div";
  1701. classes = "prs-web_utils-digital_form-timestamp";
  1702. var exists = fieldData != null && (DateTime)fieldData != DateTime.MinValue;
  1703. var valueStr = exists ? string.Format("{0:yyyy-MM-dd}T{0:HH:mm}", fieldData) : "";
  1704. var btnStr = exists ? "Clear" : "Set";
  1705. var disabled = fieldReadOnly ? " disabled" : "";
  1706. children +=
  1707. $"<input type=\"datetime-local\" disabled value=\"{valueStr}\" class=\"prs-web_utils-digital_form-timestamp_date\"{(required ? " required" : "")}/>"
  1708. + $"<div class=\"prs-button prs-web_utils-digital_form-toggle_stamp\"{disabled}>{btnStr}</div>";
  1709. }
  1710. else if (element is DFLayoutURLField)
  1711. {
  1712. properties = @"type=""url""";
  1713. classes = "prs-web_utils-digital_form-url";
  1714. value = fieldData?.ToString();
  1715. }
  1716. else if (element is DFLayoutEmbeddedImage || element is DFLayoutSignaturePad)
  1717. {
  1718. // TODO: Perform validation for images
  1719. tagType = "div";
  1720. classes = element is DFLayoutEmbeddedImage
  1721. ? "prs-web_utils-digital_form-embedded_image"
  1722. : "prs-web_utils-digital_form-signature";
  1723. var content = "";
  1724. var hasContent = fieldData != null && ((byte[])fieldData).Length > 0;
  1725. if (hasContent)
  1726. {
  1727. content = string.Format(
  1728. @"<img class=""prs-web_utils-digital_form-img"" src=""data:;base64,{0}"">",
  1729. Convert.ToBase64String((byte[])fieldData)
  1730. );
  1731. }
  1732. else
  1733. {
  1734. if (fieldReadOnly)
  1735. content = "<span>No image</span>";
  1736. else
  1737. content = "<span>Choose a file</span>";
  1738. }
  1739. 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>",
  1740. content,
  1741. fieldReadOnly ? " disabled" : "",
  1742. (hasContent && !fieldReadOnly) ? "" : " disabled"
  1743. );
  1744. }
  1745. if (tagType != null)
  1746. stringBuilder.Append(string.Format(
  1747. @"<{0} style=""{1}"" class=""{2} prs-web_utils-digital_form-field""{3}{4}{5} name=""{6}"" {7}>{8}</{0}>",
  1748. tagType,
  1749. style,
  1750. classes,
  1751. required ? " required" : "",
  1752. fieldReadOnly ? " disabled" : "",
  1753. value != null ? " value=\"" + value + "\"" : "",
  1754. fieldElement.Name,
  1755. properties,
  1756. children ?? ""
  1757. ));
  1758. }
  1759. else if (element is DFLayoutLabel)
  1760. {
  1761. stringBuilder.Append(string.Format(@"<label class=""prs-web_utils-digital_form-label"" style=""{0}"">{1}</label>", style,
  1762. (element as DFLayoutLabel).Caption));
  1763. }
  1764. else if (element is DFLayoutImage)
  1765. {
  1766. stringBuilder.Append(string.Format(@"<img src=""/document?id={0}"" class=""prs-web_utils-digital_form-image"" style=""{1}"">",
  1767. (element as DFLayoutImage).Image.ID, style));
  1768. }
  1769. }
  1770. if (!readOnly)
  1771. {
  1772. stringBuilder.Append($"<div class=\"prs-web_utils-digital_form-save_cont\" style=\"grid-column: 1 / -1; grid-row: {lastRowNum}\">");
  1773. if (saveLabel != null) stringBuilder.Append($"<div class=\"prs-button prs-web_utils-digital_form-save\">{saveLabel}</div>");
  1774. stringBuilder.Append($"<div class=\"prs-button prs-web_utils-digital_form-submit\">{submitLabel}</div>");
  1775. stringBuilder.Append(@"</div>");
  1776. }
  1777. stringBuilder.Append("</form>");
  1778. stringBuilder.Append(DigitalFormViewerScript);
  1779. return stringBuilder.ToString();
  1780. }
  1781. /// <summary>
  1782. /// Builds a layout for viewing and optionally editing digital forms.
  1783. /// </summary>
  1784. /// <note>
  1785. /// <para>
  1786. /// In addition to outputting an HTML representation of the form, a script tag is outputted, which provides form
  1787. /// submission functionality and some extra layouting functionality.
  1788. /// The submit form button has the class of "prs-web_utils-digital_form-submit", and when clicked calls an event
  1789. /// named "prs-web_utils-df_submit" on the root form tag itself.
  1790. /// This event is a JavaScript CustomEvent object with detail.formData set to a JSON object containing data which
  1791. /// can be sent directly to www.domain.com/form_submission/XXXXForm?id=ID, allowing for the form to be submitted.
  1792. /// </para>
  1793. /// </note>
  1794. /// <param name="form">The instance of the form</param>
  1795. /// <param name="entity">The entity associated with the form</param>
  1796. /// <param name="readOnly">If set to true, causes all elements to be disabled and the Submit Form button to be omitted</param>
  1797. /// <param name="id">An optional HTML id to add to root &lt;div&gt; of the generated item.</param>
  1798. /// <param name="classList">An optional list of HTML classes to add to root &lt;div&gt; of the generated item.</param>
  1799. /// <returns>A string containing raw HTML markup</returns>
  1800. public static string BuildDigitalFormViewer(IBaseDigitalFormInstance form, Entity entity, bool readOnly, string? id = null,
  1801. string[]? classList = null)
  1802. {
  1803. var formLayoutTable = new Client<DigitalFormLayout>().Query(
  1804. new Filter<DigitalFormLayout>(x => x.Form.ID).IsEqualTo(form.Form.ID),
  1805. new Columns<DigitalFormLayout>(x => x.Type).Add(x => x.Layout)
  1806. );
  1807. var variables = new Client<DigitalFormVariable>().Load(new Filter<DigitalFormVariable>(x => x.Form.ID).IsEqualTo(form.Form.ID));
  1808. var digitalFormLayout =
  1809. (formLayoutTable.Rows.FirstOrDefault(x => (DFLayoutType)x["Type"] == DFLayoutType.Mobile)
  1810. ?? formLayoutTable.Rows.FirstOrDefault(x => (DFLayoutType)x["Type"] == DFLayoutType.Desktop))?.ToObject<DigitalFormLayout>();
  1811. var layout = digitalFormLayout != null
  1812. ? DFLayout.FromLayoutString(digitalFormLayout.Layout)
  1813. : DFLayout.GenerateAutoMobileLayout(variables);
  1814. layout.LoadVariables(variables);
  1815. var data = DigitalForm.ParseFormData(form.FormData, variables, entity);
  1816. return BuildDigitalFormViewer(layout, data, readOnly, id, classList);
  1817. }
  1818. #endregion
  1819. }
  1820. public abstract class WebTemplateBase<T> : TemplateBase<T>
  1821. {
  1822. public IEncodedString JSON<U>() where U : Entity, new()
  1823. {
  1824. return new RawString(WebUtils.CreateJSONEntity<U>());
  1825. }
  1826. public IEncodedString JSON<U>(U entity) where U : Entity
  1827. {
  1828. return new RawString(WebUtils.EntityToJSON(entity));
  1829. }
  1830. public IEncodedString Get<U>(string entity, Expression<Func<U, object>> expression) where U : Entity
  1831. {
  1832. return new RawString(WebUtils.JSONGetProperty(entity, expression));
  1833. }
  1834. public IEncodedString Set<U>(string entity, Expression<Func<U, object>> expression, string value) where U : Entity
  1835. {
  1836. return new RawString(WebUtils.JSONSetProperty(entity, expression, value));
  1837. }
  1838. }
  1839. }