← Toda la documentación

Subir archivos

¿Tienes problemas? Sigue la guía para solucionar problemas de carga de imágenes si faltan solicitudes, recibes errores 413 o se pierden imágenes después de guardar.

El controlador de carga lo escribes tú. Recibe el archivo que el usuario insertó, pegó o soltó, lo envía a tu servidor y le devuelve al editor la URL que debe poner en el documento. Los ejemplos de la descarga incluyen un controlador que funciona para ASP.NET Web Forms, MVC, ASP.NET Core y PHP, además del script de cliente de abajo.

Configura un controlador o los archivos se incrustan en el contenido

Sin ningún controlador configurado, no se sube nada, y el archivo tiene que ir a alguna parte, así que va al documento:

  • Una imagen se incrusta como una URL data: en base64. La imagen pasa a formar parte del HTML que guardas, y base64 añade alrededor de un tercio: una foto de 3 MB se convierte en unos 4 MB de texto en un solo campo. Es la causa habitual de un guardado que falla, se trunca o agota el tiempo de espera al escribir en la base de datos.
  • Un documento se inserta como un enlace con el nombre del archivo, pero sin una dirección que funcione: el editor lo apunta a una URL blob: temporal, y el saneador de contenido la elimina, porque blob: no es uno de los esquemas que admite. El lector recibe un enlace que no se puede abrir. Establece fileUploadLocalObjectUrl: false para rechazar la inserción y avisar al usuario en su lugar.

Con un controlador, el contenido guarda una URL corta como /imageuploads/20260918-a1b2c3.png. Para comprobar un documento existente, mira el HTML que guardaste: src="data:image/... significa que no se está usando el controlador.

El botón Upload del cuadro de diálogo Insert Gallery solo busca la variable global window.rte_file_upload_handler. Si solo defines la opción file_upload_handler de cada editor, la galería no muestra ningún botón Upload.

La forma corta: dale al editor una URL

Si tu servidor ya acepta un archivo y responde con su URL, no necesitas escribir nada de JavaScript:

var config = {};

// The whole setup: the editor POSTs the file here and uses the URL you answer with.
config.file_upload_url = "/imageupload.ashx";

var editor = new RichTextEditor("#div_editor", config);

El editor envía el archivo por POST como multipart/form-data y lee la URL de la respuesta. Entiende las formas con las que ya responden los endpoints de otros editores, así que un servidor escrito para TinyMCE o CKEditor funciona aquí sin cambios:

Cuerpo de la respuestaEscrito para
{"location": "/img/x.png"}TinyMCE
{"url": "/img/x.png"}Adaptador de carga simple de CKEditor
{"urls": {"default": "..."}}CKEditor, varios tamaños
READY:/img/x.pnglos controladores de ejemplo de abajo
{"error": {"message": "..."}} o ERROR:messagecualquiera de ellos; se informa como fallo

Una respuesta que no tenga ninguna de esas formas se trata como una carga fallida en lugar de insertarse, así que un endpoint que responde OK no puede convertirse en el origen de una imagen.

config.file_upload_field = "file";          // form field name; CKEditor's endpoints use "upload"
config.file_upload_base_path = "https://cdn.example.com";  // prepended to a relative URL
config.file_upload_credentials = true;      // send cookies on a cross-origin upload
config.file_upload_reuse_filename = true;   // keep the original name (default: a unique one)
config.file_upload_headers = { "X-CSRF-Token": token };

Cada archivo subido recibe un nombre único de forma predeterminada, porque dos personas que suben image.png al mismo endpoint no deben sobrescribirse. Los reintentos, la cancelación y el evento de progreso funcionan igual que con tu propio controlador. Escribe un controlador cuando necesites redimensionar antes de enviar, subir directamente a S3 o mostrar tu propio cuadro de diálogo: tiene prioridad siempre que estén definidos los dos.

Sintaxis

// 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: el objeto File del navegador, con name, size y type. Envíalo con XMLHttpRequest o fetch.

