WYSIWYG editors


#62

You need to knock out the type definitions from that code to have it run:

function saveToServer(file: File) {

becomes:

function saveToServer(file) {

and

function insertToEditor(url: string) {

becomes:

function insertToEditor(url) {

Having done that, change editor to whatever you have named your Quill instance (quill in my case):

quill.getModule('toolbar').addHandler('image', () => {
  selectLocalImage();
});

Full code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Full Editor - Quill Rich Text Editor</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/monokai-sublime.min.css" />
    <link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css" />

    <style>
      body > #standalone-container {
        margin: 50px auto;
        max-width: 720px;
      }
      #editor-container {
        height: 350px;
      }
    </style>
  </head>
  <body>
    <div id="standalone-container">
        <div id="toolbar-container">

        <span class="ql-formats">
          <button class="ql-bold"></button>
          <button class="ql-italic"></button>
          <button class="ql-underline"></button>
        </span>
        <span class="ql-formats">
          <select class="ql-color"></select>
          <select class="ql-background"></select>
        </span>

        <span class="ql-formats">
          <button class="ql-header" value="1"></button>
          <button class="ql-header" value="2"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-list" value="ordered"></button>
          <button class="ql-list" value="bullet"></button>
          <button class="ql-indent" value="-1"></button>
          <button class="ql-indent" value="+1"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-direction" value="rtl"></button>
          <select class="ql-align"></select>
        </span>
        <span class="ql-formats">
          <button class="ql-link"></button>
          <button class="ql-image"></button>
          <button class="ql-video"></button>
        </span>

      </div>
      <div id="editor-container"></div>
      <br />

    <center>
    <form method="POST" action="post.asp" name="mainform">
      <input type="hidden" id="myHtml" name= "myHtml">
      <input type="submit" value="Submit">
    </form>
    </center>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
    <script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>

    <script>
      const editor = document.getElementById('editor-container');
      const hiddenInput = document.getElementById('myHtml');
      const form = document.forms.mainform;

      const quill = new Quill(editor, {
        modules: {
          syntax: true,
          toolbar: '#toolbar-container'
        },
        placeholder: 'Compose an epic...',
        theme: 'snow'
      });

      form.addEventListener('submit', function(e){
        e.preventDefault();
        hiddenInput.value = editor.firstChild.innerHTML
        this.submit();
      });

      function selectLocalImage() {
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.click();

        // Listen upload local image and save to server
        input.onchange = () => {
          const file = input.files[0];

          // file type is only image.
          if (/^image\//.test(file.type)) {
            saveToServer(file);
          } else {
            console.warn('You could only upload images.');
          }
        };
      }

      function saveToServer(file) {
        const fd = new FormData();
        fd.append('image', file);

        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://localhost:3000/upload/image', true);
        xhr.onload = () => {
          if (xhr.status === 200) {
            // this is callback data: url
            const url = JSON.parse(xhr.responseText).data;
            insertToEditor(url);
          }
        };
        xhr.send(fd);
      }

      function insertToEditor(url) {
        // push image url to rich editor.
        const range = editor.getSelection();
        editor.insertEmbed(range.index, 'image', `http://localhost:9000${url}`);
      }

      quill.getModule('toolbar').addHandler('image', () => {
        selectLocalImage();
      });
    </script>
  </body>
</html>

You’ll need to adapt the path to your server within the insertToEditor function.

HTH


#63

Thank you for that, I appreciate it. As I test it, just as is without write permissions to my server, and I try to upload a photo to the editor, should it place the uploaded image in the editor? As of now, it doesn’t. Just a side note, the editor doesn’t display in IE11 as it did before, but does in chrome and FF.


#64

It should place the image in the editor and then upload it to your server.

Yup. ES6. Probably the arrow function here:

input.onchange = () => { ... }

change to:

input.onchange = function() { ... }

#65

It’s not placing the image in the editor.

This doesn’t make a difference.


#66

It will do this after it has uploaded it, I guess.

What are you seeing in the console? I see:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:3000/upload/image. (Reason: CORS request did not succeed).[Learn More]

Which tells me that I need to be running everything at the same domain, or set up some kind of CORS handling.

Also, are you testing locally, or on your server?


#67

I am seeing no errors in console.

Will set up on server tomorrow and will get back to you.

Thank you


#68

I couldn’t get the photo uploader to work. Had my IT guy help me with path/url and permissions, etc and he couldn’t get any of it to work. Most of which I am not sure what he all did, but he know lots more than me! Perhaps, it’s just bad starting code that I gave you. Not your fault!

I appreciate your time and efforts and want to thank you again for what you have already done.

Take care.


#69

Hi,

I had a bit more time to look at this today and it wasn’t that hard to setup. I used PHP running on a local install of Apache, but you (or your IT guy) should be able to transfer the example to use ASP.

First off you need an upload directory with the correct permissions. I made mine at /var/www/html/uploads and set the permissions so that the user running Apache (www-data) can write to it.

sudo chown www-data:www-data /var/www/html/uploads
sudo chmod -R 775 /var/www/html
sudo usermod -a -G www-data jim

The final command adds my user to the www-data group.

Then we need a script to handle the upload. I’ve called mine upload.php. We are posting the image to it from the saveToServer function.

<?php
  if (preg_match('/^image\/p?jpeg$/i', $_FILES['image']['type'])){ 
    $ext = '.jpg'; 
  } else if (preg_match('/^image\/gif$/i', $_FILES['image']['type'])) { 
    $ext = '.gif'; 
  } else if (preg_match('/^image\/(x-)?png$/i', $_FILES['image']['type'])) { 
    $ext = '.png'; 
  } else { 
    $ext = '.unknown'; 
  } 

  $filename = time() . $ext; 
  $path = '/var/www/html/uploads/' . $filename;

  if (!is_uploaded_file($_FILES['image']['tmp_name']) or !copy($_FILES['image']['tmp_name'], $path)) { 
    $error = "Could not  save file as $filename!"; 
    echo $error;
  } else {
    echo $filename;
  }
?>

Here we are doing some checking on the extension type, then attempting to save the file in the uploads folder using a timestamp as the file name. If the save works, we return the filename to the client, otherwise we return a failure message.

After that, a couple of adjustments are needed in the HTML file (mine is called quill.html).

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Full Editor - Quill Rich Text Editor</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/monokai-sublime.min.css" />
    <link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css" />

    <style>
      body > #standalone-container {
        margin: 50px auto;
        max-width: 720px;
      }
      #editor-container {
        height: 350px;
      }
    </style>
  </head>
  <body>
    <div id="standalone-container">
        <div id="toolbar-container">

        <span class="ql-formats">
          <button class="ql-bold"></button>
          <button class="ql-italic"></button>
          <button class="ql-underline"></button>
        </span>
        <span class="ql-formats">
          <select class="ql-color"></select>
          <select class="ql-background"></select>
        </span>

        <span class="ql-formats">
          <button class="ql-header" value="1"></button>
          <button class="ql-header" value="2"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-list" value="ordered"></button>
          <button class="ql-list" value="bullet"></button>
          <button class="ql-indent" value="-1"></button>
          <button class="ql-indent" value="+1"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-direction" value="rtl"></button>
          <select class="ql-align"></select>
        </span>
        <span class="ql-formats">
          <button class="ql-link"></button>
          <button class="ql-image"></button>
          <button class="ql-video"></button>
        </span>

      </div>
      <div id="editor-container"></div>
      <br />

    <center>
    <form method="POST" action="post.asp" name="mainform">
      <input type="hidden" id="myHtml" name= "myHtml">
      <input type="submit" value="Submit">
    </form>
    </center>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
    <script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>

    <script>
      const editor = document.getElementById('editor-container');
      const hiddenInput = document.getElementById('myHtml');
      const form = document.forms.mainform;

      const quill = new Quill(editor, {
        modules: {
          syntax: true,
          toolbar: '#toolbar-container'
        },
        placeholder: 'Compose an epic...',
        theme: 'snow'
      });

      form.addEventListener('submit', function(e){
        e.preventDefault();
        hiddenInput.value = editor.firstChild.innerHTML
        this.submit();
      });

      function selectLocalImage() {
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.click();

        // Listen upload local image and save to server
        input.onchange = () => {
          const file = input.files[0];

          // file type is only image.
          if (/^image\//.test(file.type)) {
            saveToServer(file);
          } else {
            console.warn('You could only upload images.');
          }
        };
      }

      function saveToServer(file) {
        const fd = new FormData();
        fd.append('image', file);
        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://localhost/upload.php', true);
        xhr.onload = () => {
          if (xhr.status === 200) {
            const url = xhr.responseText;
            insertToEditor(url);
          }
        };
        xhr.send(fd);
      }

      function insertToEditor(url) {
        // push image url to rich editor.
        const range = quill.getSelection();
        quill.insertEmbed(range.index, 'image', `http://localhost/uploads/${url}`);
      }

      quill.getModule('toolbar').addHandler('image', () => {
        selectLocalImage();
      });
    </script>
  </body>
