import { storeContent, restoreContent } from 'editor/storage' import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt } 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' export interface Editor { editorEl: HTMLElement, toolbarEl: HTMLElement, contentEl: HTMLElement, wordAlertEl: HTMLElement, htmlEl: HTMLTextAreaElement, } // 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 } const _type = getType(node) if (!_type) return const { typeName, type } = _type 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() // 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 for (const child of node.childNodes) { if (child.nodeType === Node.TEXT_NODE && type.allowedChildren.indexOf('text') === -1 ) { node.removeChild(child) continue } if (!(child instanceof Element)) continue const childType = getType(child) if (childType?.typeName === 'br') continue if (!childType || type.allowedChildren.indexOf(childType.typeName) === -1) { // 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) } } 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 toolbarEl = editorEl.querySelector('.editor-toolbar') if (!toolbarEl) throw new Error('No pude encontrar .editor-toolbar') const contentEl = editorEl.querySelector('.editor-content') if (!contentEl) throw new Error('No pude encontrar .editor-content') const wordAlertEl = editorEl.querySelector('.editor-aviso-word') if (!wordAlertEl) throw new Error('No pude encontrar .editor-aviso-word') const htmlEl = editorEl.querySelector('textarea') if (!htmlEl) throw new Error('No pude encontrar el textarea para el HTML') const editor: Editor = { editorEl, toolbarEl, contentEl, wordAlertEl, htmlEl, } 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(contentEl, { childList: true, attributes: true, subtree: true, characterData: true, }) document.addEventListener("selectionchange", () => routine(editor)) // Setup botones setupMarksButtons(editor) setupBlocksButtons(editor) setupParentBlocksButtons(editor) // Finally... routine(editor) } document.addEventListener("turbolinks:load", () => { for (const editorEl of document.querySelectorAll('.editor')) { if (!editorEl.querySelector('.editor-toolbar')) continue setupEditor(editorEl) } })