callback

Type: Function callback(url, errorMessage): llámalo exactamente una vez. Si todo va bien, pasa la URL guardada; si falla, pasa null y un mensaje, que el editor muestra al usuario. Una carga que cancela el usuario también es un fallo.

optionalIndex, optionalFiles

Cuando se insertan varios archivos a la vez, se llama al controlador para cada uno: optionalFiles es el conjunto completo y optionalIndex es la posición de este archivo, así que el progreso se puede indicar como “2 de 5”.

Los controladores de ejemplo responden en texto plano: READY:<url> con estado 200, o ERROR:<message> con estado 500. El script de cliente de abajo los convierte en las dos formas del callback.

Cancelar una carga

Devuelve desde el controlador un objeto con un método abort() (o simplemente una función abort) y el editor podrá detener una solicitud que siga en curso:

config.file_upload_handler = function (file, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/imageupload.ashx");
    // ... send the file, then callback(url) or callback(null, message)
    xhr.send(formdata);

    // Returning this is what lets the editor stop the request.
    return { abort: function () { xhr.abort(); } };
};

editor.cancelUploads();          // abort everything in flight; returns the count
editor.getPendingUploadCount();  // how many are still running

cancelUploads() y destroy() cancelan las solicitudes en curso cuando el controlador proporciona una función abort. Si la imagen de destino se ha eliminado, un resultado tardío se descarta; eliminar una imagen no garantiza por sí solo que la solicitud de red se cancele de inmediato. El callback tardío de una carga cancelada se ignora, así que una respuesta lenta no puede volver a poner una imagen borrada. Se lanza un evento uploadcancel con el archivo y el motivo. No devolver nada sigue funcionando: simplemente la carga no se puede cancelar.

Imágenes ya incrustadas en el contenido

El controlador se llama para los archivos que el usuario inserta, pega o suelta. El contenido que llega de otra forma (establecido con setHTMLCode, pegado desde una página web u otro editor, o restaurado desde un borrador) conserva su URL data: en base64, así que esas imágenes se guardan dentro del HTML y tu controlador nunca las ve. Llama a uploadEmbeddedImages antes de guardar:

Para un formulario nativo, usa los ID de abajo y sustituye la acción por tu endpoint de guardado de documentos. Configura el controlador de carga antes de crear el editor. Este ejemplo espera a que terminen las cargas, rechaza las imágenes locales sin resolver y conserva la validación nativa y el botón de envío.

<form id="article-form" action="/your-document-save-endpoint" method="post">
  <!-- Include your framework's CSRF token field here. -->
  <div id="div_editor"></div>
  <input id="content" name="content" type="hidden">
  <p id="upload-save-status" role="status" aria-live="polite"></p>
  <button type="submit">Save document</button>
</form>
var form = document.getElementById("article-form");
var content = document.getElementById("content");
var saveStatus = document.getElementById("upload-save-status");
var scanning = false, approvedHTML = null;

form.addEventListener("submit", function (event) {
    var current = editor.getHTMLCode();
    if (approvedHTML !== null) {
        var approved = approvedHTML;
        approvedHTML = null;
        if (current !== approved || editor.getPendingUploadCount() > 0) {
            event.preventDefault();
            saveStatus.textContent = "Content changed. Wait for uploads, then save again.";
            return;
        }
        content.value = current; // Fresh snapshot in the native submit event.
        return;
    }
    event.preventDefault();
    if (scanning || editor.getPendingUploadCount() > 0) {
        saveStatus.textContent = "Images are still uploading. Wait, then save again.";
        return;
    }
    scanning = true;
    var submitter = event.submitter;
    saveStatus.textContent = "Checking embedded images...";
    try {
        editor.uploadEmbeddedImages(function (summary) {
            scanning = false;
            var localImage = Array.prototype.some.call(
                editor.getDocument().querySelectorAll("img"), function (image) {
                    return /^(data:|blob:|file:)/i.test((image.getAttribute("src") || "").trim());
                });
            if (summary.failed || summary.skipped || localImage || editor.getPendingUploadCount() > 0) {
                saveStatus.textContent = "Some images are not ready. Review and retry or remove them, then save again.";
                return;
            }
            approvedHTML = editor.getHTMLCode();
            saveStatus.textContent = "Images ready. Complete any required fields before saving.";
            // Re-run native validation; preserve the initiating submit button.
            if (submitter && submitter.form === form) form.requestSubmit(submitter);
            else form.requestSubmit();
            approvedHTML = null; // Validation may have stopped the submit event.
        });
    } catch (error) {
        scanning = false;
        approvedHTML = null;
        saveStatus.textContent = "Could not prepare images. Check your upload handler and try again.";
    }
});

