From f4e3a9ed83e5fdca01de488eae381d869b9aa509 Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 8 Aug 2024 03:44:38 +0330 Subject: [PATCH] JingetJsonVisualizer component added --- .../Other/JingetJsonVisualizer.razor | 77 ++++++++ .../Components/Popup/JingetMessageBox.razor | 15 +- .../Jinget.Blazor/Jinget.Blazor.csproj | 1 - 04-Presentation/Jinget.Blazor/README.md | 53 +++-- .../Jinget.Blazor/wwwroot/css/jinget.core.css | 1 + .../wwwroot/css/jinget.custom.css | 17 -- .../wwwroot/css/jinget.json.visualizer.css | 65 ++++++ .../Jinget.Blazor/wwwroot/js/jinget.core.js | 11 +- .../Jinget.Blazor/wwwroot/js/jinget.custom.js | 29 ++- .../wwwroot/js/jinget.json.visualizer.js | 185 ++++++++++++++++++ Tests/Jinget.Blazor.Test/Components/App.razor | 1 - .../Components/Layout/NavMenu.razor | 5 + .../Pages/Other/JsonVisualizer.razor | 20 ++ 13 files changed, 436 insertions(+), 44 deletions(-) create mode 100644 04-Presentation/Jinget.Blazor/Components/Other/JingetJsonVisualizer.razor create mode 100644 04-Presentation/Jinget.Blazor/wwwroot/css/jinget.json.visualizer.css create mode 100644 04-Presentation/Jinget.Blazor/wwwroot/js/jinget.json.visualizer.js create mode 100644 Tests/Jinget.Blazor.Test/Components/Pages/Other/JsonVisualizer.razor diff --git a/04-Presentation/Jinget.Blazor/Components/Other/JingetJsonVisualizer.razor b/04-Presentation/Jinget.Blazor/Components/Other/JingetJsonVisualizer.razor new file mode 100644 index 0000000..b69a980 --- /dev/null +++ b/04-Presentation/Jinget.Blazor/Components/Other/JingetJsonVisualizer.razor @@ -0,0 +1,77 @@ +@using System.Text.Json + +@inject IJSRuntime JS; + +

+
+@code {
+    [Parameter]
+    public string Id { get; set; } = "json-visualizer-" + Guid.NewGuid().ToString("N");
+
+    /// 
+    /// All nodes are collapsed at html generation. default is false.
+    /// 
+    [Parameter] public bool Collapsed { get; set; } = true;
+
+    /// 
+    /// Allow root element to be collapsed. default is true.
+    /// 
+    [Parameter] public bool RootCollapsable { get; set; } = false;
+
+    /// 
+    /// All JSON keys are surrounded with double quotation marks ({"foobar": 1} instead of {foobar: 1}). default is false.
+    /// 
+    [Parameter] public bool WithQuotes { get; set; } = false;
+
+    /// 
+    /// All values that are valid links will be clickable, if false they will only be strings. default is true.
+    /// 
+    [Parameter] public bool WithLinks { get; set; } = true;
+
+    /// 
+    /// Support different libraries for big numbers,
+    /// if true, display the real number only, false shows object containing big number with all fields instead of number only.
+    /// 
+    [Parameter] public bool BigNumbers { get; set; } = false;
+
+    DotNetObjectReference? dotnet;
+
+    protected override async Task OnAfterRenderAsync(bool firstRender)
+    {
+        if (firstRender)
+        {
+            dotnet = DotNetObjectReference.Create(this);
+        }
+        await base.OnAfterRenderAsync(firstRender);
+    }
+
+    /// 
+    /// renders the given object in json visualizer
+    /// 
+    public async Task Visualize(object data)
+    {
+        await Visualize(JsonSerializer.Serialize(data, new JsonSerializerOptions
+            {
+                ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
+            }));
+    }
+
+    /// 
+    /// renders the given string in json visualizer
+    /// 
+    public async Task Visualize(string data)
+    {
+        await JS.InvokeVoidAsync("toJsonVisualizer",
+        new
+        {
+            dotnet,
+            Id,
+            data,
+            Collapsed,
+            RootCollapsable,
+            WithQuotes,
+            WithLinks,
+            BigNumbers
+        });
+    }
+}
\ No newline at end of file
diff --git a/04-Presentation/Jinget.Blazor/Components/Popup/JingetMessageBox.razor b/04-Presentation/Jinget.Blazor/Components/Popup/JingetMessageBox.razor
index 086b410..36ad849 100644
--- a/04-Presentation/Jinget.Blazor/Components/Popup/JingetMessageBox.razor
+++ b/04-Presentation/Jinget.Blazor/Components/Popup/JingetMessageBox.razor
@@ -1,8 +1,8 @@
 @using Jinget.Blazor.Components.List