</html>

Nothing wild going on here, we are using the returned file name to load the image into the editor.

This will give you a basic working setup. However, you’ll still need to think about all of those upload related annoyances, like how do you handle large files etc. The client-side error handling could also be improved for the case that the image cannot be saved on the server, but I’m reticent to get into that before knowing if you can use the script or not.

HTH


#70

Okay, thank you very much. I will work on this and try to make sense of it. One question for you though:

What does ${url} mean - do I need to replace that with code or a specific URL? Are those single quotes too?

Thanks


#72

Okay - no IT guy, just me for now!

Able to upload file now. Progress!

I am having issues naming the file. What I am using is code from an existing form:

File.FileName =  upl.Form ("id") & ".jpg"

When the file gets uploaded the name is .jpg only. That obviously makes sense because there is no upl.form (from another form). So I am not sure what to use here.

Also, the photo is not showing in the editor. I am sure it has to do with this:quill.insertEmbed(range.index, 'image', `https://localhost/uploads/${url}`);

Don’t understand that or what to replace it with per my previous post.

Thanks!


#73

Nice one.

To give you an idea of the bigger picture, the file is being uploaded via Ajax (so seamlessly, with no page refresh on the client). Your server-side script is expected to receive this upload, save the file and upon a successful save, return the name (or path) of the newly uploaded file. Back on the client, the JavaScript receives this name/path and uses it to insert the image into the editor.