Recorre el documento actual, convierte cada origen data:image/ en un File, los envía uno a uno a través del controlador y sustituye cada origen por la URL que devuelves. Las imágenes que ya están en http(s) no se tocan. Sin ningún controlador configurado, no cambia nada y todas las imágenes cuentan como omitidas.

Que no quede ninguna carga pendiente no demuestra que todo haya ido bien: revisa las imágenes que falten después de una carga fallida o cancelada. No llames directamente a form.submit(); se salta la validación nativa y los listeners de submit. Subir una imagen es independiente de guardar el documento; tu servidor debe autenticar, autorizar y sanear el HTML enviado antes de guardarlo o mostrarlo.

Subir automáticamente y reintentar un fallo

automaticUploads programa por ti la revisión de arriba. No garantiza que el guardado esté completo: mantén las comprobaciones de pendientes y errores y revisa las imágenes incrustadas sin resolver antes de tomar la instantánea que vas a guardar:

var config = {};
config.file_upload_handler = rte_file_upload_handler;

// Upload embedded base64 images by themselves, after setHTMLCode and after a paste.
config.automaticUploads = true;
config.automaticUploadsDelay = 300;   // ms debounce; a paste of several images uploads once

// Retry a failed upload before telling the user about it.
config.uploadRetryCount = 2;          // extra attempts after the first (default 0)
config.uploadRetryDelay = 1000;       // ms before the first retry; doubles after that

Se ejecuta, con debounce, después de setHTMLCode y después de insertar o pegar HTML, y no hace nada cuando no hay ningún controlador configurado. uploadRetryCount cubre el otro fallo habitual: antes, una sola conexión cortada dejaba la imagen incrustada para siempre. El reintento es silencioso cuando funciona, cada intento lanza un evento uploadretry y, cuando se agotan los intentos, se informa del error de tu callback como antes.

Limitar el tamaño de las cargas y del contenido

Establece maxUploadFileSize y el propio editor rechaza un archivo más grande, antes de leerlo. No se incrusta ni se envía nada, y se indica al usuario qué tamaño se rechazó y cuál es el límite:

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;
};

Cubre todas las vías por las que llega un archivo: el cuadro de diálogo de imágenes, el de documentos, el pegado y arrastrar y soltar. Las imágenes ya incrustadas dentro del HTML pegado (desde Word, por ejemplo) no quedan cubiertas, porque rechazar una a mitad de un pegado dejaría el documento convertido a medias; para eso, el último recurso es maxHTMLLength, más abajo.

Mantén también la comprobación en tu controlador, porque el navegador no es un lugar fiable para imponer un límite. El controlador de ejemplo rechaza todo lo que supere 4,000,000 bytes y responde ERROR:file too big.

Dos límites del servidor también deben estar por encima de tu archivo más grande:

  • ASP.NET: maxRequestLength en <httpRuntime> (kilobytes, 4096 de forma predeterminada) y maxAllowedContentLength en <requestLimits> (bytes, unos 30 MB de forma predeterminada).
  • PHP: upload_max_filesize (2 MB de forma predeterminada) y post_max_size. Si el archivo los supera, PHP lo descarta antes de que se ejecute tu controlador y el editor informa de que no se recibió ningún archivo.