+@using Jinget.Blazor.Components.Other
 @using MudBlazor
 @using Newtonsoft.Json.Linq
 @using Jinget.Blazor.Components.Popup
-@using Texnomic.Blazor.JsonViewer
 
 @inherits JingetPopupBase
 
@@ -24,7 +24,7 @@
             @Content
         }
 
-        
+        
         
         
@@ -33,7 +33,7 @@
 
 @code {
     JingetModal modal { get; set; } = new JingetModal();
-    JsonViewer? JsonViewer { get; set; }
+    JingetJsonVisualizer? JsonVisualizer { get; set; }
 
     string? Title { get; set; }
     string? Content { get; set; }
@@ -58,6 +58,7 @@
         Severity theme = Severity.Warning,
         bool isFancy = true)
     {
+
         this.Title = title;
         this.Content = content;
         if (verboseItems != null)
@@ -82,15 +83,15 @@
                 else
                 if (Jinget.Core.Utilities.Json.JsonUtility.IsValid(verboseContent))
                 {
-                    if (JsonViewer != null)
-                        await JsonViewer.Render(verboseContent);
+                    if (JsonVisualizer != null)
+                        await JsonVisualizer.Visualize(verboseContent);
                 }
                 else
                 {
                     try
                     {
-                        if (JsonViewer != null)
-                            await JsonViewer.Render(verboseContent);
+                        if (JsonVisualizer != null)
+                            await JsonVisualizer.Visualize(verboseContent);
                     }
                     catch { }
                     verboseItems.Add(new ListItem(verboseContent, "list-group-item list-group-item-danger"));
diff --git a/04-Presentation/Jinget.Blazor/Jinget.Blazor.csproj b/04-Presentation/Jinget.Blazor/Jinget.Blazor.csproj
index 78ef023..f87444c 100644
--- a/04-Presentation/Jinget.Blazor/Jinget.Blazor.csproj
+++ b/04-Presentation/Jinget.Blazor/Jinget.Blazor.csproj
@@ -32,7 +32,6 @@
 	
 		
 		
-		
 	
 
 	
diff --git a/04-Presentation/Jinget.Blazor/README.md b/04-Presentation/Jinget.Blazor/README.md
index 9bdd8c4..98c6f3f 100644
--- a/04-Presentation/Jinget.Blazor/README.md
+++ b/04-Presentation/Jinget.Blazor/README.md
@@ -13,6 +13,7 @@ Blazor components optimised for RTL languaged such as Farsi, Arabic etc.
 - [x] **JingetDropDownList** 
 - [x] **JingetDropDownListTree** 
 - [x] **Input** 
+- [x] **JsonVisualizer** 
 
 **Services list:**
 - [x] **LocalStorage**
@@ -31,17 +32,14 @@ You can also use other methods supported by NuGet. Check [Here](https://www.nuge
 Register the required services in your `Program.cs` file:
 
 ```
-
 builder.Services.AddJingetBlazor();
-
 ```
 
 If you need to use `TokenAuthenticationStateProvider` as a provider for handling authentication state(for example in your ASP.NET Core Blazor project), 
 then you need to pass `addAuthenticationStateProvider=true` to `AddJingetBlazor`.
 If you need to use token storage services for storing/retrieving your tokens, then you need to pass `tokenConfigModel` and `addTokenRelatedServices=true` to `AddJingetBlazor`.
 If you do not need to use token storage services, but you want to register `TokenConfigModel` then you need to pass `tokenConfigModel` and `addTokenRelatedServices=false` to `AddJingetBlazor`.
-If you have no plan to use `Jinget.Blazor` components and you just want to use its services, 
-then you can pass `addComponents=false` to `AddJingetBlazor` method.
+If you have no plan to use `Jinget.Blazor` components and you just want to use its services, then you can pass `addComponents=false` to `AddJingetBlazor` method.
 
 
 ## How to Use Methods:
@@ -52,7 +50,6 @@ If you want to authenticate the user and manage user's state via `TokenAuthentic
 to your login form and authenticate the user:
 
 ```
-
 @attribute [AllowAnonymous]
 @layout LoginLayout
 @page "/login"
@@ -109,12 +106,6 @@ Install `MudBlazor` and add reference to `MudBlazor.min.js` in your `_Host.razor
 
 ```
 
-Install `Texnomic.Blazor.JsonViewer` and add reference to `jsonViewer.js` in your `_Host.razor` or `App.razor` files.
-
-```
-
-```
-
 Add reference to `jinget.core.js` in your `_Host.razor` or `App.razor` files.
 
 ```
@@ -761,6 +752,8 @@ public async Task> GetStatusAsync(string token, ob
 
 `JingetTableElement`: Render a JingetTable on the page.(Used in JingetTable component, for more info go to JingetTable component section)
 
+------------
+
 **Jinget DropDownList components**
 
 Add the `JingetDropDownList` to your page and start using it;-)
@@ -830,6 +823,8 @@ To avoid this problem, you may attach the dropdown to the modal itself by settin
 
 `SetSelectedIndexAsync`: Select item in dropdownlist based on the index in `Items`. Index starts from zero(0).
 
+------------
+
 **Jinget DropDownList Tree components**
 
 Add the `JingetDropDownListTree` to your page and start using it;-)
@@ -856,6 +851,8 @@ Other Parameters/properties/callbacks and methods for this components are exactl
 Except that `JingetDropDownList` uses `JingetDropDownItemModel` as it's data model provider class which contains `Value` and `Text` properties.
 But `JingetDropDownListTree` uses `JingetDropDownTreeItemModel` as data model provider which in addition to `JingetDropDownItemModel` properties, also contains `ParentValue` property to construct the tree structure
 
+------------
+
 **Jinget Input**
 
 Add the `JingetInput` to your page and start using it;-)
@@ -895,11 +892,41 @@ Add the `JingetInput` to your page and start using it;-)
 
 `OnChange`: Fires a callback whenever the selected item changed.
 
