Upload files

You write the upload handler. It receives the file the user inserted, pasted or dropped, sends it to your server, and hands the editor back the URL to put in the document. The samples in the download include a working handler for ASP.NET Web Forms, MVC, ASP.NET Core and PHP, plus the client script below.

Configure a handler, or files are embedded in the content

With no handler configured, nothing is uploaded and the file still has to go somewhere, so it goes into the document:

  • An image is embedded as a base64 data:URL. The picture becomes part of the HTML you save, and base64 adds about a third: a 3 MB photo turns into roughly 4 MB of text in one field. This is the usual cause of a save that fails, truncates, or times out when writing to the database.
  • A document is inserted as a link carrying the file name, but without a working address: the editor points it at a temporary blob: URL, and the content sanitizer removes that, because blob: is not one of the schemes it allows. The reader gets a link that cannot be opened. Set fileUploadLocalObjectUrl: false to refuse the insert and tell the user instead.

With a handler, the content holds a short URL such as /imageuploads/20260918-a1b2c3.png. To check an existing document, look at the HTML you stored: src="data:image/... means the handler is not being used.

The Insert Gallery dialog’s Upload button looks only for the window.rte_file_upload_handler global. If you set only the per-editor file_upload_handler option, the gallery shows no Upload button.

Syntax

// One global handler for every editor on the page.
// The Insert Gallery dialog's Upload button uses this one.
window.rte_file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };

// Or per editor, for the image dialog, paste and drag-and-drop.
var config = {};
config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) { ... };
var editor = new RichTextEditor("#div_editor", config);
file

Type: File — the browser’s File object, with name, size and type. Send it with XMLHttpRequest or fetch.

callback

Type: Function callback(url, errorMessage)— call it exactly once. On success pass the stored URL; on failure pass null and a message, which the editor shows to the user. An upload the user cancels is a failure too.

optionalIndex, optionalFiles

When several files are inserted at once, the handler is called for each one: optionalFiles is the whole set and optionalIndexis the position of this file, so progress can be reported as “2 of 5”.

The sample handlers answer in plain text: READY:<url> with status 200, or ERROR:<message> with status 500. The client script below turns those into the two callback forms.

Limiting the size of uploads and of the content

Set maxUploadFileSize and the editor refuses a larger file itself, before reading it. Nothing is embedded and nothing is sent, and the user is told which size was rejected and what the limit is:

var config = {};

// Largest file the editor will insert or upload, in bytes. 0 (the default) = no limit.
config.maxUploadFileSize = 5 * 1024 * 1024;   // 5 MB

// Optional: replace the default alert with your own dialog.
// The handler receives (state, name, message); set state.returnValue = true to say
// you have handled it, and the editor shows nothing itself.
config.oncustomdialog = function (state, name, message) {
  if (name !== "uploadtoobig") return;   // leave the others to the editor
  showMyDialog(message);
  state.returnValue = true;
};

It covers every route a file arrives by: the image dialog, the document dialog, paste, and drag-and-drop. Images already embedded inside pasted HTML (from Word, for example) are not covered, because refusing one mid-paste would leave the document half converted; maxHTMLLength below is the backstop for that.

Keep the check in your handler as well, since a browser is not a trustworthy place to enforce a limit. The sample handler rejects anything over 4,000,000 bytes and answers ERROR:file too big.

Two server limits must also be above your largest file:

  • ASP.NET: maxRequestLength in <httpRuntime> (kilobytes, default 4096) and maxAllowedContentLength in <requestLimits>(bytes, default about 30 MB).
  • PHP: upload_max_filesize(default 2 MB) and post_max_size. Below these, PHP discards the file before your handler runs and the editor reports that no file was received.

To cap the document itself, set a character limit. Both options are off by default (0), and when an edit crosses the limit the editor reverts to the last good state and shows a message rather than saving something your database will reject:

var config = {};
config.maxHTMLLength = 60000;  // characters INCLUDING the HTML tags; 0 = no limit
config.maxTextLength = 0;      // characters EXCLUDING the tags; 0 = no limit

Set maxHTMLLength a little below the size of the column you store the HTML in. Handle the reachmaxlength hook to replace the default alert with your own dialog.

