Migrate from Quill to Jodit (#264)

* Refactored JS folder.
Removed useless files

* Add border to signature webview ( Fix for light theme)

* migrated quill to jodit(links not working)

* Removed quill subfolder

* removed table styles and fixed trigger color

* disable addaptive toolbar

* Remove direction button

* MinHeight, Toolbar toggle, style for combobox

* Added reference mail to replies and forward

* Command bar in compose page
Fixed align behaviour with Jodit

* Fix theme toggle in composer

* switch cc and to in mail chain

* default selected value for aligment

* make CC/To/From to be mailto links

* Added drag and drop for images
Fixed dropzones visual states
Corrected border radius
Fixed null reference exception when event dispatched when chromium still not initialized
This commit is contained in:
Tiktack
2024-07-07 18:04:53 +02:00
committed by GitHub
parent 02cd8ed7ae
commit d6b3240506
36 changed files with 6444 additions and 23029 deletions

View File

@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Controls;
using EmailValidation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Toolkit.Uwp.Helpers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using MimeKit;
@@ -26,6 +27,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Messages.Mails;
using Wino.Core.Messages.Shell;
using Wino.Extensions;
@@ -121,7 +123,7 @@ namespace Wino.Views
private void OnFileDropGridDragOver(object sender, DragEventArgs e)
{
ViewModel.IsDraggingOverDropZone = true;
ViewModel.IsDraggingOverFilesDropZone = true;
e.AcceptedOperation = DataPackageOperation.Copy;
e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message;
@@ -132,21 +134,94 @@ namespace Wino.Views
private void OnFileDropGridDragLeave(object sender, DragEventArgs e)
{
ViewModel.IsDraggingOverDropZone = false;
ViewModel.IsDraggingOverFilesDropZone = false;
}
private async void OnFileDropGridFileDropped(object sender, DragEventArgs e)
{
try
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>();
await AttachFiles(files);
}
}
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
finally
{
ViewModel.IsDraggingOverComposerGrid = false;
ViewModel.IsDraggingOverFilesDropZone = false;
}
}
private void OnImageDropGridDragEnter(object sender, DragEventArgs e)
{
bool isValid = false;
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>();
// We can't use async/await here because DragUIOverride becomes inaccessible.
// https://github.com/microsoft/microsoft-ui-xaml/issues/9296
var files = e.DataView.GetStorageItemsAsync().GetAwaiter().GetResult().OfType<StorageFile>();
await AttachFiles(files);
foreach (var file in files)
{
if (ValidateImageFile(file))
{
isValid = true;
}
}
}
ViewModel.IsDraggingOverComposerGrid = false;
ViewModel.IsDraggingOverDropZone = false;
e.AcceptedOperation = isValid ? DataPackageOperation.Copy : DataPackageOperation.None;
if (isValid)
{
ViewModel.IsDraggingOverImagesDropZone = true;
e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message;
e.DragUIOverride.IsCaptionVisible = true;
e.DragUIOverride.IsGlyphVisible = true;
e.DragUIOverride.IsContentVisible = true;
}
}
private void OnImageDropGridDragLeave(object sender, DragEventArgs e)
{
ViewModel.IsDraggingOverImagesDropZone = false;
}
private async void OnImageDropGridImageDropped(object sender, DragEventArgs e)
{
try
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>();
var imageDataURLs = new List<string>();
foreach (var file in files)
{
if (ValidateImageFile(file))
imageDataURLs.Add(await GetDataURL(file));
}
await InvokeScriptSafeAsync($"insertImages({JsonConvert.SerializeObject(imageDataURLs)});");
}
}
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
finally
{
ViewModel.IsDraggingOverComposerGrid = false;
ViewModel.IsDraggingOverImagesDropZone = false;
}
static async Task<string> GetDataURL(StorageFile file)
{
return $"data:image/{file.FileType.Replace(".", "")};base64,{Convert.ToBase64String(await file.ReadBytesAsync())}";
}
}
private async Task AttachFiles(IEnumerable<StorageFile> files)
@@ -165,49 +240,52 @@ namespace Wino.Views
}
}
private bool ValidateImageFile(StorageFile file)
{
string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" };
var fileType = file.FileType.ToLower();
return allowedTypes.Contains(fileType);
}
private async void BoldButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('boldButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('bold')");
}
private async void ItalicButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('italicButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('italic')");
}
private async void UnderlineButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('underlineButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('underline')");
}
private async void StrokeButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('strikeButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('strikethrough')");
}
private async void BulletListButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('bulletListButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('insertunorderedlist')");
}
private async void OrderedListButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('orderedListButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('insertorderedlist')");
}
private async void IncreaseIndentClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('increaseIndentButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('indent')");
}
private async void DecreaseIndentClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('decreaseIndentButton').click();");
}
private async void DirectionButtonClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('directionButton').click();");
await InvokeScriptSafeAsync("editor.execCommand('outdent')");
}
private async void AlignmentChanged(object sender, SelectionChangedEventArgs e)
@@ -218,20 +296,26 @@ namespace Wino.Views
switch (alignment)
{
case "left":
await InvokeScriptSafeAsync("document.getElementById('ql-align-left').click();");
await InvokeScriptSafeAsync("editor.execCommand('justifyleft')");
break;
case "center":
await InvokeScriptSafeAsync("document.getElementById('ql-align-center').click();");
await InvokeScriptSafeAsync("editor.execCommand('justifycenter')");
break;
case "right":
await InvokeScriptSafeAsync("document.getElementById('ql-align-right').click();");
await InvokeScriptSafeAsync("editor.execCommand('justifyright')");
break;
case "justify":
await InvokeScriptSafeAsync("document.getElementById('ql-align-justify').click();");
await InvokeScriptSafeAsync("editor.execCommand('justifyfull')");
break;
}
}
private async void WebViewToggleButtonClicked(object sender, RoutedEventArgs e)
{
var enable = WebviewToolBarButton.IsChecked == true ? "true" : "false";
await InvokeScriptSafeAsync($"toggleToolbar('{enable}');");
}
public async Task<string> ExecuteScriptFunctionAsync(string functionName, params object[] parameters)
{
string script = functionName + "(";
@@ -252,21 +336,24 @@ namespace Wino.Views
{
try
{
return await Chromium.ExecuteScriptAsync(function);
return await Chromium?.ExecuteScriptAsync(function);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
catch (Exception) { }
return string.Empty;
}
private async void AddImageClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync("document.getElementById('addImageButton').click();");
await InvokeScriptSafeAsync("imageInput.click();");
}
private async Task FocusEditorAsync()
{
await InvokeScriptSafeAsync("quill.focus();");
await InvokeScriptSafeAsync("editor.selection.focus();");
Chromium.Focus(FocusState.Keyboard);
Chromium.Focus(FocusState.Programmatic);
@@ -279,24 +366,6 @@ namespace Wino.Views
await FocusEditorAsync();
}
private async Task<string> TryGetSelectedTextAsync()
{
try
{
return await Chromium.ExecuteScriptAsync("getSelectedText();");
}
catch (Exception) { }
return string.Empty;
}
private async void LinkButtonClicked(object sender, RoutedEventArgs e)
{
// Get selected text from Quill.
HyperlinkTextBox.Text = await TryGetSelectedTextAsync();
}
public async Task UpdateEditorThemeAsync()
{
await DOMLoadedTask.Task;
@@ -346,7 +415,7 @@ namespace Wino.Views
if (Chromium.CoreWebView2 != null)
{
Chromium.CoreWebView2.DOMContentLoaded -= DOMLoaded;
Chromium.CoreWebView2.WebMessageReceived -= ScriptMessageRecieved;
Chromium.CoreWebView2.WebMessageReceived -= ScriptMessageReceived;
}
Chromium.Close();
@@ -379,9 +448,9 @@ namespace Wino.Views
ViewModel.GetHTMLBodyFunction = new Func<Task<string>>(async () =>
{
var quillContent = await InvokeScriptSafeAsync("GetHTMLContent();");
var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();");
return JsonConvert.DeserializeObject<string>(quillContent);
return JsonConvert.DeserializeObject<string>(editorContent);
});
var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>();
@@ -391,60 +460,66 @@ namespace Wino.Views
private async void ChromiumInitialized(Microsoft.UI.Xaml.Controls.WebView2 sender, Microsoft.UI.Xaml.Controls.CoreWebView2InitializedEventArgs args)
{
var editorBundlePath = (await ViewModel.NativeAppService.GetQuillEditorBundlePathAsync()).Replace("full.html", string.Empty);
var editorBundlePath = (await ViewModel.NativeAppService.GetEditorBundlePathAsync()).Replace("editor.html", string.Empty);
Chromium.CoreWebView2.SetVirtualHostNameToFolderMapping("app.example", editorBundlePath, CoreWebView2HostResourceAccessKind.Allow);
Chromium.Source = new Uri("https://app.example/full.html");
Chromium.CoreWebView2.SetVirtualHostNameToFolderMapping("app.editor", editorBundlePath, CoreWebView2HostResourceAccessKind.Allow);
Chromium.Source = new Uri("https://app.editor/editor.html");
Chromium.CoreWebView2.DOMContentLoaded -= DOMLoaded;
Chromium.CoreWebView2.DOMContentLoaded += DOMLoaded;
Chromium.CoreWebView2.WebMessageReceived -= ScriptMessageRecieved;
Chromium.CoreWebView2.WebMessageReceived += ScriptMessageRecieved;
Chromium.CoreWebView2.WebMessageReceived -= ScriptMessageReceived;
Chromium.CoreWebView2.WebMessageReceived += ScriptMessageReceived;
}
private void ScriptMessageRecieved(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
var change = JsonConvert.DeserializeObject<string>(args.WebMessageAsJson);
var change = JsonConvert.DeserializeObject<WebViewMessage>(args.WebMessageAsJson);
bool isEnabled = change.EndsWith("ql-active");
if (change.StartsWith("ql-bold"))
BoldButton.IsChecked = isEnabled;
else if (change.StartsWith("ql-italic"))
ItalicButton.IsChecked = isEnabled;
else if (change.StartsWith("ql-underline"))
UnderlineButton.IsChecked = isEnabled;
else if (change.StartsWith("ql-strike"))
StrokeButton.IsChecked = isEnabled;
else if (change.StartsWith("orderedListButton"))
OrderedListButton.IsChecked = isEnabled;
else if (change.StartsWith("bulletListButton"))
BulletListButton.IsChecked = isEnabled;
else if (change.StartsWith("ql-direction"))
DirectionButton.IsChecked = isEnabled;
else if (change.StartsWith("ql-align-left"))
if (change.type == "bold")
{
AlignmentListView.SelectionChanged -= AlignmentChanged;
AlignmentListView.SelectedIndex = 0;
AlignmentListView.SelectionChanged += AlignmentChanged;
BoldButton.IsChecked = change.value == "true";
}
else if (change.StartsWith("ql-align-center"))
else if (change.type == "italic")
{
AlignmentListView.SelectionChanged -= AlignmentChanged;
AlignmentListView.SelectedIndex = 1;
AlignmentListView.SelectionChanged += AlignmentChanged;
ItalicButton.IsChecked = change.value == "true";
}
else if (change.StartsWith("ql-align-right"))
else if (change.type == "underline")
{
AlignmentListView.SelectionChanged -= AlignmentChanged;
AlignmentListView.SelectedIndex = 2;
AlignmentListView.SelectionChanged += AlignmentChanged;
UnderlineButton.IsChecked = change.value == "true";
}
else if (change.StartsWith("ql-align-justify"))
else if (change.type == "strikethrough")
{
StrokeButton.IsChecked = change.value == "true";
}
else if (change.type == "ol")
{
OrderedListButton.IsChecked = change.value == "true";
}
else if (change.type == "ul")
{
BulletListButton.IsChecked = change.value == "true";
}
else if (change.type == "indent")
{
IncreaseIndentButton.IsEnabled = change.value == "disabled" ? false : true;
}
else if (change.type == "outdent")
{
DecreaseIndentButton.IsEnabled = change.value == "disabled" ? false : true;
}
else if (change.type == "alignment")
{
var parsedValue = change.value switch
{
"jodit-icon_left" => 0,
"jodit-icon_center" => 1,
"jodit-icon_right" => 2,
"jodit-icon_justify" => 3,
_ => 0
};
AlignmentListView.SelectionChanged -= AlignmentChanged;
AlignmentListView.SelectedIndex = 3;
AlignmentListView.SelectedIndex = parsedValue;
AlignmentListView.SelectionChanged += AlignmentChanged;
}
}
@@ -464,14 +539,6 @@ namespace Wino.Views
await RenderInternalAsync(message.RenderModel.RenderHtml);
}
private async void HyperlinkAddClicked(object sender, RoutedEventArgs e)
{
await InvokeScriptSafeAsync($"addHyperlink('{LinkUrlTextBox.Text}')");
LinkUrlTextBox.Text = string.Empty;
HyperlinkFlyout.Hide();
}
private void BarDynamicOverflowChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args)
{
if (args.Action == CommandBarDynamicOverflowAction.AddingToOverflow)