5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-15 04:31:41 +00:00
panel/app/javascript/editor/editor.ts

253 lines
7.6 KiB
TypeScript
Raw Normal View History

import { storeContent, restoreContent } from 'editor/storage'
2021-02-13 01:14:36 +00:00
import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt, setAuxiliaryToolbar } 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'
2021-02-13 01:14:36 +00:00
import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from 'editor/types/link'
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 <figure> o que no esté como bloque (se ponen
// después del bloque en el que están como bloque de por si)
// * convierte <i> y <b> en <em> y <strong>
// 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 {
2021-02-12 21:01:22 +00:00
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') {
2021-02-13 01:14:36 +00:00
const el = type.handleEmpty.create(editor)
// mover cosas que pueden haber
// por ejemplo: cuando convertís a un <ul>, 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 <br> 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)
}
}
2021-02-13 01:14:36 +00:00
export interface Editor {
editorEl: HTMLElement,
toolbarEl: HTMLElement,
toolbar: {
auxiliary: {
mark: {
parentEl: HTMLElement,
colorEl: HTMLInputElement,
},
multimedia: {
parentEl: HTMLElement,
fileEl: HTMLInputElement,
uploadEl: HTMLButtonElement,
altEl: HTMLInputElement,
},
link: {
parentEl: HTMLElement,
urlEl: HTMLInputElement,
},
},
},
contentEl: HTMLElement,
wordAlertEl: HTMLElement,
htmlEl: HTMLTextAreaElement,
}
function getSel<T extends Element>(parentEl: HTMLElement, selector: string): T {
const el = parentEl.querySelector<T>(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,
2021-02-13 01:14:36 +00:00
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]'),
},
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))
2021-02-13 01:14:36 +00:00
observer.observe(editor.contentEl, {
childList: true,
attributes: true,
subtree: true,
characterData: true,
})
document.addEventListener("selectionchange", () => routine(editor))
2021-02-13 01:14:36 +00:00
// 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)
2021-02-13 01:14:36 +00:00
setupLinkAuxiliaryToolbar(editor)
setupMarkAuxiliaryToolbar(editor)
2021-02-13 01:14:36 +00:00
// Finally...
routine(editor)
}
document.addEventListener("turbolinks:load", () => {
2021-02-13 01:14:36 +00:00
for (const editorEl of document.querySelectorAll<HTMLElement>('.editor[data-editor]')) {
try {
setupEditor(editorEl)
} catch (error) {
//alert(`No pude iniciar el editor: ${error}`)
// TODO: mostrar error
console.error('no se pudo iniciar el editor, error completo', error)
}
}
})