sutty/app/javascript/editor/types.ts

127 lines
4 KiB
TypeScript
Raw Normal View History

2021-02-13 01:14:36 +00:00
import { Editor } from 'editor/editor'
import { marks } from 'editor/types/marks'
import { blocks, li, EditorBlock } from 'editor/types/blocks'
import { parentBlocks } from 'editor/types/parentBlocks'
2021-02-14 16:01:41 +00:00
import { multimedia } from 'editor/types/multimedia'
import { blockNames, parentBlockNames, safeGetRangeAt, safeGetSelection } from 'editor/utils'
export interface EditorNode {
selector: string,
// la string es el nombre en la gran lista de types O 'text'
// XXX: esto es un hack para no poner EditorNode dentro de EditorNode,
// quizás podemos hacer que esto sea una función que retorna bool
2021-02-14 16:01:41 +00:00
allowedChildren: string[] | 'ignore-children',
// * si es 'do-nothing', no hace nada si está vacío (esto es para cuando
// permitís 'text' entonces se puede tipear adentro, ej: párrafo vacío)
// * si es 'remove', sacamos el coso si está vacío.
// ej: strong: { handleNothing: 'remove' }
// * si es un block, insertamos el bloque y movemos la selección ahí
// ej: ul: { handleNothing: li }
handleEmpty: 'do-nothing' | 'remove' | EditorBlock,
// esta función puede ser llamada para cosas que no necesariamente sea la
// creación del nodo con el botón; por ejemplo, al intentar recuperar
// el formato. esto es importante por que, por ejemplo, no deberíamos
// cambiar la selección acá.
2021-02-13 01:14:36 +00:00
create: (editor: Editor) => HTMLElement,
onClick?: (editor: Editor, target: Element) => void,
}
export const types: { [propName: string]: EditorNode } = {
...marks,
...blocks,
li,
...parentBlocks,
contentEl: {
selector: '.editor-content',
2021-02-14 16:01:41 +00:00
allowedChildren: [...blockNames, ...parentBlockNames, 'multimedia'],
handleEmpty: blocks.paragraph,
create: () => { throw new Error('se intentó crear contentEl') }
},
br: {
selector: 'br',
allowedChildren: [],
handleEmpty: 'do-nothing',
create: () => { throw new Error('se intentó crear br') }
},
2021-02-14 16:01:41 +00:00
multimedia,
}
export function getType (node: Element): { typeName: string, type: EditorNode } | null {
for (let [typeName, type] of Object.entries(types)) {
2021-03-27 18:45:46 +00:00
if (node.matches(type.selector)) {
return { typeName, type }
}
}
2021-03-27 18:45:46 +00:00
return null
}
// encuentra el primer pariente que pueda tener al type, y retorna un array
// donde
// array[0] = elemento que matchea el type
// array[array.len - 1] = primer elemento seleccionado
export function getValidParentInSelection (args: {
editor: Editor,
type: string,
}): Element[] {
const sel = safeGetSelection(args.editor)
if (!sel) throw new Error('No se donde insertar esto')
const range = safeGetRangeAt(sel)
if (!range) throw new Error('No se donde insertar esto')
let list: Element[] = []
if (!sel.anchorNode) {
throw new Error('No se donde insertar esto')
} else if (sel.anchorNode instanceof Element) {
list = [sel.anchorNode]
} else if (sel.anchorNode.parentElement) {
list = [sel.anchorNode.parentElement]
} else {
throw new Error('No se donde insertar esto')
}
while (true) {
const el = list[0]
if (!args.editor.contentEl.contains(el)
&& el != args.editor.contentEl)
throw new Error('No se donde insertar esto')
const type = getType(el)
if (type) {
//if (type.typeName === 'contentEl') break
//if (parentBlockNames.includes(type.typeName)) break
if ((type.type.allowedChildren instanceof Array)
&& type.type.allowedChildren.includes(args.type)) break
}
if (el.parentElement) {
list = [el.parentElement, ...list]
} else {
throw new Error('No se donde insertar esto')
}
}
return list
}
export function getValidChildren (node: Element, type: EditorNode): Node[] {
2021-02-14 16:01:41 +00:00
if (type.allowedChildren === 'ignore-children')
throw new Error('se llamó a getValidChildren con un type que no lo permite!')
return [...node.childNodes].filter(n => {
// si permite texto y esto es un texto, es válido
if (n.nodeType === Node.TEXT_NODE)
return type.allowedChildren.includes('text') && n.textContent?.length
// si no es un elemento, no es válido
if (!(n instanceof Element))
return false
const t = getType(n)
if (!t) return false
return type.allowedChildren.includes(t.typeName)
})
}