+------------
+
+**Jinget JsonVisualizer component**
+
+Add the `JingetJsonVisualizer` to your page and start using it;-)
+
+*Note: that this component is based on [Alexandre Bodelot](https://github.com/abodelot "Alexandre Bodelot") jquery.json-viewer library.*  [View on GitHub](htthttps://github.com/abodelot/jquery.json-viewerp:// "View on GitHub")
+
+```
+
+```
+
+***Parameters:***
+
+`Id`: Unique identifier for component in page. This parameter is required.
+
+`Collapsed`: All nodes are collapsed at html generation. default is false.
+
+`RootCollapsable`: Allow root element to be collapsed. default is true.
+
+`WithQuotes`: All JSON keys are surrounded with double quotation marks ({"foobar": 1} instead of {foobar: 1}). default is false.
+
+`WithLinks`: All values that are valid links will be clickable, if false they will only be strings. default is true.
+
+`BigNumbers`: Support different libraries for big numbers, if true, display the real number only, 
+false shows object containing big number with all fields instead of number only.
+
+***Methods:***
+
+`Visualize`: Renders the given object in json visualizer
 
 ------------
-**Services**
+## How to Use Services:
 
-***LocalStorage/SessionStorage:***
+**LocalStorage/SessionStorage:**
 
 If you need to work with `localStorage` then you need to inject `ILocalStorageService` and 
 if you want to work with `sessionStorage` then you need to inject `ISessionStorageService` to your page or classes.
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.core.css b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.core.css
index 1e74f04..dd9352c 100644
--- a/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.core.css
+++ b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.core.css
@@ -11,3 +11,4 @@
 @import url('jinget.select2.css');
 @import url('jinget.select2.theme.material.css');
 @import url('jinget.custom.css');
+@import url('jinget.json.visualizer.css');
\ No newline at end of file
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.custom.css b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.custom.css
index 10862bb..971dc87 100644
--- a/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.custom.css
+++ b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.custom.css
@@ -27,16 +27,6 @@
     margin-right: auto !important;
 }
 