Shrinking photographs before they are uploaded

A phone camera produces files far larger than any web page needs, and refusing them with maxUploadFileSize only tells the writer to go and find a different picture. The editor has no resizing setting of its own; the handler is where it belongs, because only your application knows how large the stored copy should be. Scale the image on a canvas and upload the result instead of the original:

config.file_upload_handler = function (file, callback, optionalIndex, optionalFiles) {
    var MAXWIDTH = 1600, MAXHEIGHT = 1600, QUALITY = 0.82;

    function send(blob, name) {
        // your own upload code: post "blob" under the name "name",
        // then callback(theUrl) or callback(null, theError).
    }

    // Leave anything that is not a still photograph alone. GIFs would lose
    // their animation, and a PDF is not something a canvas can redraw.
    if (file.type.indexOf("image/") !== 0 || file.type === "image/gif") {
        send(file, file.name);
        return;
    }

    var url = URL.createObjectURL(file);
    var img = new Image();
    img.onload = function () {
        var scale = Math.min(1, MAXWIDTH / img.width, MAXHEIGHT / img.height);
        if (scale === 1) { URL.revokeObjectURL(url); send(file, file.name); return; }  // already small enough

        var canvas = document.createElement("canvas");
        canvas.width = Math.round(img.width * scale);
        canvas.height = Math.round(img.height * scale);
        canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
        URL.revokeObjectURL(url);

        canvas.toBlob(function (blob) {
            if (!blob) { send(file, file.name); return; }   // the browser refused: send the original
            send(blob, file.name.replace(/\.[^.]+$/, "") + ".jpg");
        }, "image/jpeg", QUALITY);
    };
    img.onerror = function () { URL.revokeObjectURL(url); callback(null, "not-an-image"); };
    img.src = url;
};

Measured on a 2400 × 1600 photograph: the 3.9 MB original is uploaded as a 1600 × 1067 JPEG of 94 KB, and the document holds the URL your server returned.

Two things to keep in mind. The conversion to JPEG discards transparency, so branch on file.typeif your users paste logos as well as photographs. And re-encoding strips the file's metadata — usually welcome, since that is where a photograph's GPS coordinates live, but not if you rely on it.

When an upload fails

Call the callback with null and a message: callback(null, "http-error-413"). The editor shows The file could not be uploaded. followed by your message in brackets, and the uploadfailedhook fires first, so you can show your own dialog instead. That hook receives your message exactly as you passed it — the raw code, not the sentence around it — so you can branch on it.

A failed upload does not leave the image out: the editor falls back to embedding it in the content as a base64 data: URL, the same as if no handler were configured. An upload endpoint that quietly rejects large files therefore produces exactly the oversized HTML you were trying to avoid. Set maxUploadFileSize to the limit your endpoint enforces, so the file is refused before it is read.

Template JavaScript code

This is the client script from the samples: it shows a progress dialog with a Cancel button, posts the file, and calls back with the URL or the error.

var uploadhandlerpath = "/imageupload.ashx";


