import { storeContent, restoreContent, forgetContent } from "editor/storage"; import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt, setAuxiliaryToolbar, parentBlockNames, clearSelected, } from "editor/utils"; import { types, getValidChildren, getType } from "editor/types"; import { setupButtons as setupMarksButtons } from "editor/types/marks"; import { setupButtons as setupBlocksButtons } from "editor/types/blocks"; import { setupButtons as setupParentBlocksButtons } from "editor/types/parentBlocks"; import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from "editor/types/link"; import { setupAuxiliaryToolbar as setupMultimediaAuxiliaryToolbar, setupButtons as setupMultimediaButtons, } from "editor/types/multimedia"; import { setupAuxiliaryToolbar as setupMarkAuxiliaryToolbar } from "editor/types/mark"; // Esta funcion corrije errores que pueden haber como: // * que un nodo que no tiene 'text' permitido no tenga children (se les // inserta un allowedChildren[0]) // * TODO: que haya una imágen sin
o que no esté como bloque (se ponen // después del bloque en el que están como bloque de por si) // * convierte y en y // Lo hace para que siga la estructura del documento y que no se borren por // cleanContent luego. function fixContent(editor: Editor, node: Element = editor.contentEl): void { if (node.tagName === "SCRIPT" || node.tagName === "STYLE") { node.parentElement?.removeChild(node); return; } if (node.tagName === "I") { const el = document.createElement("em"); moveChildren(node, el, null); node.parentElement?.replaceChild(el, node); node = el; } if (node.tagName === "B") { const el = document.createElement("strong"); moveChildren(node, el, null); node.parentElement?.replaceChild(el, node); node = el; } if (node instanceof HTMLImageElement) { node.dataset.multimediaInner = ""; const figureEl = types.multimedia.create(editor); let targetEl = node.parentElement; if (!targetEl) throw new Error("No encontré lx objetivo"); while (true) { const type = getType(targetEl); if (!type) throw new Error("lx objetivo tiene tipo"); if (type.type.allowedChildren.includes("multimedia")) break; if (!targetEl.parentElement) throw new Error("No encontré lx objetivo"); targetEl = targetEl.parentElement; } let parentEl = [...targetEl.childNodes].find((el) => el.contains(node)); if (!parentEl) throw new Error("no encontré lx pariente"); targetEl.insertBefore(figureEl, parentEl); const innerEl = figureEl.querySelector("[data-multimedia-inner]"); if (!innerEl) throw new Error("Raro."); figureEl.replaceChild(node, innerEl); node = figureEl; } const _type = getType(node); if (!_type) return; const { typeName, type } = _type; if (type.allowedChildren !== "ignore-children") { const sel = safeGetSelection(editor); const range = sel && safeGetRangeAt(sel); if (getValidChildren(node, type).length == 0) { if (typeof type.handleEmpty !== "string") { const el = type.handleEmpty.create(editor); // mover cosas que pueden haber // por ejemplo: cuando convertís a un
    , queda texto fuera del li que // creamos acá moveChildren(node, el, null); node.appendChild(el); if (range?.intersectsNode(node)) sel?.collapse(el); } } for (const child of node.childNodes) { if (!(child instanceof Element)) continue; fixContent(editor, child); } } } // Esta funcion hace que los elementos del editor sigan la estructura. // TODO: nos falta borrar atributos (style, y básicamente cualquier otra cosa) // Edge cases: // * no borramos los
    por que se requieren para que los navegadores // funcionen bien al escribir. no se deberían mostrar de todas maneras function cleanContent(editor: Editor, node: Element = editor.contentEl): void { const _type = getType(node); if (!_type) { node.parentElement?.removeChild(node); return; } const { type } = _type; if (type.allowedChildren !== "ignore-children") { for (const child of node.childNodes) { if ( child.nodeType === Node.TEXT_NODE && !type.allowedChildren.includes("text") ) { node.removeChild(child); continue; } if (!(child instanceof Element)) continue; const childType = getType(child); if (childType?.typeName === "br") continue; if (!childType || !type.allowedChildren.includes(childType.typeName)) { // XXX: esto extrae las cosas de adentro para que no sea destructivo moveChildren(child, node, child); node.removeChild(child); return; } cleanContent(editor, child); } // solo contar children válido para ese nodo const validChildrenLength = getValidChildren(node, type).length; const sel = safeGetSelection(editor); const range = sel && safeGetRangeAt(sel); if ( type.handleEmpty === "remove" && validChildrenLength == 0 //&& (!range || !range.intersectsNode(node)) ) { node.parentNode?.removeChild(node); return; } } } function routine(editor: Editor): void { try { fixContent(editor); cleanContent(editor); storeContent(editor); editor.htmlEl.value = editor.contentEl.innerHTML; } catch (error) { console.error("Hubo un problema corriendo la rutina", editor, error); } } export interface Editor { editorEl: HTMLElement; toolbarEl: HTMLElement; toolbar: { auxiliary: { mark: { parentEl: HTMLElement; colorEl: HTMLInputElement; textColorEl: HTMLInputElement; }; multimedia: { parentEl: HTMLElement; fileEl: HTMLInputElement; uploadEl: HTMLButtonElement; altEl: HTMLInputElement; removeEl: HTMLButtonElement; }; link: { parentEl: HTMLElement; urlEl: HTMLInputElement; }; }; }; contentEl: HTMLElement; wordAlertEl: HTMLElement; htmlEl: HTMLTextAreaElement; } function getSel(parentEl: HTMLElement, selector: string): T { const el = parentEl.querySelector(selector); if (!el) throw new Error(`No pude encontrar un componente \`${selector}\``); return el; } function setupEditor(editorEl: HTMLElement): void { // XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor? document.execCommand("defaultParagraphSeparator", false, "p"); const editor: Editor = { editorEl, toolbarEl: getSel(editorEl, ".editor-toolbar"), toolbar: { auxiliary: { mark: { parentEl: getSel(editorEl, "[data-editor-auxiliary=mark]"), colorEl: getSel( editorEl, "[data-editor-auxiliary=mark] [name=mark-color]" ), textColorEl: getSel( editorEl, "[data-editor-auxiliary=mark] [name=mark-text-color]" ), }, multimedia: { parentEl: getSel(editorEl, "[data-editor-auxiliary=multimedia]"), fileEl: getSel( editorEl, "[data-editor-auxiliary=multimedia] [name=multimedia-file]" ), uploadEl: getSel( editorEl, "[data-editor-auxiliary=multimedia] [name=multimedia-file-upload]" ), altEl: getSel( editorEl, "[data-editor-auxiliary=multimedia] [name=multimedia-alt]" ), removeEl: getSel( editorEl, "[data-editor-auxiliary=multimedia] [name=multimedia-remove]" ), }, link: { parentEl: getSel(editorEl, "[data-editor-auxiliary=link]"), urlEl: getSel( editorEl, "[data-editor-auxiliary=link] [name=link-url]" ), }, }, }, contentEl: getSel(editorEl, ".editor-content"), wordAlertEl: getSel(editorEl, ".editor-aviso-word"), htmlEl: getSel(editorEl, "textarea"), }; console.debug("iniciando editor", editor); // Recuperar el contenido si hay algo guardado, si tuviéramos un campo // de última edición podríamos saber si el artículo fue editado // después o la versión local es la última. // // TODO: Preguntar si se lo quiere recuperar. restoreContent(editor); // Word alert editor.contentEl.addEventListener("paste", () => { editor.wordAlertEl.style.display = "block"; }); // Setup routine listeners const observer = new MutationObserver(() => routine(editor)); observer.observe(editor.contentEl, { childList: true, attributes: true, subtree: true, characterData: true, }); document.addEventListener("selectionchange", () => routine(editor)); // Capture onClick editor.contentEl.addEventListener( "click", (event) => { const target = event.target! as Element; const type = getType(target); if (!type || !type.type.onClick) { setAuxiliaryToolbar(editor, null); clearSelected(editor); return true; } type.type.onClick(editor, target); return false; }, true ); // Clean seleted const selectedEl = editor.contentEl.querySelector("[data-editor-selected]"); if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected; // Setup botones setupMarksButtons(editor); setupBlocksButtons(editor); setupParentBlocksButtons(editor); setupMultimediaButtons(editor); setupLinkAuxiliaryToolbar(editor); setupMultimediaAuxiliaryToolbar(editor); setupMarkAuxiliaryToolbar(editor); // Finally... routine(editor); } document.addEventListener("turbolinks:load", () => { const flash = document.querySelector(".js-flash"); if (flash) { const keys = JSON.parse(flash.dataset.keys || "[]"); switch (flash.dataset.target) { case "editor": switch (flash.dataset.action) { case "forget-content": keys.forEach(forgetContent); } } } for (const editorEl of document.querySelectorAll( ".editor[data-editor]" )) { try { setupEditor(editorEl); } catch (error) { // TODO: mostrar error console.error("no se pudo iniciar el editor, error completo", error); } } });