import { storeContent, restoreContent, forgetContent } from 'editor/storage' import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt, setAuxiliaryToolbar, parentBlockNames } 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]) // * 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') const innerEl = figureEl.querySelector('[data-multimedia-inner]') if (!innerEl) throw new Error('Raro.') figureEl.replaceChild(node, innerEl) targetEl.insertBefore(figureEl, parentEl) 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, }, 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]'), }, 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) const selectedEl = editor.contentEl.querySelector('[data-editor-selected]') if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected 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) } } })