function rte_file_upload_handler(file, callback, optionalIndex, optionalFiles) {

    function append(parent, tagname, csstext) {
        var tag = parent.ownerDocument.createElement(tagname);
        if (csstext) tag.style.cssText = csstext;
        parent.appendChild(tag);
        return tag;
    }

    var uploadcancelled = false;

    var dialogouter = append(document.body, "div", "display:flex;align-items:center;justify-content:center;z-index:2147483646;position:fixed;inset:0;padding:16px;background-color:rgba(15,23,42,0.28)");
    dialogouter.setAttribute("role", "dialog");
    dialogouter.setAttribute("aria-modal", "true");
    var dialoginner = append(dialogouter, "div", "box-sizing:border-box;min-width:260px;max-width:calc(100vw - 32px);padding:16px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;box-shadow:0 16px 40px rgba(15,23,42,.2)");

    var line1 = append(dialoginner, "div", "margin:0 0 10px;color:#172033;font:700 15px/1.3 Aptos,Segoe UI,sans-serif;");
    line1.id = "rte-upload-progress-title-" + Date.now();
    dialogouter.setAttribute("aria-labelledby", line1.id);
    line1.innerText = "Uploading...";

    var totalsize = file.size;
    var sentsize = 0;

    if (optionalFiles && optionalFiles.length > 1) {
        totalsize = 0;
        for (var i = 0; i < optionalFiles.length; i++) {
            totalsize += optionalFiles[i].size;
            if (i < optionalIndex) sentsize = totalsize;
        }
        console.log(totalsize, optionalIndex, optionalFiles)
        line1.innerText = "Uploading..." + (optionalIndex + 1) + "/" + optionalFiles.length;
    }


    var line2 = append(dialoginner, "div", "margin:0 0 8px;color:#52657e;font:600 12px/1.3 Aptos,Segoe UI,sans-serif;");
    line2.innerText = "0%";

    var progressbar = append(dialoginner, "div", "height:8px;overflow:hidden;border-radius:4px;background:#e7eef7;");
    var progressbg = append(progressbar, "div", "width:0;height:100%;border-radius:inherit;background:#2487e8;transition:width 120ms ease;");

    var line3 = append(dialoginner, "div", "display:flex;justify-content:flex-end;margin-top:14px;");
    var btn = append(line3, "button");
    btn.style.cssText = "min-height:32px;padding:7px 11px;border:1px solid #bdd0e6;border-radius:7px;background:#fff;color:#315277;font:700 12px/1 Aptos,Segoe UI,sans-serif;cursor:pointer;";
    btn.innerText = "Cancel";
    btn.onclick = function () {
        uploadcancelled = true;
        xh.abort();
    }

    var xh = new XMLHttpRequest();
    xh.open("POST", uploadhandlerpath + "?name=" + encodeURIComponent(file.name) + "&type=" + encodeURIComponent(file.type) + "&size=" + file.size, true);
    xh.onload = xh.onabort = xh.onerror = function (pe) {
        console.log(pe);
        console.log(xh);
        dialogouter.remove();
        if (pe.type == "load") {
            if (xh.status != 200) {
                console.log("uploaderror", pe);
                if (xh.responseText.startsWith("ERROR:")) {
                    callback(null, "http-error-" + xh.responseText.substring(6));
                }
                else {
                    callback(null, "http-error-" + xh.status);
                }
            }
            else if (xh.responseText.startsWith("READY:")) {
                console.log("File uploaded to " + xh.responseText.substring(6));
                callback(xh.responseText.substring(6));
            }
            else {
                callback(null, "http-error-" + xh.responseText);
            }
        }
        else if (uploadcancelled) {
            console.log("uploadcancelled", pe);
            callback(null, "cancelled");
        }
        else {
            console.log("uploaderror", pe);
            callback(null, pe.type);
        }
    }
    xh.upload.onprogress = function (pe) {
        console.log(pe);
        //pe.total
        var percent = totalsize ? Math.floor(100 * (sentsize + pe.loaded) / totalsize) : 0;
        line2.innerText = percent + "%";

        progressbg.style.cssText = "width:" + percent + "%";
    }
    xh.send(file);
}

How an uploaded image is aligned

The editor applies no alignment of its own. An inserted image is an inline <img> with no alignattribute and no style, so it sits in the text and follows the alignment of the paragraph that contains it — left, unless that paragraph is centred or right-aligned:

<!-- what the editor inserts: no align attribute, no style -->
<p>Some text <img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

<!-- the same image, centred only because its paragraph is centred -->
<p style="text-align:center"><img src="/imageuploads/20260918-a1b2c3.png" loading="lazy" decoding="async"></p>

There is therefore no “default image alignment” setting to change. If images look centred everywhere, the cause is a rule in your own stylesheet, or paragraphs that are centred. Check the HTML you stored: if the <img> carries no alignment, it is your CSS.

Users set alignment per image by selecting it and using the alignment menu on the image toolbar. To change what every image does, write the rule in your own CSS and give the editor the same rule, so the editing view matches the published page:

var config = {};

// Applies inside the editing area. Use the same rule on the pages that display
// the saved content, or the two will not look alike.
config.contentCssText = "p { text-align: left; } p img { float: none; }";

// Or load one stylesheet in both places:
config.contentCssUrl = "/css/editor-content.css";

Further reading