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"; 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 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á. create: (editor: Editor) => HTMLElement; onClick?: (editor: Editor, target: Element) => void; } export const types: { [propName: string]: EditorNode } = { ...marks, ...blocks, li, ...parentBlocks, contentEl: { selector: ".editor-content", 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"); }, }, multimedia, }; export function getType( node: Element ): { typeName: string; type: EditorNode } | null { for (let [typeName, type] of Object.entries(types)) { if (node.matches(type.selector)) { return { typeName, type }; } } 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[] { 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); }); }