diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor
new file mode 100644
index 0000000000..9d9802a461
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor
@@ -0,0 +1,11 @@
+@namespace Bit.BlazorUI
+@inherits BitComponentBase
+
+
+
+
\ No newline at end of file
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor.cs
new file mode 100644
index 0000000000..bff1b45cbb
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.razor.cs
@@ -0,0 +1,37 @@
+namespace Bit.BlazorUI;
+
+///
+/// BitMarkdownEditor is a simple editor like GitHub md editor.
+///
+public partial class BitMarkdownEditor : BitComponentBase
+{
+ private ElementReference _textAreaRef = default!;
+
+
+
+ [Inject] private IJSRuntime _js { get; set; } = default!;
+
+
+
+ ///
+ /// Returns the current value of the editor.
+ ///
+ public async ValueTask GetValue()
+ {
+ return await _js.BitMarkdownEditorGetValue(_Id);
+ }
+
+
+
+ protected override string RootElementClass => "bit-mde";
+
+ protected override Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _js.BitMarkdownEditorInit(_Id, _textAreaRef);
+ }
+
+ return base.OnAfterRenderAsync(firstRender);
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.scss
new file mode 100644
index 0000000000..3220996f8b
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.scss
@@ -0,0 +1,12 @@
+@import '../../../Bit.BlazorUI/Styles/functions.scss';
+
+.bit-mde {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.bit-mde-txa {
+ width: 100%;
+ height: 100%;
+ padding: spacing(1);
+}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.ts
new file mode 100644
index 0000000000..0e766c5a7d
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditor.ts
@@ -0,0 +1,351 @@
+namespace BitBlazorUI {
+ type Content = {
+ value: string;
+ type: "inline" | "block" | "wrap";
+ };
+
+ export class MarkdownEditor {
+ private static _editors: { [key: string]: MarkdownEditor } = {};
+
+ public static init(id: string, textArea: HTMLTextAreaElement) {
+ const editor = new MarkdownEditor(textArea);
+
+ MarkdownEditor._editors[id] = editor;
+ }
+
+ public static getValue(id: string) {
+ const editor = MarkdownEditor._editors[id];
+ if (!editor) return;
+
+ return editor.value;
+ }
+
+ #opens: string[] = [];
+
+ #pairs: { [key: string]: string } = {
+ "(": ")",
+ "{": "}",
+ "[": "]",
+ "<": ">",
+ '"': '"',
+ "`": "`",
+ };
+
+ #textArea: HTMLTextAreaElement;
+
+ constructor(textArea: HTMLTextAreaElement) {
+ this.#textArea = textArea;
+
+ this.#init();
+ }
+
+ get value() {
+ return this.#textArea.value;
+ }
+
+ set value(value) {
+ this.#textArea.value = value;
+ }
+
+ get #block() {
+ const codeBlocks = this.value.split("```");
+ let total = 0;
+ for (const [i, b] of codeBlocks.entries()) {
+ total += b.length + 3;
+ if (this.#start < total) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ get #end() {
+ return this.#textArea.selectionEnd;
+ }
+
+ get #start() {
+ return this.#textArea.selectionStart;
+ }
+
+ #set(start: number, end: number) {
+ this.#textArea.setSelectionRange(start, end);
+ }
+
+ #getLine() {
+ const total = this.value.split("\n");
+ let count = 0;
+ for (let i = 0; i < total.length; i++) {
+ const length = total.at(i)?.length ?? 0;
+ count++;
+ count += length;
+ if (count > this.#end) {
+ return {
+ total,
+ num: i,
+ col: this.#end - (count - length - 1),
+ };
+ }
+ }
+ return { total, num: 0, col: 0 };
+ }
+
+ #insert(
+ element: Content,
+ start: number,
+ end: number,
+ ) {
+ if (element.type === "inline") {
+ this.value = `${this.value.slice(0, end)}${element.value}${this.value.slice(end)}`;
+ } else if (element.type === "wrap") {
+ this.value = insert(this.value, element.value, start);
+ this.value = insert(
+ this.value,
+ this.#pairs[element.value] as string,
+ end + element.value.length,
+ );
+ if (element.value.length < 2) this.#opens.push(element.value);
+ } else if (element.type === "block") {
+ const { total, num } = this.#getLine();
+ const first = element.value.at(0);
+ if (first && total[num]?.startsWith(first)) {
+ total[num] = element.value.trim() + total[num];
+ } else {
+ total[num] = element.value + total[num];
+ }
+ this.value = total.join("\n");
+ }
+ }
+
+ #setCaret(
+ text: string,
+ start: number,
+ end: number,
+ ) {
+ let startPos = 0;
+ let endPos = 0;
+ if (/[a-z]/i.test(text)) {
+ for (let i = end; i < this.value.length; i++) {
+ if (this.value[i]?.match(/[a-z]/i)) {
+ if (!startPos) {
+ startPos = i;
+ } else {
+ endPos = i + 1;
+ }
+ } else if (startPos) {
+ break;
+ }
+ }
+ } else {
+ startPos = start + text.length;
+ endPos = end + text.length;
+ }
+
+ this.#set(startPos, endPos);
+ this.#textArea.focus();
+ }
+
+ #add(element: Content) {
+ const end = this.#end;
+ const start = this.#start;
+
+ this.#insert(element, start, end);
+ this.#setCaret(element.value, start, end);
+ }
+
+ #getLists(str: string | undefined) {
+ if (!str) return;
+
+ if (startsWithDash(str)) {
+ return '- ';
+ }
+
+ const listNum = startsWithNumber(str);
+ if (listNum) return `${listNum}. `;
+ }
+
+ #correct(cur: number, isDec = false) {
+ const { total } = this.#getLine();
+ for (let i = cur + 1; i < total.length; i++) {
+ const l = total[i];
+ if (!l) continue;
+
+ if (startsWithDash(l)) {
+ if (l.length > 2) {
+ total[i] = l;
+ } else {
+ continue;
+ }
+ } else {
+ const number = startsWithNumber(l);
+ if (!number) {
+ break;
+ } else {
+ let newNumber: number;
+ if (isDec) {
+ if (number > 1) {
+ newNumber = number - 1;
+ } else {
+ break;
+ }
+ } else {
+ newNumber = number + 1;
+ }
+ total[i] = l.slice(String(number).length);
+ total[i] = String(newNumber) + total[i];
+ }
+ }
+ }
+ this.value = total.join("\n");
+ }
+
+ #init() {
+ this.#textArea.addEventListener("keydown", async (e) => {
+ const reseters = ["Delete", "ArrowUp", "ArrowDown"];
+ const next = this.value[this.#end] ?? "";
+ if (reseters.includes(e.key)) {
+ this.#opens = [];
+ } else if (e.key === "Backspace") {
+ const prev = this.value[this.#start - 1];
+ if (
+ prev &&
+ prev in this.#pairs &&
+ next === this.#pairs[prev]
+ ) {
+ e.preventDefault();
+ const start = this.#start - 1;
+ const end = this.#end - 1;
+ this.value = remove(this.value, start);
+ this.value = remove(this.value, end);
+ setTimeout(() => {
+ this.#set(start, end);
+ }, 0);
+ this.#opens.pop();
+ }
+ if (prev === "\n" && this.#start === this.#end) {
+ e.preventDefault();
+ const pos = this.#start - 1;
+ const { num } = this.#getLine();
+ this.#correct(num, true);
+ this.value = remove(this.value, pos);
+ setTimeout(async () => {
+ this.#set(pos, pos);
+ }, 0);
+ }
+ } else if (e.key === "Tab") {
+ if (this.#block % 2 !== 0) {
+ e.preventDefault();
+ this.#add({
+ type: "inline",
+ value: "\t",
+ });
+ }
+ } else if (e.key === "Enter") {
+ const { total, num, col } = this.#getLine();
+ const line = total.at(num);
+ let rep = this.#getLists(line);
+ const orig = rep;
+
+ const n = startsWithNumber(rep);
+ if (n) rep = `${n + 1}. `;
+
+ if (rep && (orig && orig.length < col)) {
+ e.preventDefault();
+ if (n) this.#correct(num);
+ this.#add({
+ type: "inline",
+ value: `\n${rep}`,
+ });
+ } else if (rep && (orig && orig.length === col)) {
+ e.preventDefault();
+
+ const origEnd = this.#end;
+ const pos = origEnd - orig.length;
+
+ for (let i = 0; i < orig.length; i++) {
+ this.value = remove(this.value, origEnd - (i + 1));
+ }
+
+ setTimeout(async () => {
+ this.#set(pos, pos);
+ this.#textArea.focus();
+ this.#add({
+ type: "inline",
+ value: `\n`,
+ });
+ }, 0);
+ }
+ } else {
+ const nextIsPaired = Object.values(this.#pairs).includes(next);
+ const isSelected = this.#start !== this.#end;
+ if (e.ctrlKey || e.metaKey) {
+ if (this.#start === this.#end) {
+ if (e.key === "c" || e.key === "x") {
+ e.preventDefault();
+ const { total, num, col } = this.#getLine();
+
+ await navigator.clipboard.writeText(`${num === 0 && e.key === "x" ? "" : "\n"}${total[num]}`);
+
+ if (e.key === "x") {
+ const pos = this.#start - col;
+ total.splice(num, 1);
+ this.value = total.join("\n");
+ setTimeout(() => this.#set(pos, pos), 0);
+ }
+ }
+ }
+ }
+
+ if ((e.ctrlKey || e.metaKey) && e.key) {
+ // TODO: handle shortkeys for example
+ } else if (
+ nextIsPaired &&
+ (next === e.key || e.key === "ArrowRight") &&
+ this.#opens.length &&
+ !isSelected
+ ) {
+ e.preventDefault();
+ this.#set(this.#start + 1, this.#end + 1);
+ this.#opens.pop();
+ } else if (e.key in this.#pairs) {
+ e.preventDefault();
+ this.#add({
+ type: "wrap",
+ value: e.key,
+ });
+ this.#opens.push(e.key);
+ }
+ }
+ });
+
+ this.#textArea.addEventListener("dblclick", () => {
+ if (this.#start !== this.#end) {
+ if (this.value[this.#start] === " ") {
+ this.#set(this.#start + 1, this.#end);
+ }
+ if (this.value[this.#end - 1] === " ") {
+ this.#set(this.#start, this.#end - 1);
+ }
+ }
+ });
+
+ this.#textArea.addEventListener("click", () => (this.#opens = []));
+ }
+ }
+
+ const startsWithDash = (str: string | undefined) => {
+ return !!(str?.startsWith('- '));
+ };
+
+ const startsWithNumber = (str: string | undefined) => {
+ const result = str?.match(/^(\d+)\./);
+ return result ? Number(result[1]) : null;
+ };
+
+ const insert = (str: string, char: string, index: number) => {
+ return str.slice(0, index) + char + str.slice(index);
+ };
+
+ const remove = (str: string, index: number) => {
+ return str.slice(0, index) + str.slice(index + 1);
+ };
+}
\ No newline at end of file
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditorJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditorJsRuntimeExtensions.cs
new file mode 100644
index 0000000000..795bce58c9
--- /dev/null
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownEditor/BitMarkdownEditorJsRuntimeExtensions.cs
@@ -0,0 +1,14 @@
+namespace Bit.BlazorUI;
+
+internal static class BitMarkdownEditorJsRuntimeExtensions
+{
+ public static ValueTask BitMarkdownEditorInit(this IJSRuntime jsRuntime, string id, ElementReference element)
+ {
+ return jsRuntime.InvokeVoid("BitBlazorUI.MarkdownEditor.init", id, element);
+ }
+
+ public static ValueTask BitMarkdownEditorGetValue(this IJSRuntime jsRuntime, string id)
+ {
+ return jsRuntime.Invoke("BitBlazorUI.MarkdownEditor.getValue", id);
+ }
+}
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
index a58e1aa4f7..a5be6a9d53 100644
--- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
+++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/extra-components.scss
@@ -3,6 +3,7 @@
@import "../Components/DataGrid/Pagination/BitDataGridPaginator.scss";
@import "../Components/ErrorBoundary/BitErrorBoundary.scss";
@import "../Components/InfiniteScrolling/BitInfiniteScrolling.scss";
+@import "../Components/MarkdownEditor/BitMarkdownEditor.scss";
@import "../Components/MarkdownViewer/BitMarkdownViewer.scss";
@import "../Components/MessageBox/BitMessageBox.scss";
@import "../Components/NavPanel/BitNavPanel.scss";
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs
index 3f496563bf..eea0bb5368 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs
@@ -78,7 +78,7 @@ public BitDir? Dir
public override Task SetParametersAsync(ParameterView parameters)
{
HtmlAttributes.Clear();
- var parametersDictionary = ParametersCache ?? throw new InvalidOperationException();
+ var parametersDictionary = ParametersCache ?? new Dictionary(parameters.ToDictionary());
foreach (var parameter in parametersDictionary!)
{
switch (parameter.Key)
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor
new file mode 100644
index 0000000000..98ce765a56
--- /dev/null
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor
@@ -0,0 +1,21 @@
+@page "/components/markdowneditor"
+
+
+
+
+
+ Get Value
+
+
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.cs
new file mode 100644
index 0000000000..743aa373b8
--- /dev/null
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.cs
@@ -0,0 +1,35 @@
+namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.MarkdownEditor;
+
+public partial class BitMarkdownEditorDemo
+{
+ private readonly List componentParameters =
+ [
+ ];
+
+
+
+ private BitMarkdownEditor editorRef = default!;
+ private string? value;
+ private async Task GetValue()
+ {
+ value = await editorRef.GetValue();
+ }
+
+
+
+ private readonly string example1RazorCode = @"
+Get Value
+";
+ private readonly string example1CsharpCode = @"
+private BitMarkdownEditor editorRef = default!;
+private string? value;
+private async Task GetValue()
+{
+ value = await editorRef.GetValue();
+}";
+}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.scss
new file mode 100644
index 0000000000..e4c5a61f90
--- /dev/null
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.scss
@@ -0,0 +1,2 @@
+::deep {
+}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor
index d496fabc78..f78a206150 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor
@@ -259,6 +259,9 @@
InfiniteScrolling
+
+ MarkdownEditor
+
MarkdownViewer
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/MainLayout.razor.NavItems.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/MainLayout.razor.NavItems.cs
index 0df3a24cac..4e03ceb45a 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/MainLayout.razor.NavItems.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/MainLayout.razor.NavItems.cs
@@ -155,6 +155,7 @@ public partial class MainLayout
new() { Text = "DataGrid", Url = "/components/datagrid", AdditionalUrls = ["/components/data-grid"] },
new() { Text = "ErrorBoundary", Url = "/components/errorboundary" },
new() { Text = "InfiniteScrolling", Url = "/components/infinitescrolling" },
+ new() { Text = "MarkdownEditor", Url = "/components/markdowneditor", Description = "MdEditor" },
new() { Text = "MarkdownViewer", Url = "/components/markdownviewer", Description = "MdViewer, MD" },
new() { Text = "MessageBox", Url = "/components/messagebox" },
new() { Text = "NavPanel", Url = "/components/navpanel" },
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json
index 67619b86d9..b6e9834124 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json
@@ -77,6 +77,12 @@
"minify": { "enabled": false },
"options": { "sourceMap": false }
},
+ {
+ "outputFile": "Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.css",
+ "inputFile": "Pages/Components/Extras/MarkdownEditor/BitMarkdownEditorDemo.razor.scss",
+ "minify": { "enabled": false },
+ "options": { "sourceMap": false }
+ },
{
"outputFile": "Pages/Components/Extras/MarkdownViewer/BitMarkdownViewerDemo.razor.css",
"inputFile": "Pages/Components/Extras/MarkdownViewer/BitMarkdownViewerDemo.razor.scss",