Well, you can’t use the file’s real name, because as soon as you have two uploads with the same name, you will overwrite the first with the second. In my example I used a timestamp. This is probably adequate, depending on how many users you have. If you wanted to be sure, you could always append a timestamp to the original name of the file, or to be really, really sure, use the client’s IP address in the filename.

The backticks are ES6 syntax (called template strings). The dollar and curly braces are used for string interpolation, where url is a placeholder for the value returned from your server-side script.

The above code in ES5 would be something like:

var imagePath = 'https://localhost/uploads/' + url
quill.insertEmbed(range.index, 'image',imagePath);

So, two things:

  1. This is only a small, dedicated script. Do you have the possibility to run PHP on your server?
  2. If not, I would suggest we remove saving the file from the equation and work on having the server-side script return a value to the client-side script, as that is an important piece of the puzzle. After that we can add in the rest of the functionality until you have a working demo.

#74

Yes, was aware of that and I was going to ask you the best way. I think appending a timestamp to the original name of the file would be fine. Is that done in your code or my upload code?

Still not understanding that and what to change on the existing or not.

No, just asp or asp.net

Thank you and let me know how to proceed!


#75

Update: I have partly figured this out. It doesn’t bring up the named jpg image. When I view course the /name.jpg is missing.


#76

Let’s start off by making sure your server-side script and your client-side script can communicate. Sorry in advance, if this seems a step backwards before going forwards, but it’ll help me get an idea of what is happening at your end.