Para limitar el propio documento, establece un límite de caracteres. Ambas opciones están desactivadas de forma predeterminada (0), y cuando una edición supera el límite, el editor vuelve al último estado correcto y muestra un mensaje en lugar de guardar algo que tu base de datos rechazará:

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

Establece maxHTMLLength un poco por debajo del tamaño de la columna en la que guardas el HTML. Gestiona el hook reachmaxlength para sustituir la alerta predeterminada por tu propio cuadro de diálogo.

Reducir las fotografías antes de subirlas

La cámara de un teléfono produce archivos mucho más grandes de lo que necesita cualquier página web, y rechazarlos con maxUploadFileSize solo le dice a quien escribe que vaya a buscar otra imagen. El editor no tiene ningún ajuste de redimensionamiento propio; su lugar es el controlador, porque solo tu aplicación sabe de qué tamaño debe ser la copia guardada. Escala la imagen en un canvas y sube el resultado en lugar del 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;
};

Medido con una fotografía de 2400 × 1600: el original de 3.9 MB se sube como un JPEG de 1600 × 1067 y 94 KB, y el documento guarda la URL que devolvió tu servidor.

Dos cosas que hay que tener en cuenta. La conversión a JPEG descarta la transparencia, así que distingue según file.type si tus usuarios pegan logotipos además de fotografías. Y volver a codificar elimina los metadatos del archivo: normalmente es bienvenido, porque ahí están las coordenadas GPS de una fotografía, pero no si dependes de ellos.

Cuando falla una carga

Llama al callback con null y un mensaje: callback(null, "http-error-413"). El editor muestra The file could not be uploaded. seguido de tu mensaje entre corchetes, y antes se lanza el hook uploadfailed, para que puedas mostrar tu propio cuadro de diálogo en su lugar. Ese hook recibe tu mensaje exactamente como lo pasaste (el código en bruto, no la frase que lo rodea), así que puedes tomar decisiones según él.

Cuando un controlador de carga configurado informa de un fallo, el núcleo distribuido actualmente elimina la imagen en lugar de conservar su vista previa en base64. Establece maxUploadFileSize en el límite que impone tu endpoint, para que el archivo se rechace antes de leerlo.

Plantilla de código JavaScript

Adaptado del controlador de la descarga que funciona, este script de cliente muestra un cuadro de diálogo de progreso con un botón Cancel, envía el archivo y llama al callback con la URL o el error. Se resuelve una sola vez, agota el tiempo de espera a los 60 segundos, permite cancelar con el teclado y restaura el foco. El contador de una sola página rte_upload_state pertenece a este ejemplo, no a la API del editor; aíslalo por editor en una aplicación con varios editores.

var uploadhandlerpath = "/imageupload.ashx";

