tinymce / tinymce-blazor

Blazor integration
MIT License
45 stars 14 forks source link

How to add complex properties via Conf to Tinymce.init? #19

Closed felinepc closed 3 years ago

felinepc commented 3 years ago

The component exposes a Conf property of type Dictionary<string, object> for init configuration. This works well for simple string/int/boolean properties such as toolbar and height.

But what if we want to pass configurations with array and function types such as:

formats: {alignleft : [{selector: 'p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}}, 
                           {selector : 'img', attributes: {align : 'left'}}],
              alignright : [{selector: 'p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}}, 
                            {selector : 'img', attributes: {align : 'right'}}],
              aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}}}

and

setup: function(editor) {

        editor.ui.registry.addButton('wrapinpre', {
            tooltip: 'Insert Code',
            icon: 'sourcecode',
            onAction: function(_) {
                editor.insertContent('<pre>' + tinymce.activeEditor.selection.getContent({
                    format: 'text'
                }) + '</pre>');
            }
        });
    }

I tried adding these as strings to the dictionary but they don't work.

I know the component also takes the JsConfSrc base configuration which can be done in plain js added to _Host.cshtml. But there are some specific configurations that I'd like to add dynamically based on the page the editor is used on, and therefore the Conf property needs to be used.

Assistance with this would be greatly appreciated!

exalate-issue-sync[bot] commented 3 years ago

Ref: INT-2589

jscasca commented 3 years ago

Hi @felinepc

Configuration elements that are not functions can be created as c# objects and they will get parsed into JS objects. In your case for the formats options, a string will be passed as a string instead of the actual object, so in order to achieve your goal you will have to create a similar object: E.G: (You can one line the whole thing or separate in as many parts)

private static Dictionary<string, object> alignleft = new Dictionary<string, object>{ { "selector", "p,h1,h2"}, {"styles", new Dictionary<string, object>{{"textAlign", "left"}}} };
private static Dictionary<string, object> formats = new Dictionary<string, object>{{"alignleft", alignleft}, {"alignright", alignleft}};
private Dictionary<string, object> conf = new Dictionary<string, object> { { "toolbar", "customButton | undo redo | bold italic"}, { "width", 400}, {"formats", formats} };

Using functions can be a bit more problematic. I didn't want to use eval to pass parameters unless there was no other way around. I believe that you can build the necessary functions in the JS host. However, you can certainly use eval to pass a configuration to your JS to setup a configuration before the editor loads.

window.evalConf = (conf) => {window.eval.conf = eval(conf);};
window.switchConf = (id) => {
  window.switch.conf = page === 'x' { <conf 1> } : { <conf 2>};
};

And setup the configurations on init

@inject IJSRuntime JSRuntime
<TinyMCE.Blazor.Editor JsConfSrc="<evalConf | switchConf>" />
protected override Task OnInitializedAsync() {
  JSRuntime.InvokeVoidAsync("evalConf", "<eval conf>");
  JSRuntime.InvokeVoidAsync("switchConf", "x");
}

Let me know if this helps

felinepc commented 3 years ago

@jscasca Thanks for the instructions. The array configuration now makes perfect sense, but I'm still having trouble with the function configuration, as I have very limited JS skills (hence going with Blazor haha).

Basically this is what I need:

I put together a custom image upload handler function

images_upload_handler: function custom_upload_handler(blobInfo, success, failure, progress)
{
    //rest of uploading codes omitted
    xhr.setRequestHeader('x-xsrf-token', my_xsrfToken)
}

As you can see, my uploader needs to set a xsrf token header, but that token needs to be passed in by Blazor at runtime (my Blazor components have an injected BlazorTokenProvider which will have access to the token, i.e., BlazorTokenProvider.XsrfToken).

Now if I put this uploader function in _Host.cshtml and use JsConfSrc, I don't know how I can pass in the token from TinyMCE.Blazor.Editor.

Can you please help me out here? Thanks again!

jscasca commented 3 years ago

@felinepc There are multiple ways to achieve this and since it's a very common use case I have been trying to put together some sample codes for people to get started. I think the easiest way to get this done is by handling the image_upload in the C# side. To do this you can expose a public function that you can consume from your JS image_upload_handler. E.G: In your _host: (You can pass multiple parameters, I only saved the blobInfo.blob())

const upload_handler = (blobInfo, success, failure, progress) => {
            DotNet.invokeMethodAsync("SampleBlazorServer", "UploadHandler", <value that you want to save>).then((data) => {
                success(data);
            });
        };
        window.my = {
                conf: {
                    toolbar: 'image',
                    plugins: 'image',
                    images_upload_handler: upload_handler,
                    ...

And then, in your component:

private static Func<string, Task<string>> UploadHandlerAsync;
private Task<string> LocalUploadHandler(string val) {
    // Handle the upload here.... and then return the new location
    return Task.FromResult("https://i.imgur.com/GSGew1V.jpeg");
}

[JSInvokable]
public static Task<string> UploadHandler(string val) {
    return UploadHandlerAsync.Invoke(val);
}
protected override Task OnInitializedAsync() {
  UploadHandlerAsync = LocalUploadHandler;
  return base.OnInitializedAsync();
}

Let me know if this helps

felinepc commented 3 years ago

@jscasca Thanks I finally figured it out. I realized that in order to get something like this to work properly, I had to at least grasp the basics of Blazor JS Interop (something I hoped I'd never need to use myself hehe).

So after reading through the docs and then came back to your two solutions, I realized that I could just pass the variable to the _Host.cshtml JS configuration via the InvokeVoidAsync call (using your first approach, but no need for eval), like this:

protected async override Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JSRuntime.InvokeVoidAsync("setXsrfToken", BlazorTokenProvider.XsrfToken);
    }
}

My _Host.cshtml would look like this:

<script>
var xsrfToken = '';

window.setXsrfToken = (token) => { xsrfToken = token;};

window.tinyConf = { 
    images_upload_handler: //Use the token as needed in my uploader function etc
};
</script>

Now when I first saw your second approach, I thought it would be a great idea to call C# function directly from JS for upload logic (instead of using XHR to post to an upload endpoint). But upon further reading, I suspect it might not work well for my scenario because I'm using Blazor Server instead of WASM, and JS calls to invoke .NET methods are subject to message size limits which can be easily exceeded by file uploads.

Since I now have a fully functional setup (my JS upload handler posts to a Razor page protected by [Authorize] and XSRF token in the same Blazor Server project), I have not given that "JS invoking .NET" method a try. Do you think the message size limit would be a problem here, or the blobInfo.Blob passed over is merely a reference and won't actually carry the file data yet?

jscasca commented 3 years ago

Glad you got it working. Yes, for the aforementioned approach you would need to chunk large images and handle sending those chunks over. By default it only allows 32Kb of data if I remember correctly. The wrapper uses chunking to send large pieces of content over 32K.

I will close the ticket but feel free to open a new case if you run into any bug or have feature requests.