mirror of
https://0xacab.org/sutty/sutty
synced 2024-07-06 00:25:44 +00:00
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
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);
|
|
});
|
|
}
|