function rte_file_upload_handler(file, callback, optionalIndex, optionalFiles) {
	var originalCallback = callback;
	var finished = false;
	rte_upload_state.pending++;
	callback = function (url, error) {
		if (finished) return;
		finished = true;
		if (error) rte_upload_state.error = String(error);
		try { originalCallback(url, error); }
		finally { rte_upload_state.pending--; }
	};
	try {

	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;box-sizing:border-box;background-color:rgba(15,23,42,0.28)");
	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;");
	var previousFocus = document.activeElement;
	window.__rteDemoUploadDialogId = (window.__rteDemoUploadDialogId || 0) + 1;
	line1.id = "rte-upload-label-" + window.__rteDemoUploadDialogId;
	dialogouter.setAttribute("role", "dialog");
	dialogouter.setAttribute("aria-modal", "true");
	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;
		}
		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;");
	progressbar.setAttribute("role", "progressbar");
	progressbar.setAttribute("aria-label", "File upload progress");
	progressbar.setAttribute("aria-valuemin", "0");
	progressbar.setAttribute("aria-valuemax", "100");
	progressbar.setAttribute("aria-valuenow", "0");
	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.type = "button";
	btn.setAttribute("aria-label", "Cancel file upload");
	dialoginner.addEventListener("keydown", function (event) {
		if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); btn.click(); }
		else if (event.key === "Tab" && !event.ctrlKey && !event.metaKey && !event.altKey) { event.preventDefault(); btn.focus(); }
	});
	btn.focus();
	btn.innerText = "Cancel";
	btn.onclick = function () {
		uploadcancelled = true;
		xh.abort();
	}

	var xh = new XMLHttpRequest();
	xh.timeout = 60000;
	xh.open("POST", uploadhandlerpath + "?name=" + encodeURIComponent(file.name || "edited-image.png") + "&type=" + encodeURIComponent(file.type || "image/png") + "&size=" + file.size, true);
	xh.onload = xh.onabort = xh.onerror = xh.ontimeout = function (pe) {
		if (finished) return;
		var restoreFocus = dialoginner.contains(document.activeElement);
		dialogouter.remove();
		try { if (restoreFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (error) { /* Never prevent upload settlement. */ }
		if (pe.type == "load") {
			if (xh.status != 200) {
				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:")) {
				var resolved;
				try {
					var value = xh.responseText.substring(6).trim();
					if (!value) throw new Error('Missing URL');
					resolved = new URL(value, new URL(uploadhandlerpath, document.baseURI));
					if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') throw new Error('Invalid URL');
				} catch (error) { callback(null, 'Server returned an invalid image URL'); return; }
				callback(resolved.href);
			}
			else {
				callback(null, "http-error-" + xh.responseText);
			}
		}
		else if (uploadcancelled) {
			callback(null, "cancelled");
		}
		else {
			callback(null, pe.type);
		}
	}
	xh.upload.onprogress = function (pe) {
		if (finished) return;
		//pe.total
		var percent = totalsize > 0 ? Math.max(0, Math.min(100, Math.floor(100 * (sentsize + pe.loaded) / totalsize))) : 0;
		line2.innerText = percent + "%";
		progressbar.setAttribute("aria-valuenow", String(percent));

		progressbg.style.width = percent + "%";
	}
	xh.send(file);
	return function () { if (!finished) { uploadcancelled = true; xh.abort(); } };
	} catch (error) {
		if (finished) throw error;
		var restoreFailedFocus = dialoginner && dialoginner.contains(document.activeElement);
		if (dialogouter && dialogouter.parentNode) dialogouter.parentNode.removeChild(dialogouter);
		try { if (restoreFailedFocus && previousFocus && previousFocus.isConnected && typeof previousFocus.focus === "function") previousFocus.focus(); } catch (focusError) { /* Preserve upload error. */ }
		callback(null, 'Could not start upload');
	}
}

// Single-page demo state, not an editor API.
var rte_upload_state = { pending: 0, error: '' };

var config = {
    file_upload_handler: rte_file_upload_handler,
    maxUploadFileSize: 4000000, // Match your server byte limit.
    allowBase64Images: false
};
var editor = new RichTextEditor("#div_editor", config);

Cómo se alinea una imagen subida

El editor no aplica ninguna alineación propia. Una imagen insertada es un <img> en línea sin atributo align y sin estilo, así que se queda en el texto y sigue la alineación del párrafo que la contiene: a la izquierda, a menos que ese párrafo esté centrado o alineado a la derecha:

<!-- 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>

Por tanto, no hay ningún ajuste de “alineación predeterminada de las imágenes” que cambiar. Si las imágenes aparecen centradas en todas partes, la causa es una regla de tu propia hoja de estilos o párrafos que están centrados. Revisa el HTML que guardaste: si el <img> no lleva ninguna alineación, es tu CSS.

Los usuarios definen la alineación de cada imagen seleccionándola y usando el menú de alineación de la barra de herramientas de imágenes. Para cambiar lo que hacen todas las imágenes, escribe la regla en tu propio CSS y dale al editor la misma regla, para que la vista de edición coincida con la página publicada:

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";

Para seguir leyendo