-
-/*.select2-container .select2-selection--single .select2-selection__arrow {
-    position: absolute !important;
-    left: 0px !important;
-}*/
-
-
-/*.select2-container .select2-selection--single .select2-selection__rendered {
-    padding-left: 20px !important;
-}*/
 /*Select2.js END*/
 
 /*MudBlazor START*/
@@ -65,10 +55,3 @@
     direction: ltr !important;
 }
 /*MudBlazor END*/
-
-/*JsonViewer START*/
-json-viewer {
-    direction: ltr !important;
-    overflow-wrap: break-word !important;
-}
-/*JsonViewer END*/
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.json.visualizer.css b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.json.visualizer.css
new file mode 100644
index 0000000..3da7fe6
--- /dev/null
+++ b/04-Presentation/Jinget.Blazor/wwwroot/css/jinget.json.visualizer.css
@@ -0,0 +1,65 @@
+/* Root element */
+.json-document {
+    padding: 1em 2em;
+}
+
+/* Syntax highlighting for JSON objects */
+ul.json-dict, ol.json-array {
+    list-style-type: none;
+    margin: 0 0 0 1px;
+    border-left: 1px dotted #ccc;
+    padding-left: 2em;
+}
+
+.json-string {
+    color: #0B7500;
+    white-space: break-spaces;
+}
+
+.json-literal {
+    color: #1A01CC;
+    font-weight: bold;
+}
+
+/* Toggle button */
+a.json-toggle {
+    position: relative;
+    color: inherit;
+    text-decoration: none;
+}
+
+    a.json-toggle:focus {
+        outline: none;
+    }
+
+    a.json-toggle:before {
+        font-size: 1.1em;
+        color: #c0c0c0;
+        content: "\25BC"; /* down arrow */
+        position: absolute;
+        display: inline-block;
+        width: 1em;
+        text-align: center;
+        line-height: 1em;
+        left: -1.2em;
+    }
+
+    a.json-toggle:hover:before {
+        color: #aaa;
+    }
+
+    a.json-toggle.collapsed:before {
+        /* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
+        transform: rotate(-90deg);
+    }
+
+/* Collapsable placeholder links */
+a.json-placeholder {
+    color: #aaa;
+    padding: 0 1em;
+    text-decoration: none;
+}
+
+    a.json-placeholder:hover {
+        text-decoration: underline;
+    }
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.core.js b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.core.js
index 933787f..eb83052 100644
--- a/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.core.js
+++ b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.core.js
@@ -18,13 +18,16 @@
 loadScript({
     url: '_content/Jinget.Blazor/js/jinget.select2.js',
     callback: loadScript({
-        url: '_content/Jinget.Blazor/js/jinget.jalali.picker.date.js',
+        url: '_content/Jinget.Blazor/js/jinget.json.visualizer.js',
         callback: loadScript({
-            url: '_content/Jinget.Blazor/js/jinget.custom.js',
+            url: '_content/Jinget.Blazor/js/jinget.jalali.picker.date.js',
             callback: loadScript({
-                url: '_content/Jinget.Blazor/js/jinget.select2ext.js',
+                url: '_content/Jinget.Blazor/js/jinget.custom.js',
                 callback: loadScript({
-                    url: '_content/Jinget.Blazor/js/jinget.select2tree.js'
+                    url: '_content/Jinget.Blazor/js/jinget.select2ext.js',
+                    callback: loadScript({
+                        url: '_content/Jinget.Blazor/js/jinget.select2tree.js'
+                    })
                 })
             })
         })
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.custom.js b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.custom.js
index 405bb6e..d5300c5 100644
--- a/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.custom.js
+++ b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.custom.js
@@ -1,4 +1,31 @@
-/*select2 START*/
+/*jinget.json.visualizer START*/
+window.toJsonVisualizer = (params = {
+    id, data, collapsed=false, rootCollapsable=true, withQuotes=false, withLinks=true, bigNumbers=false
+} = {}) => {
+    $('#' + params.id).jsonVisualizer(
+        params.data, {
+
+        //all nodes are collapsed at html generation
+        collapsed: params.collapsed,
+
+        //allow root element to be collasped
+        rootCollapsable: params.rootCollapsable,
+
+        //all JSON keys are surrounded with double quotation marks
+        withQuotes: params.withQuotes,
+
+        //all values that are valid links will be clickable
+        withLinks: params.withLinks,
+
+        //support different libraries for big numbers,
+        //if true display the real number only, false shows object containing big number with all fields instead of number only.
+        bigNumbers: params.bigNumbers
+    });
+}
+
+/*jinget.json.visualizer END*/
+
+/*select2 START*/
 window.initJingetDropDownList = (params = {
     dotnet, id, isSearchable = false, isRtl = true, noResultText='Nothing to display!',
     searchPlaceholderText='', parentElementId=''
diff --git a/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.json.visualizer.js b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.json.visualizer.js
new file mode 100644
index 0000000..3cd17ee
--- /dev/null
+++ b/04-Presentation/Jinget.Blazor/wwwroot/js/jinget.json.visualizer.js
@@ -0,0 +1,185 @@
+/**
+ * jQuery json-viewer
+ * @author: Alexandre Bodelot 
+ * @link: https://github.com/abodelot/jquery.json-viewer
+ */
+(function ($) {
+
+    /**
+     * Check if arg is either an array with at least 1 element, or a dict with at least 1 key
+     * @return boolean
+     */
+    function isCollapsable(arg) {
+        return arg instanceof Object && Object.keys(arg).length > 0;
+    }
+
+    /**
+     * Check if a string looks like a URL, based on protocol
+     * This doesn't attempt to validate URLs, there's no use and syntax can be too complex
+     * @return boolean
+     */
+    function isUrl(string) {
+        var protocols = ['http', 'https', 'ftp', 'ftps'];
+        for (var i = 0; i < protocols.length; ++i) {
+            if (string.startsWith(protocols[i] + '://')) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the input string html escaped
+     * @return string
+     */
+    function htmlEscape(s) {
+        return s.replace(/&/g, '&')
+            .replace(//g, '>')
+            .replace(/'/g, ''')
+            .replace(/"/g, '"');
+    }
+
+    /**
+     * Transform a json object into html representation
+     * @return string
+     */
+    function json2html(json, options) {
+        var html = '';
+        if (typeof json === 'string') {
+            // Escape tags and quotes
+            json = htmlEscape(json);
+
+            if (options.withLinks && isUrl(json)) {
+                html += '' + json + '';
+            } else {
+                // Escape double quotes in the rendered non-URL string.
+                json = json.replace(/"/g, '\\"');
+                html += '"' + json + '"';
+            }
+        } else if (typeof json === 'number' || typeof json === 'bigint') {
+            html += '' + json + '';
+        } else if (typeof json === 'boolean') {
+            html += '' + json + '';
+        } else if (json === null) {
+            html += 'null';
+        } else if (json instanceof Array) {
+            if (json.length > 0) {
+                html += '[
    '; + for (var i = 0; i < json.length; ++i) { + html += '
  1. '; + // Add toggle button if item is collapsable + if (isCollapsable(json[i])) { + html += ''; + } + html += json2html(json[i], options); + // Add comma if item is not last + if (i < json.length - 1) { + html += ','; + } + html += '
  2. '; + } + html += '
]'; + } else { + html += '[]'; + } + } else if (typeof json === 'object') { + // Optional support different libraries for big numbers + // json.isLosslessNumber: package lossless-json + // json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others? + if (options.bigNumbers && (typeof json.toExponential === 'function' || json.isLosslessNumber)) { + html += '' + json.toString() + ''; + } else { + var keyCount = Object.keys(json).length; + if (keyCount > 0) { + html += '{
    '; + for (var key in json) { + if (Object.prototype.hasOwnProperty.call(json, key)) { + // define a parameter of the json value first to prevent get null from key when the key changed by the function `htmlEscape(key)` + let jsonElement = json[key]; + key = htmlEscape(key); + var keyRepr = options.withQuotes ? + '"' + key + '"' : key; + + html += '
  • '; + // Add toggle button if item is collapsable + if (isCollapsable(jsonElement)) { + html += '' + keyRepr + ''; + } else { + html += keyRepr; + } + html += ': ' + json2html(jsonElement, options); + // Add comma if item is not last + if (--keyCount > 0) { + html += ','; + } + html += '
  • '; + } + } + html += '
}'; + } else { + html += '{}'; + } + } + } + return html; + } + + /** + * jQuery plugin method + * @param json: a javascript object + * @param options: an optional options hash + */ + $.fn.jsonVisualizer = function (json, options) { + // Merge user options with default options + options = Object.assign({}, { + collapsed: false, + rootCollapsable: true, + withQuotes: false, + withLinks: true, + bigNumbers: false + }, options); + + // jQuery chaining + return this.each(function () { + try { + json = JSON.parse(json); + } catch { } + // Transform to HTML + var html = json2html(json, options); + if (options.rootCollapsable && isCollapsable(json)) { + html = '' + html; + } + + // Insert HTML in target DOM element + $(this).html(html); + $(this).addClass('json-document'); + + // Bind click on toggle buttons + $(this).off('click'); + $(this).on('click', 'a.json-toggle', function () { + var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); + target.toggle(); + if (target.is(':visible')) { + target.siblings('.json-placeholder').remove(); + } else { + var count = target.children('li').length; + var placeholder = count + (count > 1 ? ' items' : ' item'); + target.after('' + placeholder + ''); + } + return false; + }); + + // Simulate click on toggle button when placeholder is clicked + $(this).on('click', 'a.json-placeholder', function () { + $(this).siblings('a.json-toggle').click(); + return false; + }); + + if (options.collapsed == true) { + // Trigger click to collapse all nodes + $(this).find('a.json-toggle').click(); + } + }); + }; +})(jQuery); \ No newline at end of file diff --git a/Tests/Jinget.Blazor.Test/Components/App.razor b/Tests/Jinget.Blazor.Test/Components/App.razor index 39da223..3bb42c2 100644 --- a/Tests/Jinget.Blazor.Test/Components/App.razor +++ b/Tests/Jinget.Blazor.Test/Components/App.razor @@ -26,7 +26,6 @@ - diff --git a/Tests/Jinget.Blazor.Test/Components/Layout/NavMenu.razor b/Tests/Jinget.Blazor.Test/Components/Layout/NavMenu.razor index a20c2d6..1b90bd4 100644 --- a/Tests/Jinget.Blazor.Test/Components/Layout/NavMenu.razor +++ b/Tests/Jinget.Blazor.Test/Components/Layout/NavMenu.razor @@ -69,6 +69,11 @@ Dynamic Form +