I’ve changed the file/folder structure slightly for this example. For me, everything is now located in a /var/www/html/quill. This probably won’t affect things your end, but is worth mentioning.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Full Editor - Quill Rich Text Editor</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/monokai-sublime.min.css" />
    <link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css" />

    <style>
      body > #standalone-container {
        margin: 50px auto;
        max-width: 720px;
      }
      #editor-container {
        height: 350px;
      }
    </style>
  </head>
  <body>
    <div id="standalone-container">
        <div id="toolbar-container">

        <span class="ql-formats">
          <button class="ql-bold"></button>
          <button class="ql-italic"></button>
          <button class="ql-underline"></button>
        </span>
        <span class="ql-formats">
          <select class="ql-color"></select>
          <select class="ql-background"></select>
        </span>

        <span class="ql-formats">
          <button class="ql-header" value="1"></button>
          <button class="ql-header" value="2"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-list" value="ordered"></button>
          <button class="ql-list" value="bullet"></button>
          <button class="ql-indent" value="-1"></button>
          <button class="ql-indent" value="+1"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-direction" value="rtl"></button>
          <select class="ql-align"></select>
        </span>
        <span class="ql-formats">
          <button class="ql-link"></button>
          <button class="ql-image"></button>
          <button class="ql-video"></button>
        </span>

      </div>
      <div id="editor-container"></div>
      <br />

    <center>
    <form method="POST" action="post.asp" name="mainform">
      <input type="hidden" id="myHtml" name= "myHtml">
      <input type="submit" value="Submit">
    </form>
    </center>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
    <script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>

    <script>
      const editor = document.getElementById('editor-container');
      const hiddenInput = document.getElementById('myHtml');
      const form = document.forms.mainform;

      const quill = new Quill(editor, {
        modules: {
          syntax: true,
          toolbar: '#toolbar-container'
        },
        placeholder: 'Compose an epic...',
        theme: 'snow'
      });

      form.addEventListener('submit', function(e){
        e.preventDefault();
        hiddenInput.value = editor.firstChild.innerHTML
        this.submit();
      });

      function selectLocalImage() {
        const input = document.createElement('input');
        input.setAttribute('type', 'file');
        input.click();

        // Listen upload local image and save to server
        input.onchange = () => {
          const file = input.files[0];

          // file type is only image.
          if (/^image\//.test(file.type)) {
            saveToServer(file);
          } else {
            console.warn('You could only upload images.');
          }
        };
      }

      function saveToServer(file) {
        const fd = new FormData();
        fd.append('image', file);
        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://localhost/quill/upload.php', true);
        xhr.onload = () => {
          if (xhr.status === 200) {
            console.log(xhr.responseText);
          }
        };
        xhr.send(fd);
      }

      function insertToEditor(url) {
        // push image url to rich editor.
        const range = quill.getSelection();
        quill.insertEmbed(range.index, 'image', `http://localhost/uploads/${url}`);
      }

      quill.getModule('toolbar').addHandler('image', () => {
        selectLocalImage();
      });
    </script>
  </body>
</html>

This is the same as before, except for the saveToServer function, which now only logs the server’s response (xhr.responseText) to the console.

Now, change the server-side script to simply print out whatever information it receives from the client. In PHP, as mentioned, everything is contained in the $_FILES variable. This means I can just dump the contents of this variable:

<?php
  print_r($_FILES);
?>

Obviously, you’ll need to translate this into ASP.

Now, when you select a file using the image select button in the Quill editor, the client-side script will still send everything to the server as before, but when it receives a response from the server, instead of using the response to insert the image into the editor, it just logs it to the console.

For me, that looks like this:

Can you adjust things your end so that you get a similar response in the client, make a screengrab and post it back here.


#77

Not sure how to convert this but I did an asp response.write file.filename and I get this, which looks like just the name of the uploaded image. Don’t understand the Mutation Events information:

sitepoint2


#78

You can ignore that, just a warning from the Quill library.

You need to find out unfortunately. For example, you’ll need to do a little server-side validation and check that the extension is a valid type (e.g. not an executable file).

Try googling “File uploads in ASP” and seeing what you come up with.


#79

If you are talking about the .jpg extension - that is uploading the file and I can see the image in the folder, it’s just named .jpg though. Maybe you are looking for more than that?

I have been trying since my last post to execute code to get you what you want, nothing successful yet. What variable or variables are you specifically looking for?


#80

Some info on the uploaded file would be good, if only for some server-side validation. You don’t want people uploading executable files and then running them on your server.

Could you maybe try Googling “Ajax file upload ASP” or something similar and seeing what you come up with.


#81

I think my code already covers that. I tried to upload a word document and I get this:
sitepoint3

I have been looking for all I can find but can you tell me what specifically what I am looking for? I am able to see all of the server variables, but I think you are looking for something more?

Thanks


#82

I’m afraid not. That validation is taking place on the client (in the selectLocalImage function). I can also send a POST request to your server, completely bypassing that check.

Oh ok, that’s cool (probably). What can you see? We’re looking for info about the uploaded file.