mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-23 00:26:22 +00:00
style: correr prettier sobre el editor
This commit is contained in:
parent
df38e12e3c
commit
525a6fc680
10 changed files with 969 additions and 859 deletions
|
@ -1,18 +1,23 @@
|
||||||
import { storeContent, restoreContent, forgetContent } from 'editor/storage'
|
import { storeContent, restoreContent, forgetContent } from "editor/storage";
|
||||||
import {
|
import {
|
||||||
isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt,
|
isDirectChild,
|
||||||
setAuxiliaryToolbar, parentBlockNames, clearSelected,
|
moveChildren,
|
||||||
} from 'editor/utils'
|
safeGetSelection,
|
||||||
import { types, getValidChildren, getType } from 'editor/types'
|
safeGetRangeAt,
|
||||||
import { setupButtons as setupMarksButtons } from 'editor/types/marks'
|
setAuxiliaryToolbar,
|
||||||
import { setupButtons as setupBlocksButtons } from 'editor/types/blocks'
|
parentBlockNames,
|
||||||
import { setupButtons as setupParentBlocksButtons } from 'editor/types/parentBlocks'
|
clearSelected,
|
||||||
import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from 'editor/types/link'
|
} 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 {
|
import {
|
||||||
setupAuxiliaryToolbar as setupMultimediaAuxiliaryToolbar,
|
setupAuxiliaryToolbar as setupMultimediaAuxiliaryToolbar,
|
||||||
setupButtons as setupMultimediaButtons,
|
setupButtons as setupMultimediaButtons,
|
||||||
} from 'editor/types/multimedia'
|
} from "editor/types/multimedia";
|
||||||
import { setupAuxiliaryToolbar as setupMarkAuxiliaryToolbar } from 'editor/types/mark'
|
import { setupAuxiliaryToolbar as setupMarkAuxiliaryToolbar } from "editor/types/mark";
|
||||||
|
|
||||||
// Esta funcion corrije errores que pueden haber como:
|
// Esta funcion corrije errores que pueden haber como:
|
||||||
// * que un nodo que no tiene 'text' permitido no tenga children (se les
|
// * que un nodo que no tiene 'text' permitido no tenga children (se les
|
||||||
|
@ -22,77 +27,74 @@ import { setupAuxiliaryToolbar as setupMarkAuxiliaryToolbar } from 'editor/types
|
||||||
// * convierte <i> y <b> en <em> y <strong>
|
// * convierte <i> y <b> en <em> y <strong>
|
||||||
// Lo hace para que siga la estructura del documento y que no se borren por
|
// Lo hace para que siga la estructura del documento y que no se borren por
|
||||||
// cleanContent luego.
|
// cleanContent luego.
|
||||||
function fixContent (editor: Editor, node: Element = editor.contentEl): void {
|
function fixContent(editor: Editor, node: Element = editor.contentEl): void {
|
||||||
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') {
|
if (node.tagName === "SCRIPT" || node.tagName === "STYLE") {
|
||||||
node.parentElement?.removeChild(node)
|
node.parentElement?.removeChild(node);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.tagName === 'I') {
|
if (node.tagName === "I") {
|
||||||
const el = document.createElement('em')
|
const el = document.createElement("em");
|
||||||
moveChildren(node, el, null)
|
moveChildren(node, el, null);
|
||||||
node.parentElement?.replaceChild(el, node)
|
node.parentElement?.replaceChild(el, node);
|
||||||
node = el
|
node = el;
|
||||||
}
|
}
|
||||||
if (node.tagName === 'B') {
|
if (node.tagName === "B") {
|
||||||
const el = document.createElement('strong')
|
const el = document.createElement("strong");
|
||||||
moveChildren(node, el, null)
|
moveChildren(node, el, null);
|
||||||
node.parentElement?.replaceChild(el, node)
|
node.parentElement?.replaceChild(el, node);
|
||||||
node = el
|
node = el;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node instanceof HTMLImageElement) {
|
if (node instanceof HTMLImageElement) {
|
||||||
node.dataset.multimediaInner = ''
|
node.dataset.multimediaInner = "";
|
||||||
const figureEl = types.multimedia.create(editor)
|
const figureEl = types.multimedia.create(editor);
|
||||||
|
|
||||||
let targetEl = node.parentElement
|
let targetEl = node.parentElement;
|
||||||
if (!targetEl) throw new Error('No encontré lx objetivo')
|
if (!targetEl) throw new Error("No encontré lx objetivo");
|
||||||
while (true) {
|
while (true) {
|
||||||
const type = getType(targetEl)
|
const type = getType(targetEl);
|
||||||
if (!type) throw new Error('lx objetivo tiene tipo')
|
if (!type) throw new Error("lx objetivo tiene tipo");
|
||||||
if (type.type.allowedChildren.includes('multimedia')) break
|
if (type.type.allowedChildren.includes("multimedia")) break;
|
||||||
if (!targetEl.parentElement) throw new Error('No encontré lx objetivo')
|
if (!targetEl.parentElement) throw new Error("No encontré lx objetivo");
|
||||||
targetEl = targetEl.parentElement
|
targetEl = targetEl.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentEl = [...targetEl.childNodes].find(
|
let parentEl = [...targetEl.childNodes].find((el) => el.contains(node));
|
||||||
el => el.contains(node)
|
if (!parentEl) throw new Error("no encontré lx pariente");
|
||||||
)
|
targetEl.insertBefore(figureEl, parentEl);
|
||||||
if (!parentEl) throw new Error('no encontré lx pariente')
|
|
||||||
targetEl.insertBefore(figureEl, parentEl)
|
|
||||||
|
|
||||||
const innerEl = figureEl.querySelector('[data-multimedia-inner]')
|
const innerEl = figureEl.querySelector("[data-multimedia-inner]");
|
||||||
if (!innerEl) throw new Error('Raro.')
|
if (!innerEl) throw new Error("Raro.");
|
||||||
figureEl.replaceChild(node, innerEl)
|
figureEl.replaceChild(node, innerEl);
|
||||||
|
|
||||||
node = figureEl
|
node = figureEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _type = getType(node)
|
const _type = getType(node);
|
||||||
if (!_type) return
|
if (!_type) return;
|
||||||
|
|
||||||
const { typeName, type } = _type
|
const { typeName, type } = _type;
|
||||||
|
|
||||||
if (type.allowedChildren !== 'ignore-children') {
|
if (type.allowedChildren !== "ignore-children") {
|
||||||
const sel = safeGetSelection(editor)
|
const sel = safeGetSelection(editor);
|
||||||
const range = sel && safeGetRangeAt(sel)
|
const range = sel && safeGetRangeAt(sel);
|
||||||
|
|
||||||
if (getValidChildren(node, type).length == 0) {
|
if (getValidChildren(node, type).length == 0) {
|
||||||
if (typeof type.handleEmpty !== 'string') {
|
if (typeof type.handleEmpty !== "string") {
|
||||||
const el = type.handleEmpty.create(editor)
|
const el = type.handleEmpty.create(editor);
|
||||||
// mover cosas que pueden haber
|
// mover cosas que pueden haber
|
||||||
// por ejemplo: cuando convertís a un <ul>, queda texto fuera del li que
|
// por ejemplo: cuando convertís a un <ul>, queda texto fuera del li que
|
||||||
// creamos acá
|
// creamos acá
|
||||||
moveChildren(node, el, null)
|
moveChildren(node, el, null);
|
||||||
node.appendChild(el)
|
node.appendChild(el);
|
||||||
if (range?.intersectsNode(node))
|
if (range?.intersectsNode(node)) sel?.collapse(el);
|
||||||
sel?.collapse(el)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
if (!(child instanceof Element)) continue
|
if (!(child instanceof Element)) continue;
|
||||||
fixContent(editor, child)
|
fixContent(editor, child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,205 +104,231 @@ function fixContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
// Edge cases:
|
// Edge cases:
|
||||||
// * no borramos los <br> por que se requieren para que los navegadores
|
// * no borramos los <br> por que se requieren para que los navegadores
|
||||||
// funcionen bien al escribir. no se deberían mostrar de todas maneras
|
// funcionen bien al escribir. no se deberían mostrar de todas maneras
|
||||||
function cleanContent (editor: Editor, node: Element = editor.contentEl): void {
|
function cleanContent(editor: Editor, node: Element = editor.contentEl): void {
|
||||||
const _type = getType(node)
|
const _type = getType(node);
|
||||||
if (!_type) {
|
if (!_type) {
|
||||||
node.parentElement?.removeChild(node)
|
node.parentElement?.removeChild(node);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type } = _type
|
const { type } = _type;
|
||||||
|
|
||||||
if (type.allowedChildren !== 'ignore-children') {
|
if (type.allowedChildren !== "ignore-children") {
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
if (child.nodeType === Node.TEXT_NODE
|
if (
|
||||||
&& !type.allowedChildren.includes('text')
|
child.nodeType === Node.TEXT_NODE &&
|
||||||
|
!type.allowedChildren.includes("text")
|
||||||
) {
|
) {
|
||||||
node.removeChild(child)
|
node.removeChild(child);
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(child instanceof Element)) continue
|
if (!(child instanceof Element)) continue;
|
||||||
|
|
||||||
const childType = getType(child)
|
const childType = getType(child);
|
||||||
if (childType?.typeName === 'br') continue
|
if (childType?.typeName === "br") continue;
|
||||||
if (!childType || !type.allowedChildren.includes(childType.typeName)) {
|
if (!childType || !type.allowedChildren.includes(childType.typeName)) {
|
||||||
// XXX: esto extrae las cosas de adentro para que no sea destructivo
|
// XXX: esto extrae las cosas de adentro para que no sea destructivo
|
||||||
moveChildren(child, node, child)
|
moveChildren(child, node, child);
|
||||||
node.removeChild(child)
|
node.removeChild(child);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanContent(editor, child)
|
cleanContent(editor, child);
|
||||||
}
|
}
|
||||||
|
|
||||||
// solo contar children válido para ese nodo
|
// solo contar children válido para ese nodo
|
||||||
const validChildrenLength = getValidChildren(node, type).length
|
const validChildrenLength = getValidChildren(node, type).length;
|
||||||
|
|
||||||
const sel = safeGetSelection(editor)
|
const sel = safeGetSelection(editor);
|
||||||
const range = sel && safeGetRangeAt(sel)
|
const range = sel && safeGetRangeAt(sel);
|
||||||
if (type.handleEmpty === 'remove'
|
if (
|
||||||
&& validChildrenLength == 0
|
type.handleEmpty === "remove" &&
|
||||||
|
validChildrenLength == 0
|
||||||
//&& (!range || !range.intersectsNode(node))
|
//&& (!range || !range.intersectsNode(node))
|
||||||
) {
|
) {
|
||||||
node.parentNode?.removeChild(node)
|
node.parentNode?.removeChild(node);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function routine (editor: Editor): void {
|
function routine(editor: Editor): void {
|
||||||
try {
|
try {
|
||||||
fixContent(editor)
|
fixContent(editor);
|
||||||
cleanContent(editor)
|
cleanContent(editor);
|
||||||
storeContent(editor)
|
storeContent(editor);
|
||||||
|
|
||||||
editor.htmlEl.value = editor.contentEl.innerHTML
|
editor.htmlEl.value = editor.contentEl.innerHTML;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Hubo un problema corriendo la rutina', editor, error)
|
console.error("Hubo un problema corriendo la rutina", editor, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Editor {
|
export interface Editor {
|
||||||
editorEl: HTMLElement,
|
editorEl: HTMLElement;
|
||||||
toolbarEl: HTMLElement,
|
toolbarEl: HTMLElement;
|
||||||
toolbar: {
|
toolbar: {
|
||||||
auxiliary: {
|
auxiliary: {
|
||||||
mark: {
|
mark: {
|
||||||
parentEl: HTMLElement,
|
parentEl: HTMLElement;
|
||||||
colorEl: HTMLInputElement,
|
colorEl: HTMLInputElement;
|
||||||
},
|
};
|
||||||
multimedia: {
|
multimedia: {
|
||||||
parentEl: HTMLElement,
|
parentEl: HTMLElement;
|
||||||
fileEl: HTMLInputElement,
|
fileEl: HTMLInputElement;
|
||||||
uploadEl: HTMLButtonElement,
|
uploadEl: HTMLButtonElement;
|
||||||
altEl: HTMLInputElement,
|
altEl: HTMLInputElement;
|
||||||
removeEl: HTMLButtonElement,
|
removeEl: HTMLButtonElement;
|
||||||
},
|
};
|
||||||
link: {
|
link: {
|
||||||
parentEl: HTMLElement,
|
parentEl: HTMLElement;
|
||||||
urlEl: HTMLInputElement,
|
urlEl: HTMLInputElement;
|
||||||
},
|
};
|
||||||
},
|
};
|
||||||
},
|
};
|
||||||
contentEl: HTMLElement,
|
contentEl: HTMLElement;
|
||||||
wordAlertEl: HTMLElement,
|
wordAlertEl: HTMLElement;
|
||||||
htmlEl: HTMLTextAreaElement,
|
htmlEl: HTMLTextAreaElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSel<T extends Element>(parentEl: HTMLElement, selector: string): T {
|
function getSel<T extends Element>(parentEl: HTMLElement, selector: string): T {
|
||||||
const el = parentEl.querySelector<T>(selector)
|
const el = parentEl.querySelector<T>(selector);
|
||||||
if (!el) throw new Error(`No pude encontrar un componente \`${selector}\``)
|
if (!el) throw new Error(`No pude encontrar un componente \`${selector}\``);
|
||||||
return el
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEditor (editorEl: HTMLElement): void {
|
function setupEditor(editorEl: HTMLElement): void {
|
||||||
// XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor?
|
// XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor?
|
||||||
document.execCommand('defaultParagraphSeparator', false, 'p')
|
document.execCommand("defaultParagraphSeparator", false, "p");
|
||||||
|
|
||||||
const editor: Editor = {
|
const editor: Editor = {
|
||||||
editorEl,
|
editorEl,
|
||||||
toolbarEl: getSel(editorEl, '.editor-toolbar'),
|
toolbarEl: getSel(editorEl, ".editor-toolbar"),
|
||||||
toolbar: {
|
toolbar: {
|
||||||
auxiliary: {
|
auxiliary: {
|
||||||
mark: {
|
mark: {
|
||||||
parentEl: getSel(editorEl, '[data-editor-auxiliary=mark]'),
|
parentEl: getSel(editorEl, "[data-editor-auxiliary=mark]"),
|
||||||
colorEl: getSel(editorEl, '[data-editor-auxiliary=mark] [name=mark-color]'),
|
colorEl: getSel(
|
||||||
|
editorEl,
|
||||||
|
"[data-editor-auxiliary=mark] [name=mark-color]"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
multimedia: {
|
multimedia: {
|
||||||
parentEl: getSel(editorEl, '[data-editor-auxiliary=multimedia]'),
|
parentEl: getSel(editorEl, "[data-editor-auxiliary=multimedia]"),
|
||||||
fileEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file]'),
|
fileEl: getSel(
|
||||||
uploadEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file-upload]'),
|
editorEl,
|
||||||
altEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-alt]'),
|
"[data-editor-auxiliary=multimedia] [name=multimedia-file]"
|
||||||
removeEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-remove]'),
|
),
|
||||||
|
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: {
|
link: {
|
||||||
parentEl: getSel(editorEl, '[data-editor-auxiliary=link]'),
|
parentEl: getSel(editorEl, "[data-editor-auxiliary=link]"),
|
||||||
urlEl: getSel(editorEl, '[data-editor-auxiliary=link] [name=link-url]'),
|
urlEl: getSel(
|
||||||
|
editorEl,
|
||||||
|
"[data-editor-auxiliary=link] [name=link-url]"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contentEl: getSel(editorEl, '.editor-content'),
|
contentEl: getSel(editorEl, ".editor-content"),
|
||||||
wordAlertEl: getSel(editorEl, '.editor-aviso-word'),
|
wordAlertEl: getSel(editorEl, ".editor-aviso-word"),
|
||||||
htmlEl: getSel(editorEl, 'textarea'),
|
htmlEl: getSel(editorEl, "textarea"),
|
||||||
}
|
};
|
||||||
console.debug('iniciando editor', editor)
|
console.debug("iniciando editor", editor);
|
||||||
|
|
||||||
// Recuperar el contenido si hay algo guardado, si tuviéramos un campo
|
// 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
|
// de última edición podríamos saber si el artículo fue editado
|
||||||
// después o la versión local es la última.
|
// después o la versión local es la última.
|
||||||
//
|
//
|
||||||
// TODO: Preguntar si se lo quiere recuperar.
|
// TODO: Preguntar si se lo quiere recuperar.
|
||||||
restoreContent(editor)
|
restoreContent(editor);
|
||||||
|
|
||||||
// Word alert
|
// Word alert
|
||||||
editor.contentEl.addEventListener('paste', () => {
|
editor.contentEl.addEventListener("paste", () => {
|
||||||
editor.wordAlertEl.style.display = 'block'
|
editor.wordAlertEl.style.display = "block";
|
||||||
})
|
});
|
||||||
|
|
||||||
// Setup routine listeners
|
// Setup routine listeners
|
||||||
const observer = new MutationObserver(() => routine(editor))
|
const observer = new MutationObserver(() => routine(editor));
|
||||||
observer.observe(editor.contentEl, {
|
observer.observe(editor.contentEl, {
|
||||||
childList: true,
|
childList: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
characterData: true,
|
characterData: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
document.addEventListener("selectionchange", () => routine(editor))
|
document.addEventListener("selectionchange", () => routine(editor));
|
||||||
|
|
||||||
// Capture onClick
|
// Capture onClick
|
||||||
editor.contentEl.addEventListener('click', event => {
|
editor.contentEl.addEventListener(
|
||||||
const target = event.target! as Element
|
"click",
|
||||||
const type = getType(target)
|
(event) => {
|
||||||
|
const target = event.target! as Element;
|
||||||
|
const type = getType(target);
|
||||||
if (!type || !type.type.onClick) {
|
if (!type || !type.type.onClick) {
|
||||||
setAuxiliaryToolbar(editor, null)
|
setAuxiliaryToolbar(editor, null);
|
||||||
clearSelected(editor)
|
clearSelected(editor);
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
type.type.onClick(editor, target)
|
type.type.onClick(editor, target);
|
||||||
return false
|
return false;
|
||||||
}, true)
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Clean seleted
|
// Clean seleted
|
||||||
const selectedEl = editor.contentEl.querySelector('[data-editor-selected]')
|
const selectedEl = editor.contentEl.querySelector("[data-editor-selected]");
|
||||||
if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected
|
if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected;
|
||||||
|
|
||||||
// Setup botones
|
// Setup botones
|
||||||
setupMarksButtons(editor)
|
setupMarksButtons(editor);
|
||||||
setupBlocksButtons(editor)
|
setupBlocksButtons(editor);
|
||||||
setupParentBlocksButtons(editor)
|
setupParentBlocksButtons(editor);
|
||||||
setupMultimediaButtons(editor)
|
setupMultimediaButtons(editor);
|
||||||
|
|
||||||
setupLinkAuxiliaryToolbar(editor)
|
setupLinkAuxiliaryToolbar(editor);
|
||||||
setupMultimediaAuxiliaryToolbar(editor)
|
setupMultimediaAuxiliaryToolbar(editor);
|
||||||
setupMarkAuxiliaryToolbar(editor)
|
setupMarkAuxiliaryToolbar(editor);
|
||||||
|
|
||||||
// Finally...
|
// Finally...
|
||||||
routine(editor)
|
routine(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("turbolinks:load", () => {
|
document.addEventListener("turbolinks:load", () => {
|
||||||
const flash = document.querySelector<HTMLElement>('.js-flash')
|
const flash = document.querySelector<HTMLElement>(".js-flash");
|
||||||
|
|
||||||
if (flash) {
|
if (flash) {
|
||||||
const keys = JSON.parse(flash.dataset.keys || '[]')
|
const keys = JSON.parse(flash.dataset.keys || "[]");
|
||||||
|
|
||||||
switch (flash.dataset.target) {
|
switch (flash.dataset.target) {
|
||||||
case 'editor':
|
case "editor":
|
||||||
switch (flash.dataset.action) {
|
switch (flash.dataset.action) {
|
||||||
case 'forget-content':
|
case "forget-content":
|
||||||
keys.forEach(forgetContent)
|
keys.forEach(forgetContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const editorEl of document.querySelectorAll<HTMLElement>('.editor[data-editor]')) {
|
for (const editorEl of document.querySelectorAll<HTMLElement>(
|
||||||
|
".editor[data-editor]"
|
||||||
|
)) {
|
||||||
try {
|
try {
|
||||||
setupEditor(editorEl)
|
setupEditor(editorEl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: mostrar error
|
// TODO: mostrar error
|
||||||
console.error('no se pudo iniciar el editor, error completo', error)
|
console.error("no se pudo iniciar el editor, error completo", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Guarda una copia local de los cambios para poder recuperarlos
|
* Guarda una copia local de los cambios para poder recuperarlos
|
||||||
|
@ -6,27 +6,33 @@ import { Editor } from 'editor/editor'
|
||||||
*
|
*
|
||||||
* Usamos la URL completa sin anchors.
|
* Usamos la URL completa sin anchors.
|
||||||
*/
|
*/
|
||||||
function getStorageKey (editor: Editor): string {
|
function getStorageKey(editor: Editor): string {
|
||||||
const keyEl = editor.editorEl.querySelector<HTMLInputElement>('[data-target="storage-key"]')
|
const keyEl = editor.editorEl.querySelector<HTMLInputElement>(
|
||||||
if (!keyEl) throw new Error('No encuentro la llave para guardar los artículos')
|
'[data-target="storage-key"]'
|
||||||
return keyEl.value
|
);
|
||||||
|
if (!keyEl)
|
||||||
|
throw new Error("No encuentro la llave para guardar los artículos");
|
||||||
|
return keyEl.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function forgetContent (storedKey: string): void {
|
export function forgetContent(storedKey: string): void {
|
||||||
window.localStorage.removeItem(storedKey)
|
window.localStorage.removeItem(storedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storeContent (editor: Editor): void {
|
export function storeContent(editor: Editor): void {
|
||||||
if (editor.contentEl.innerText.trim().length === 0) return
|
if (editor.contentEl.innerText.trim().length === 0) return;
|
||||||
|
|
||||||
window.localStorage.setItem(getStorageKey(editor), editor.contentEl.innerHTML)
|
window.localStorage.setItem(
|
||||||
|
getStorageKey(editor),
|
||||||
|
editor.contentEl.innerHTML
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreContent (editor: Editor): void {
|
export function restoreContent(editor: Editor): void {
|
||||||
const content = window.localStorage.getItem(getStorageKey(editor))
|
const content = window.localStorage.getItem(getStorageKey(editor));
|
||||||
|
|
||||||
if (!content) return
|
if (!content) return;
|
||||||
if (content.trim().length === 0) return
|
if (content.trim().length === 0) return;
|
||||||
|
|
||||||
editor.contentEl.innerHTML = content
|
editor.contentEl.innerHTML = content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import { marks } from 'editor/types/marks'
|
import { marks } from "editor/types/marks";
|
||||||
import { blocks, li, EditorBlock } from 'editor/types/blocks'
|
import { blocks, li, EditorBlock } from "editor/types/blocks";
|
||||||
import { parentBlocks } from 'editor/types/parentBlocks'
|
import { parentBlocks } from "editor/types/parentBlocks";
|
||||||
import { multimedia } from 'editor/types/multimedia'
|
import { multimedia } from "editor/types/multimedia";
|
||||||
import { blockNames, parentBlockNames, safeGetRangeAt, safeGetSelection } from 'editor/utils'
|
import {
|
||||||
|
blockNames,
|
||||||
|
parentBlockNames,
|
||||||
|
safeGetRangeAt,
|
||||||
|
safeGetSelection,
|
||||||
|
} from "editor/utils";
|
||||||
|
|
||||||
export interface EditorNode {
|
export interface EditorNode {
|
||||||
selector: string,
|
selector: string;
|
||||||
// la string es el nombre en la gran lista de types O 'text'
|
// 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,
|
// 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
|
// quizás podemos hacer que esto sea una función que retorna bool
|
||||||
allowedChildren: string[] | 'ignore-children',
|
allowedChildren: string[] | "ignore-children";
|
||||||
|
|
||||||
// * si es 'do-nothing', no hace nada si está vacío (esto es para cuando
|
// * 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)
|
// permitís 'text' entonces se puede tipear adentro, ej: párrafo vacío)
|
||||||
|
@ -18,15 +23,15 @@ export interface EditorNode {
|
||||||
// ej: strong: { handleNothing: 'remove' }
|
// ej: strong: { handleNothing: 'remove' }
|
||||||
// * si es un block, insertamos el bloque y movemos la selección ahí
|
// * si es un block, insertamos el bloque y movemos la selección ahí
|
||||||
// ej: ul: { handleNothing: li }
|
// ej: ul: { handleNothing: li }
|
||||||
handleEmpty: 'do-nothing' | 'remove' | EditorBlock,
|
handleEmpty: "do-nothing" | "remove" | EditorBlock;
|
||||||
|
|
||||||
// esta función puede ser llamada para cosas que no necesariamente sea la
|
// 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
|
// 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
|
// el formato. esto es importante por que, por ejemplo, no deberíamos
|
||||||
// cambiar la selección acá.
|
// cambiar la selección acá.
|
||||||
create: (editor: Editor) => HTMLElement,
|
create: (editor: Editor) => HTMLElement;
|
||||||
|
|
||||||
onClick?: (editor: Editor, target: Element) => void,
|
onClick?: (editor: Editor, target: Element) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const types: { [propName: string]: EditorNode } = {
|
export const types: { [propName: string]: EditorNode } = {
|
||||||
|
@ -35,92 +40,101 @@ export const types: { [propName: string]: EditorNode } = {
|
||||||
li,
|
li,
|
||||||
...parentBlocks,
|
...parentBlocks,
|
||||||
contentEl: {
|
contentEl: {
|
||||||
selector: '.editor-content',
|
selector: ".editor-content",
|
||||||
allowedChildren: [...blockNames, ...parentBlockNames, 'multimedia'],
|
allowedChildren: [...blockNames, ...parentBlockNames, "multimedia"],
|
||||||
handleEmpty: blocks.paragraph,
|
handleEmpty: blocks.paragraph,
|
||||||
create: () => { throw new Error('se intentó crear contentEl') }
|
create: () => {
|
||||||
|
throw new Error("se intentó crear contentEl");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
br: {
|
br: {
|
||||||
selector: 'br',
|
selector: "br",
|
||||||
allowedChildren: [],
|
allowedChildren: [],
|
||||||
handleEmpty: 'do-nothing',
|
handleEmpty: "do-nothing",
|
||||||
create: () => { throw new Error('se intentó crear br') }
|
create: () => {
|
||||||
|
throw new Error("se intentó crear br");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
multimedia,
|
multimedia,
|
||||||
}
|
};
|
||||||
|
|
||||||
export function getType (node: Element): { typeName: string, type: EditorNode } | null {
|
export function getType(
|
||||||
|
node: Element
|
||||||
|
): { typeName: string; type: EditorNode } | null {
|
||||||
for (let [typeName, type] of Object.entries(types)) {
|
for (let [typeName, type] of Object.entries(types)) {
|
||||||
if (node.matches(type.selector)) {
|
if (node.matches(type.selector)) {
|
||||||
return { typeName, type }
|
return { typeName, type };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// encuentra el primer pariente que pueda tener al type, y retorna un array
|
// encuentra el primer pariente que pueda tener al type, y retorna un array
|
||||||
// donde
|
// donde
|
||||||
// array[0] = elemento que matchea el type
|
// array[0] = elemento que matchea el type
|
||||||
// array[array.len - 1] = primer elemento seleccionado
|
// array[array.len - 1] = primer elemento seleccionado
|
||||||
export function getValidParentInSelection (args: {
|
export function getValidParentInSelection(args: {
|
||||||
editor: Editor,
|
editor: Editor;
|
||||||
type: string,
|
type: string;
|
||||||
}): Element[] {
|
}): Element[] {
|
||||||
const sel = safeGetSelection(args.editor)
|
const sel = safeGetSelection(args.editor);
|
||||||
if (!sel) throw new Error('No se donde insertar esto')
|
if (!sel) throw new Error("No se donde insertar esto");
|
||||||
const range = safeGetRangeAt(sel)
|
const range = safeGetRangeAt(sel);
|
||||||
if (!range) throw new Error('No se donde insertar esto')
|
if (!range) throw new Error("No se donde insertar esto");
|
||||||
|
|
||||||
let list: Element[] = []
|
let list: Element[] = [];
|
||||||
|
|
||||||
if (!sel.anchorNode) {
|
if (!sel.anchorNode) {
|
||||||
throw new Error('No se donde insertar esto')
|
throw new Error("No se donde insertar esto");
|
||||||
} else if (sel.anchorNode instanceof Element) {
|
} else if (sel.anchorNode instanceof Element) {
|
||||||
list = [sel.anchorNode]
|
list = [sel.anchorNode];
|
||||||
} else if (sel.anchorNode.parentElement) {
|
} else if (sel.anchorNode.parentElement) {
|
||||||
list = [sel.anchorNode.parentElement]
|
list = [sel.anchorNode.parentElement];
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No se donde insertar esto')
|
throw new Error("No se donde insertar esto");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const el = list[0]
|
const el = list[0];
|
||||||
if (!args.editor.contentEl.contains(el)
|
if (!args.editor.contentEl.contains(el) && el != args.editor.contentEl)
|
||||||
&& el != args.editor.contentEl)
|
throw new Error("No se donde insertar esto");
|
||||||
throw new Error('No se donde insertar esto')
|
const type = getType(el);
|
||||||
const type = getType(el)
|
|
||||||
|
|
||||||
if (type) {
|
if (type) {
|
||||||
//if (type.typeName === 'contentEl') break
|
//if (type.typeName === 'contentEl') break
|
||||||
//if (parentBlockNames.includes(type.typeName)) break
|
//if (parentBlockNames.includes(type.typeName)) break
|
||||||
if ((type.type.allowedChildren instanceof Array)
|
if (
|
||||||
&& type.type.allowedChildren.includes(args.type)) break
|
type.type.allowedChildren instanceof Array &&
|
||||||
|
type.type.allowedChildren.includes(args.type)
|
||||||
|
)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
list = [el.parentElement, ...list]
|
list = [el.parentElement, ...list];
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No se donde insertar esto')
|
throw new Error("No se donde insertar esto");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValidChildren (node: Element, type: EditorNode): Node[] {
|
export function getValidChildren(node: Element, type: EditorNode): Node[] {
|
||||||
if (type.allowedChildren === 'ignore-children')
|
if (type.allowedChildren === "ignore-children")
|
||||||
throw new Error('se llamó a getValidChildren con un type que no lo permite!')
|
throw new Error(
|
||||||
return [...node.childNodes].filter(n => {
|
"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
|
// si permite texto y esto es un texto, es válido
|
||||||
if (n.nodeType === Node.TEXT_NODE)
|
if (n.nodeType === Node.TEXT_NODE)
|
||||||
return type.allowedChildren.includes('text') && n.textContent?.length
|
return type.allowedChildren.includes("text") && n.textContent?.length;
|
||||||
|
|
||||||
// si no es un elemento, no es válido
|
// si no es un elemento, no es válido
|
||||||
if (!(n instanceof Element))
|
if (!(n instanceof Element)) return false;
|
||||||
return false
|
|
||||||
|
|
||||||
const t = getType(n)
|
const t = getType(n);
|
||||||
if (!t) return false
|
if (!t) return false;
|
||||||
return type.allowedChildren.includes(t.typeName)
|
return type.allowedChildren.includes(t.typeName);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,72 +1,76 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection,
|
||||||
|
safeGetRangeAt,
|
||||||
moveChildren,
|
moveChildren,
|
||||||
markNames, blockNames, parentBlockNames,
|
markNames,
|
||||||
} from 'editor/utils'
|
blockNames,
|
||||||
import { EditorNode, getType, getValidParentInSelection } from 'editor/types'
|
parentBlockNames,
|
||||||
|
} from "editor/utils";
|
||||||
|
import { EditorNode, getType, getValidParentInSelection } from "editor/types";
|
||||||
|
|
||||||
export interface EditorBlock extends EditorNode {
|
export interface EditorBlock extends EditorNode {}
|
||||||
}
|
|
||||||
|
|
||||||
function makeBlock (tag: string): EditorBlock {
|
function makeBlock(tag: string): EditorBlock {
|
||||||
return {
|
return {
|
||||||
selector: tag,
|
selector: tag,
|
||||||
allowedChildren: [...markNames, 'text'],
|
allowedChildren: [...markNames, "text"],
|
||||||
handleEmpty: 'do-nothing',
|
handleEmpty: "do-nothing",
|
||||||
create: () => document.createElement(tag),
|
create: () => document.createElement(tag),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const li: EditorBlock = makeBlock('li')
|
export const li: EditorBlock = makeBlock("li");
|
||||||
|
|
||||||
// XXX: si agregás algo acá, agregalo a blockNames
|
// XXX: si agregás algo acá, agregalo a blockNames
|
||||||
// (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml)
|
// (y probablemente le quieras hacer un botón en app/views/posts/attributes/_content.haml)
|
||||||
export const blocks: { [propName: string]: EditorBlock } = {
|
export const blocks: { [propName: string]: EditorBlock } = {
|
||||||
paragraph: makeBlock('p'),
|
paragraph: makeBlock("p"),
|
||||||
h1: makeBlock('h1'),
|
h1: makeBlock("h1"),
|
||||||
h2: makeBlock('h2'),
|
h2: makeBlock("h2"),
|
||||||
h3: makeBlock('h3'),
|
h3: makeBlock("h3"),
|
||||||
h4: makeBlock('h4'),
|
h4: makeBlock("h4"),
|
||||||
h5: makeBlock('h5'),
|
h5: makeBlock("h5"),
|
||||||
h6: makeBlock('h6'),
|
h6: makeBlock("h6"),
|
||||||
unordered_list: {
|
unordered_list: {
|
||||||
...makeBlock('ul'),
|
...makeBlock("ul"),
|
||||||
allowedChildren: ['li'],
|
allowedChildren: ["li"],
|
||||||
handleEmpty: li,
|
handleEmpty: li,
|
||||||
},
|
},
|
||||||
ordered_list: {
|
ordered_list: {
|
||||||
...makeBlock('ol'),
|
...makeBlock("ol"),
|
||||||
allowedChildren: ['li'],
|
allowedChildren: ["li"],
|
||||||
handleEmpty: li,
|
handleEmpty: li,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export function setupButtons (editor: Editor): void {
|
export function setupButtons(editor: Editor): void {
|
||||||
for (const [ name, type ] of Object.entries(blocks)) {
|
for (const [name, type] of Object.entries(blocks)) {
|
||||||
const buttonEl = editor.toolbarEl.querySelector(`[data-editor-button="block-${name}"]`)
|
const buttonEl = editor.toolbarEl.querySelector(
|
||||||
if (!buttonEl) continue
|
`[data-editor-button="block-${name}"]`
|
||||||
buttonEl.addEventListener("click", event => {
|
);
|
||||||
event.preventDefault()
|
if (!buttonEl) continue;
|
||||||
|
buttonEl.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const list = getValidParentInSelection({ editor, type: name })
|
const list = getValidParentInSelection({ editor, type: name });
|
||||||
|
|
||||||
// No borrar cosas como multimedia
|
// No borrar cosas como multimedia
|
||||||
if (blockNames.indexOf(getType(list[1])!.typeName) === -1) {
|
if (blockNames.indexOf(getType(list[1])!.typeName) === -1) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let replacementType = list[1].matches(type.selector)
|
let replacementType = list[1].matches(type.selector)
|
||||||
? blocks.paragraph
|
? blocks.paragraph
|
||||||
: type
|
: type;
|
||||||
|
|
||||||
const el = replacementType.create(editor)
|
const el = replacementType.create(editor);
|
||||||
replacementType.onClick && replacementType.onClick(editor, el)
|
replacementType.onClick && replacementType.onClick(editor, el);
|
||||||
moveChildren(list[1], el, null)
|
moveChildren(list[1], el, null);
|
||||||
list[0].replaceChild(el, list[1])
|
list[0].replaceChild(el, list[1]);
|
||||||
window.getSelection()?.collapse(el)
|
window.getSelection()?.collapse(el);
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import { EditorNode } from 'editor/types'
|
import { EditorNode } from "editor/types";
|
||||||
import { markNames, setAuxiliaryToolbar, clearSelected } from 'editor/utils'
|
import { markNames, setAuxiliaryToolbar, clearSelected } from "editor/utils";
|
||||||
|
|
||||||
function select (editor: Editor, el: HTMLAnchorElement): void {
|
function select(editor: Editor, el: HTMLAnchorElement): void {
|
||||||
clearSelected(editor)
|
clearSelected(editor);
|
||||||
el.dataset.editorSelected = ''
|
el.dataset.editorSelected = "";
|
||||||
editor.toolbar.auxiliary.link.urlEl.value = el.href
|
editor.toolbar.auxiliary.link.urlEl.value = el.href;
|
||||||
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.link.parentEl)
|
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.link.parentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const link: EditorNode = {
|
export const link: EditorNode = {
|
||||||
selector: 'a',
|
selector: "a",
|
||||||
allowedChildren: [...markNames.filter(n => n !== 'link'), 'text'],
|
allowedChildren: [...markNames.filter((n) => n !== "link"), "text"],
|
||||||
handleEmpty: 'remove',
|
handleEmpty: "remove",
|
||||||
create: () => document.createElement('a'),
|
create: () => document.createElement("a"),
|
||||||
onClick (editor, el) {
|
onClick(editor, el) {
|
||||||
if (!(el instanceof HTMLAnchorElement))
|
if (!(el instanceof HTMLAnchorElement)) throw new Error("oh no");
|
||||||
throw new Error('oh no')
|
select(editor, el);
|
||||||
select(editor, el)
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export function setupAuxiliaryToolbar (editor: Editor): void {
|
export function setupAuxiliaryToolbar(editor: Editor): void {
|
||||||
editor.toolbar.auxiliary.link.urlEl.addEventListener('input', event => {
|
editor.toolbar.auxiliary.link.urlEl.addEventListener("input", (event) => {
|
||||||
const url = editor.toolbar.auxiliary.link.urlEl.value
|
const url = editor.toolbar.auxiliary.link.urlEl.value;
|
||||||
const selectedEl = editor.contentEl
|
const selectedEl = editor.contentEl.querySelector<HTMLAnchorElement>(
|
||||||
.querySelector<HTMLAnchorElement>('a[data-editor-selected]')
|
"a[data-editor-selected]"
|
||||||
|
);
|
||||||
if (!selectedEl)
|
if (!selectedEl)
|
||||||
throw new Error('No pude encontrar el link para setear el enlace')
|
throw new Error("No pude encontrar el link para setear el enlace");
|
||||||
|
|
||||||
selectedEl.href = url
|
selectedEl.href = url;
|
||||||
})
|
});
|
||||||
editor.toolbar.auxiliary.link.urlEl.addEventListener('keydown', event => {
|
editor.toolbar.auxiliary.link.urlEl.addEventListener("keydown", (event) => {
|
||||||
if (event.keyCode == 13) event.preventDefault()
|
if (event.keyCode == 13) event.preventDefault();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,48 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import { EditorNode } from 'editor/types'
|
import { EditorNode } from "editor/types";
|
||||||
import { markNames, setAuxiliaryToolbar, clearSelected } from 'editor/utils'
|
import { markNames, setAuxiliaryToolbar, clearSelected } from "editor/utils";
|
||||||
|
|
||||||
const hex = (x: string) => ("0" + parseInt(x).toString(16)).slice(-2)
|
const hex = (x: string) => ("0" + parseInt(x).toString(16)).slice(-2);
|
||||||
// https://stackoverflow.com/a/3627747
|
// https://stackoverflow.com/a/3627747
|
||||||
// TODO: cambiar por una solución más copada
|
// TODO: cambiar por una solución más copada
|
||||||
function rgbToHex (rgb: string): string {
|
function rgbToHex(rgb: string): string {
|
||||||
const matches = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
|
const matches = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||||
if (!matches) throw new Error('no pude parsear el rgb()')
|
if (!matches) throw new Error("no pude parsear el rgb()");
|
||||||
return "#" + hex(matches[1]) + hex(matches[2]) + hex(matches[3])
|
return "#" + hex(matches[1]) + hex(matches[2]) + hex(matches[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function select (editor: Editor, el: HTMLElement): void {
|
function select(editor: Editor, el: HTMLElement): void {
|
||||||
clearSelected(editor)
|
clearSelected(editor);
|
||||||
el.dataset.editorSelected = ''
|
el.dataset.editorSelected = "";
|
||||||
editor.toolbar.auxiliary.mark.colorEl.value
|
editor.toolbar.auxiliary.mark.colorEl.value = el.style.backgroundColor
|
||||||
= el.style.backgroundColor
|
|
||||||
? rgbToHex(el.style.backgroundColor)
|
? rgbToHex(el.style.backgroundColor)
|
||||||
: '#f206f9'
|
: "#f206f9";
|
||||||
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.mark.parentEl)
|
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.mark.parentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mark: EditorNode = {
|
export const mark: EditorNode = {
|
||||||
selector: 'mark',
|
selector: "mark",
|
||||||
allowedChildren: [...markNames.filter(n => n !== 'mark'), 'text'],
|
allowedChildren: [...markNames.filter((n) => n !== "mark"), "text"],
|
||||||
handleEmpty: 'remove',
|
handleEmpty: "remove",
|
||||||
create: () => document.createElement('mark'),
|
create: () => document.createElement("mark"),
|
||||||
onClick (editor, el) {
|
onClick(editor, el) {
|
||||||
if (!(el instanceof HTMLElement))
|
if (!(el instanceof HTMLElement)) throw new Error("oh no");
|
||||||
throw new Error('oh no')
|
select(editor, el);
|
||||||
select(editor, el)
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export function setupAuxiliaryToolbar (editor: Editor): void {
|
export function setupAuxiliaryToolbar(editor: Editor): void {
|
||||||
editor.toolbar.auxiliary.mark.colorEl.addEventListener('input', event => {
|
editor.toolbar.auxiliary.mark.colorEl.addEventListener("input", (event) => {
|
||||||
const color = editor.toolbar.auxiliary.mark.colorEl.value
|
const color = editor.toolbar.auxiliary.mark.colorEl.value;
|
||||||
const selectedEl = editor.contentEl
|
const selectedEl = editor.contentEl.querySelector<HTMLElement>(
|
||||||
.querySelector<HTMLElement>('mark[data-editor-selected]')
|
"mark[data-editor-selected]"
|
||||||
|
);
|
||||||
if (!selectedEl)
|
if (!selectedEl)
|
||||||
throw new Error('No pude encontrar el mark para setear el color')
|
throw new Error("No pude encontrar el mark para setear el color");
|
||||||
|
|
||||||
selectedEl.style.backgroundColor = color
|
selectedEl.style.backgroundColor = color;
|
||||||
})
|
});
|
||||||
editor.toolbar.auxiliary.mark.colorEl.addEventListener('keydown', event => {
|
editor.toolbar.auxiliary.mark.colorEl.addEventListener("keydown", (event) => {
|
||||||
if (event.keyCode == 13) event.preventDefault()
|
if (event.keyCode == 13) event.preventDefault();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,102 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import { EditorNode } from 'editor/types'
|
import { EditorNode } from "editor/types";
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection,
|
||||||
|
safeGetRangeAt,
|
||||||
moveChildren,
|
moveChildren,
|
||||||
markNames,
|
markNames,
|
||||||
} from 'editor/utils'
|
} from "editor/utils";
|
||||||
import { link } from 'editor/types/link'
|
import { link } from "editor/types/link";
|
||||||
import { mark } from 'editor/types/mark'
|
import { mark } from "editor/types/mark";
|
||||||
|
|
||||||
function makeMark (name: string, tag: string): EditorNode {
|
function makeMark(name: string, tag: string): EditorNode {
|
||||||
return {
|
return {
|
||||||
selector: tag,
|
selector: tag,
|
||||||
allowedChildren: [...markNames.filter(n => n !== name), 'text'],
|
allowedChildren: [...markNames.filter((n) => n !== name), "text"],
|
||||||
handleEmpty: 'remove',
|
handleEmpty: "remove",
|
||||||
create: () => document.createElement(tag),
|
create: () => document.createElement(tag),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: si agregás algo acá, agregalo a markNames
|
// XXX: si agregás algo acá, agregalo a markNames
|
||||||
export const marks: { [propName: string]: EditorNode } = {
|
export const marks: { [propName: string]: EditorNode } = {
|
||||||
bold: makeMark('bold', 'strong'),
|
bold: makeMark("bold", "strong"),
|
||||||
italic: makeMark('italic', 'em'),
|
italic: makeMark("italic", "em"),
|
||||||
deleted: makeMark('deleted', 'del'),
|
deleted: makeMark("deleted", "del"),
|
||||||
underline: makeMark('underline', 'u'),
|
underline: makeMark("underline", "u"),
|
||||||
sub: makeMark('sub', 'sub'),
|
sub: makeMark("sub", "sub"),
|
||||||
super: makeMark('super', 'sup'),
|
super: makeMark("super", "sup"),
|
||||||
mark,
|
mark,
|
||||||
link,
|
link,
|
||||||
small: makeMark('small', 'small'),
|
small: makeMark("small", "small"),
|
||||||
}
|
};
|
||||||
|
|
||||||
function recursiveFilterSelection (
|
function recursiveFilterSelection(
|
||||||
node: Element,
|
node: Element,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
selector: string,
|
selector: string
|
||||||
): Element[] {
|
): Element[] {
|
||||||
let output: Element[] = []
|
let output: Element[] = [];
|
||||||
for (const child of [...node.children]) {
|
for (const child of [...node.children]) {
|
||||||
if (child.matches(selector)
|
if (child.matches(selector) && selection.containsNode(child))
|
||||||
&& selection.containsNode(child)
|
output.push(child);
|
||||||
) output.push(child)
|
output = [
|
||||||
output = [...output, ...recursiveFilterSelection(child, selection, selector)]
|
...output,
|
||||||
|
...recursiveFilterSelection(child, selection, selector),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return output
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupButtons (editor: Editor): void {
|
export function setupButtons(editor: Editor): void {
|
||||||
for (const [ name, type ] of Object.entries(marks)) {
|
for (const [name, type] of Object.entries(marks)) {
|
||||||
const buttonEl = editor.toolbarEl.querySelector(`[data-editor-button="mark-${name}"]`)
|
const buttonEl = editor.toolbarEl.querySelector(
|
||||||
if (!buttonEl) continue
|
`[data-editor-button="mark-${name}"]`
|
||||||
buttonEl.addEventListener("click", event => {
|
);
|
||||||
event.preventDefault()
|
if (!buttonEl) continue;
|
||||||
|
buttonEl.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const sel = safeGetSelection(editor)
|
const sel = safeGetSelection(editor);
|
||||||
if (!sel) return
|
if (!sel) return;
|
||||||
const range = safeGetRangeAt(sel)
|
const range = safeGetRangeAt(sel);
|
||||||
if (!range) return
|
if (!range) return;
|
||||||
|
|
||||||
let parentEl = range.commonAncestorContainer
|
let parentEl = range.commonAncestorContainer;
|
||||||
while (!(parentEl instanceof Element)) {
|
while (!(parentEl instanceof Element)) {
|
||||||
if (!parentEl.parentElement) return
|
if (!parentEl.parentElement) return;
|
||||||
parentEl = parentEl.parentElement
|
parentEl = parentEl.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingMarks = recursiveFilterSelection(
|
const existingMarks = recursiveFilterSelection(
|
||||||
parentEl,
|
parentEl,
|
||||||
sel,
|
sel,
|
||||||
type.selector,
|
type.selector
|
||||||
)
|
);
|
||||||
console.debug('marks encontradas:', existingMarks)
|
console.debug("marks encontradas:", existingMarks);
|
||||||
|
|
||||||
if (existingMarks.length > 0) {
|
if (existingMarks.length > 0) {
|
||||||
const mark = existingMarks[0]
|
const mark = existingMarks[0];
|
||||||
if (!mark.parentElement)
|
if (!mark.parentElement) throw new Error(":/");
|
||||||
throw new Error(':/')
|
moveChildren(mark, mark.parentElement, mark);
|
||||||
moveChildren(mark, mark.parentElement, mark)
|
mark.parentElement.removeChild(mark);
|
||||||
mark.parentElement.removeChild(mark)
|
|
||||||
} else {
|
} else {
|
||||||
if (range.commonAncestorContainer === editor.contentEl)
|
if (range.commonAncestorContainer === editor.contentEl)
|
||||||
// TODO: mostrar error
|
// TODO: mostrar error
|
||||||
return console.error("No puedo marcar cosas a través de distintos bloques!")
|
return console.error(
|
||||||
|
"No puedo marcar cosas a través de distintos bloques!"
|
||||||
|
);
|
||||||
|
|
||||||
const tagEl = type.create(editor)
|
const tagEl = type.create(editor);
|
||||||
type.onClick && type.onClick(editor, tagEl)
|
type.onClick && type.onClick(editor, tagEl);
|
||||||
|
|
||||||
tagEl.appendChild(range.extractContents())
|
tagEl.appendChild(range.extractContents());
|
||||||
|
|
||||||
range.insertNode(tagEl)
|
range.insertNode(tagEl);
|
||||||
range.selectNode(tagEl)
|
range.selectNode(tagEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,69 @@
|
||||||
import * as ActiveStorage from '@rails/activestorage'
|
import * as ActiveStorage from "@rails/activestorage";
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import { EditorNode, getValidParentInSelection } from 'editor/types'
|
import { EditorNode, getValidParentInSelection } from "editor/types";
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection,
|
||||||
markNames, parentBlockNames,
|
safeGetRangeAt,
|
||||||
setAuxiliaryToolbar, clearSelected,
|
markNames,
|
||||||
} from 'editor/utils'
|
parentBlockNames,
|
||||||
|
setAuxiliaryToolbar,
|
||||||
|
clearSelected,
|
||||||
|
} from "editor/utils";
|
||||||
|
|
||||||
function uploadFile (file: File): Promise<string> {
|
function uploadFile(file: File): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const upload = new ActiveStorage.DirectUpload(
|
const upload = new ActiveStorage.DirectUpload(
|
||||||
file,
|
file,
|
||||||
origin + '/rails/active_storage/direct_uploads',
|
origin + "/rails/active_storage/direct_uploads"
|
||||||
)
|
);
|
||||||
|
|
||||||
upload.create((error: any, blob: any) => {
|
upload.create((error: any, blob: any) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
const url = `${origin}/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`
|
const url = `${origin}/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`;
|
||||||
resolve(url)
|
resolve(url);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAlt (multimediaInnerEl: HTMLElement): string | null {
|
function getAlt(multimediaInnerEl: HTMLElement): string | null {
|
||||||
switch (multimediaInnerEl.tagName) {
|
switch (multimediaInnerEl.tagName) {
|
||||||
case 'VIDEO':
|
case "VIDEO":
|
||||||
case 'AUDIO':
|
case "AUDIO":
|
||||||
return multimediaInnerEl.getAttribute('aria-label')
|
return multimediaInnerEl.getAttribute("aria-label");
|
||||||
case 'IMG':
|
case "IMG":
|
||||||
return (multimediaInnerEl as HTMLImageElement).alt
|
return (multimediaInnerEl as HTMLImageElement).alt;
|
||||||
case 'IFRAME':
|
case "IFRAME":
|
||||||
return multimediaInnerEl.title
|
return multimediaInnerEl.title;
|
||||||
default:
|
default:
|
||||||
throw new Error('no pude conseguir el alt')
|
throw new Error("no pude conseguir el alt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function setAlt (multimediaInnerEl: HTMLElement, value: string): void {
|
function setAlt(multimediaInnerEl: HTMLElement, value: string): void {
|
||||||
switch (multimediaInnerEl.tagName) {
|
switch (multimediaInnerEl.tagName) {
|
||||||
case 'VIDEO':
|
case "VIDEO":
|
||||||
case 'AUDIO':
|
case "AUDIO":
|
||||||
multimediaInnerEl.setAttribute('aria-label', value)
|
multimediaInnerEl.setAttribute("aria-label", value);
|
||||||
break
|
break;
|
||||||
case 'IMG':
|
case "IMG":
|
||||||
(multimediaInnerEl as HTMLImageElement).alt = value
|
(multimediaInnerEl as HTMLImageElement).alt = value;
|
||||||
break
|
break;
|
||||||
case 'IFRAME':
|
case "IFRAME":
|
||||||
multimediaInnerEl.title = value
|
multimediaInnerEl.title = value;
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('no pude setear el alt')
|
throw new Error("no pude setear el alt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function select (editor: Editor, el: HTMLElement): void {
|
function select(editor: Editor, el: HTMLElement): void {
|
||||||
clearSelected(editor)
|
clearSelected(editor);
|
||||||
el.dataset.editorSelected = ''
|
el.dataset.editorSelected = "";
|
||||||
|
|
||||||
const innerEl = el.querySelector<HTMLElement>('[data-multimedia-inner]')
|
const innerEl = el.querySelector<HTMLElement>("[data-multimedia-inner]");
|
||||||
if (!innerEl) throw new Error('No hay multimedia válida')
|
if (!innerEl) throw new Error("No hay multimedia válida");
|
||||||
if (innerEl.tagName === "P") {
|
if (innerEl.tagName === "P") {
|
||||||
editor.toolbar.auxiliary.multimedia.altEl.value = "";
|
editor.toolbar.auxiliary.multimedia.altEl.value = "";
|
||||||
editor.toolbar.auxiliary.multimedia.altEl.disabled = true;
|
editor.toolbar.auxiliary.multimedia.altEl.disabled = true;
|
||||||
|
@ -69,138 +72,159 @@ function select (editor: Editor, el: HTMLElement): void {
|
||||||
editor.toolbar.auxiliary.multimedia.altEl.disabled = false;
|
editor.toolbar.auxiliary.multimedia.altEl.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.multimedia.parentEl)
|
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.multimedia.parentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const multimedia: EditorNode = {
|
export const multimedia: EditorNode = {
|
||||||
selector: 'figure[data-multimedia]',
|
selector: "figure[data-multimedia]",
|
||||||
allowedChildren: 'ignore-children',
|
allowedChildren: "ignore-children",
|
||||||
handleEmpty: 'remove',
|
handleEmpty: "remove",
|
||||||
create: () => {
|
create: () => {
|
||||||
const figureEl = document.createElement('figure')
|
const figureEl = document.createElement("figure");
|
||||||
figureEl.dataset.multimedia = ''
|
figureEl.dataset.multimedia = "";
|
||||||
figureEl.contentEditable = 'false'
|
figureEl.contentEditable = "false";
|
||||||
|
|
||||||
const placeholderEl = document.createElement('p')
|
const placeholderEl = document.createElement("p");
|
||||||
placeholderEl.dataset.multimediaInner = ''
|
placeholderEl.dataset.multimediaInner = "";
|
||||||
// TODO i18n
|
// TODO i18n
|
||||||
placeholderEl.append('¡Clickeame para subir un archivo!')
|
placeholderEl.append("¡Clickeame para subir un archivo!");
|
||||||
figureEl.appendChild(placeholderEl)
|
figureEl.appendChild(placeholderEl);
|
||||||
|
|
||||||
const descriptionEl = document.createElement('figcaption')
|
const descriptionEl = document.createElement("figcaption");
|
||||||
descriptionEl.contentEditable = 'true'
|
descriptionEl.contentEditable = "true";
|
||||||
// TODO i18n
|
// TODO i18n
|
||||||
descriptionEl.append('Escribí acá la descripción del archivo.')
|
descriptionEl.append("Escribí acá la descripción del archivo.");
|
||||||
figureEl.appendChild(descriptionEl)
|
figureEl.appendChild(descriptionEl);
|
||||||
|
|
||||||
return figureEl
|
return figureEl;
|
||||||
},
|
},
|
||||||
onClick (editor, el) {
|
onClick(editor, el) {
|
||||||
if (!(el instanceof HTMLElement))
|
if (!(el instanceof HTMLElement)) throw new Error("oh no");
|
||||||
throw new Error('oh no')
|
select(editor, el);
|
||||||
select(editor, el)
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
function createElementWithFile (url: string, type: string): HTMLElement {
|
function createElementWithFile(url: string, type: string): HTMLElement {
|
||||||
if (type.match(/^image\/.+$/)) {
|
if (type.match(/^image\/.+$/)) {
|
||||||
const el = document.createElement('img')
|
const el = document.createElement("img");
|
||||||
el.dataset.multimediaInner = ''
|
el.dataset.multimediaInner = "";
|
||||||
el.src = url
|
el.src = url;
|
||||||
return el
|
return el;
|
||||||
} else if (type.match(/^video\/.+$/)) {
|
} else if (type.match(/^video\/.+$/)) {
|
||||||
const el = document.createElement('video')
|
const el = document.createElement("video");
|
||||||
el.controls = true
|
el.controls = true;
|
||||||
el.dataset.multimediaInner = ''
|
el.dataset.multimediaInner = "";
|
||||||
el.src = url
|
el.src = url;
|
||||||
return el
|
return el;
|
||||||
} else if (type.match(/^audio\/.+$/)) {
|
} else if (type.match(/^audio\/.+$/)) {
|
||||||
const el = document.createElement('audio')
|
const el = document.createElement("audio");
|
||||||
el.controls = true
|
el.controls = true;
|
||||||
el.dataset.multimediaInner = ''
|
el.dataset.multimediaInner = "";
|
||||||
el.src = url
|
el.src = url;
|
||||||
return el
|
return el;
|
||||||
} else if (type.match(/^application\/pdf$/)) {
|
} else if (type.match(/^application\/pdf$/)) {
|
||||||
const el = document.createElement('iframe')
|
const el = document.createElement("iframe");
|
||||||
el.dataset.multimediaInner = ''
|
el.dataset.multimediaInner = "";
|
||||||
el.src = url
|
el.src = url;
|
||||||
return el
|
return el;
|
||||||
} else {
|
} else {
|
||||||
// TODO: chequear si el archivo es válido antes de subir
|
// TODO: chequear si el archivo es válido antes de subir
|
||||||
throw new Error('Tipo de archivo no reconocido')
|
throw new Error("Tipo de archivo no reconocido");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupAuxiliaryToolbar (editor: Editor): void {
|
export function setupAuxiliaryToolbar(editor: Editor): void {
|
||||||
editor.toolbar.auxiliary.multimedia.uploadEl.addEventListener('click', event => {
|
editor.toolbar.auxiliary.multimedia.uploadEl.addEventListener(
|
||||||
const files = editor.toolbar.auxiliary.multimedia.fileEl.files
|
"click",
|
||||||
if (!files || !files.length) throw new Error('no hay archivos para subir')
|
(event) => {
|
||||||
const file = files[0]
|
const files = editor.toolbar.auxiliary.multimedia.fileEl.files;
|
||||||
|
if (!files || !files.length)
|
||||||
|
throw new Error("no hay archivos para subir");
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
const selectedEl = editor.contentEl
|
const selectedEl = editor.contentEl.querySelector<HTMLElement>(
|
||||||
.querySelector<HTMLElement>('figure[data-editor-selected]')
|
"figure[data-editor-selected]"
|
||||||
|
);
|
||||||
if (!selectedEl)
|
if (!selectedEl)
|
||||||
throw new Error('No pude encontrar el elemento para setear el archivo')
|
throw new Error("No pude encontrar el elemento para setear el archivo");
|
||||||
|
|
||||||
selectedEl.dataset.editorLoading = ''
|
selectedEl.dataset.editorLoading = "";
|
||||||
uploadFile(file)
|
uploadFile(file)
|
||||||
.then(url => {
|
.then((url) => {
|
||||||
const innerEl = selectedEl.querySelector('[data-multimedia-inner]')
|
const innerEl = selectedEl.querySelector("[data-multimedia-inner]");
|
||||||
if (!innerEl) throw new Error('No hay multimedia a reemplazar')
|
if (!innerEl) throw new Error("No hay multimedia a reemplazar");
|
||||||
|
|
||||||
const el = createElementWithFile(url, file.type)
|
const el = createElementWithFile(url, file.type);
|
||||||
setAlt(el, editor.toolbar.auxiliary.multimedia.altEl.value)
|
setAlt(el, editor.toolbar.auxiliary.multimedia.altEl.value);
|
||||||
selectedEl.replaceChild(el, innerEl)
|
selectedEl.replaceChild(el, innerEl);
|
||||||
|
|
||||||
select(editor, selectedEl)
|
select(editor, selectedEl);
|
||||||
|
|
||||||
delete selectedEl.dataset.editorError
|
delete selectedEl.dataset.editorError;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
// TODO: mostrar error
|
// TODO: mostrar error
|
||||||
selectedEl.dataset.editorError = ''
|
selectedEl.dataset.editorError = "";
|
||||||
})
|
|
||||||
.finally(() => { delete selectedEl.dataset.editorLoading })
|
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
delete selectedEl.dataset.editorLoading;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
editor.toolbar.auxiliary.multimedia.removeEl.addEventListener('click', event => {
|
editor.toolbar.auxiliary.multimedia.removeEl.addEventListener(
|
||||||
const selectedEl = editor.contentEl
|
"click",
|
||||||
.querySelector<HTMLElement>('figure[data-editor-selected]')
|
(event) => {
|
||||||
|
const selectedEl = editor.contentEl.querySelector<HTMLElement>(
|
||||||
|
"figure[data-editor-selected]"
|
||||||
|
);
|
||||||
if (!selectedEl)
|
if (!selectedEl)
|
||||||
throw new Error('No pude encontrar el elemento para borrar')
|
throw new Error("No pude encontrar el elemento para borrar");
|
||||||
|
|
||||||
selectedEl.parentElement?.removeChild(selectedEl)
|
selectedEl.parentElement?.removeChild(selectedEl);
|
||||||
setAuxiliaryToolbar(editor, null)
|
setAuxiliaryToolbar(editor, null);
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
editor.toolbar.auxiliary.multimedia.altEl.addEventListener('input', event => {
|
editor.toolbar.auxiliary.multimedia.altEl.addEventListener(
|
||||||
const selectedEl = editor.contentEl
|
"input",
|
||||||
.querySelector<HTMLAnchorElement>('figure[data-editor-selected]')
|
(event) => {
|
||||||
|
const selectedEl = editor.contentEl.querySelector<HTMLAnchorElement>(
|
||||||
|
"figure[data-editor-selected]"
|
||||||
|
);
|
||||||
if (!selectedEl)
|
if (!selectedEl)
|
||||||
throw new Error('No pude encontrar el multimedia para setear el alt')
|
throw new Error("No pude encontrar el multimedia para setear el alt");
|
||||||
|
|
||||||
const innerEl = selectedEl.querySelector<HTMLElement>('[data-multimedia-inner]')
|
const innerEl = selectedEl.querySelector<HTMLElement>(
|
||||||
if (!innerEl) throw new Error('No hay multimedia a para setear el alt')
|
"[data-multimedia-inner]"
|
||||||
|
);
|
||||||
|
if (!innerEl) throw new Error("No hay multimedia a para setear el alt");
|
||||||
|
|
||||||
setAlt(innerEl, editor.toolbar.auxiliary.multimedia.altEl.value)
|
setAlt(innerEl, editor.toolbar.auxiliary.multimedia.altEl.value);
|
||||||
})
|
}
|
||||||
editor.toolbar.auxiliary.multimedia.altEl.addEventListener('keydown', event => {
|
);
|
||||||
if (event.keyCode == 13) event.preventDefault()
|
editor.toolbar.auxiliary.multimedia.altEl.addEventListener(
|
||||||
})
|
"keydown",
|
||||||
|
(event) => {
|
||||||
|
if (event.keyCode == 13) event.preventDefault();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupButtons (editor: Editor): void {
|
export function setupButtons(editor: Editor): void {
|
||||||
const buttonEl = editor.toolbarEl.querySelector('[data-editor-button="multimedia"]')
|
const buttonEl = editor.toolbarEl.querySelector(
|
||||||
if (!buttonEl) throw new Error('No encontre el botón de multimedia')
|
'[data-editor-button="multimedia"]'
|
||||||
buttonEl.addEventListener('click', event => {
|
);
|
||||||
event.preventDefault()
|
if (!buttonEl) throw new Error("No encontre el botón de multimedia");
|
||||||
|
buttonEl.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const list = getValidParentInSelection({ editor, type: 'multimedia' })
|
const list = getValidParentInSelection({ editor, type: "multimedia" });
|
||||||
|
|
||||||
const el = multimedia.create(editor)
|
const el = multimedia.create(editor);
|
||||||
list[0].insertBefore(el, list[1].nextElementSibling)
|
list[0].insertBefore(el, list[1].nextElementSibling);
|
||||||
select(editor, el)
|
select(editor, el);
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,75 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection,
|
||||||
|
safeGetRangeAt,
|
||||||
moveChildren,
|
moveChildren,
|
||||||
blockNames, parentBlockNames,
|
blockNames,
|
||||||
} from 'editor/utils'
|
parentBlockNames,
|
||||||
import { EditorNode, getType, getValidParentInSelection } from 'editor/types'
|
} from "editor/utils";
|
||||||
|
import { EditorNode, getType, getValidParentInSelection } from "editor/types";
|
||||||
|
|
||||||
function makeParentBlock (tag: string, create: EditorNode["create"]): EditorNode {
|
function makeParentBlock(
|
||||||
|
tag: string,
|
||||||
|
create: EditorNode["create"]
|
||||||
|
): EditorNode {
|
||||||
return {
|
return {
|
||||||
selector: tag,
|
selector: tag,
|
||||||
allowedChildren: [...blockNames, 'multimedia'],
|
allowedChildren: [...blockNames, "multimedia"],
|
||||||
handleEmpty: 'remove',
|
handleEmpty: "remove",
|
||||||
create,
|
create,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: añadir blockquote
|
// TODO: añadir blockquote
|
||||||
// XXX: si agregás algo acá, probablemente le quieras hacer un botón
|
// XXX: si agregás algo acá, probablemente le quieras hacer un botón
|
||||||
// en app/views/posts/attributes/_content.haml
|
// en app/views/posts/attributes/_content.haml
|
||||||
export const parentBlocks: { [propName: string]: EditorNode } = {
|
export const parentBlocks: { [propName: string]: EditorNode } = {
|
||||||
left: makeParentBlock('div[data-align=left]', () => {
|
left: makeParentBlock("div[data-align=left]", () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement("div");
|
||||||
el.dataset.align = 'left'
|
el.dataset.align = "left";
|
||||||
return el
|
return el;
|
||||||
}),
|
}),
|
||||||
center: makeParentBlock('div[data-align=center]', () => {
|
center: makeParentBlock("div[data-align=center]", () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement("div");
|
||||||
el.dataset.align = 'center'
|
el.dataset.align = "center";
|
||||||
return el
|
return el;
|
||||||
}),
|
}),
|
||||||
right: makeParentBlock('div[data-align=right]', () => {
|
right: makeParentBlock("div[data-align=right]", () => {
|
||||||
const el = document.createElement('div')
|
const el = document.createElement("div");
|
||||||
el.dataset.align = 'right'
|
el.dataset.align = "right";
|
||||||
return el
|
return el;
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
|
|
||||||
export function setupButtons (editor: Editor): void {
|
export function setupButtons(editor: Editor): void {
|
||||||
for (const [ name, type ] of Object.entries(parentBlocks)) {
|
for (const [name, type] of Object.entries(parentBlocks)) {
|
||||||
const buttonEl = editor.toolbarEl.querySelector(
|
const buttonEl = editor.toolbarEl.querySelector(
|
||||||
`[data-editor-button="parentBlock-${name}"]`
|
`[data-editor-button="parentBlock-${name}"]`
|
||||||
)
|
);
|
||||||
if (!buttonEl) continue
|
if (!buttonEl) continue;
|
||||||
buttonEl.addEventListener("click", event => {
|
buttonEl.addEventListener("click", (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
|
||||||
// TODO: Esto solo mueve el bloque en el que está el final de la selección
|
// TODO: Esto solo mueve el bloque en el que está el final de la selección
|
||||||
// (anchorNode). quizás lo podemos hacer al revés (iterar desde contentEl
|
// (anchorNode). quizás lo podemos hacer al revés (iterar desde contentEl
|
||||||
// para encontrar los bloques que están seleccionados y moverlos/cambiarles
|
// para encontrar los bloques que están seleccionados y moverlos/cambiarles
|
||||||
// el parentBlock)
|
// el parentBlock)
|
||||||
|
|
||||||
const list = getValidParentInSelection({ editor, type: name })
|
const list = getValidParentInSelection({ editor, type: name });
|
||||||
|
|
||||||
const replacementEl = type.create(editor)
|
const replacementEl = type.create(editor);
|
||||||
if (list[0] == editor.contentEl) {
|
if (list[0] == editor.contentEl) {
|
||||||
// no está en un parentBlock
|
// no está en un parentBlock
|
||||||
editor.contentEl.insertBefore(replacementEl, list[1])
|
editor.contentEl.insertBefore(replacementEl, list[1]);
|
||||||
replacementEl.appendChild(list[1])
|
replacementEl.appendChild(list[1]);
|
||||||
} else {
|
} else {
|
||||||
// está en un parentBlock
|
// está en un parentBlock
|
||||||
moveChildren(list[0], replacementEl, null)
|
moveChildren(list[0], replacementEl, null);
|
||||||
editor.contentEl.replaceChild(replacementEl, list[0])
|
editor.contentEl.replaceChild(replacementEl, list[0]);
|
||||||
}
|
}
|
||||||
window.getSelection()?.collapse(replacementEl)
|
window.getSelection()?.collapse(replacementEl);
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +1,101 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from "editor/editor";
|
||||||
|
|
||||||
export const blockNames = ['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'unordered_list', 'ordered_list']
|
export const blockNames = [
|
||||||
export const markNames = ['bold', 'italic', 'deleted', 'underline', 'sub', 'super', 'mark', 'link', 'small']
|
"paragraph",
|
||||||
export const parentBlockNames = ['left', 'center', 'right']
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"unordered_list",
|
||||||
|
"ordered_list",
|
||||||
|
];
|
||||||
|
export const markNames = [
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"deleted",
|
||||||
|
"underline",
|
||||||
|
"sub",
|
||||||
|
"super",
|
||||||
|
"mark",
|
||||||
|
"link",
|
||||||
|
"small",
|
||||||
|
];
|
||||||
|
export const parentBlockNames = ["left", "center", "right"];
|
||||||
|
|
||||||
export function moveChildren (from: Element, to: Element, toRef: Node | null) {
|
export function moveChildren(from: Element, to: Element, toRef: Node | null) {
|
||||||
while (from.firstChild) to.insertBefore(from.firstChild, toRef)
|
while (from.firstChild) to.insertBefore(from.firstChild, toRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDirectChild (node: Node, supposedChild: Node): boolean {
|
export function isDirectChild(node: Node, supposedChild: Node): boolean {
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
if (child == supposedChild) return true
|
if (child == supposedChild) return true;
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeGetSelection (editor: Editor): Selection | null {
|
export function safeGetSelection(editor: Editor): Selection | null {
|
||||||
const sel = window.getSelection()
|
const sel = window.getSelection();
|
||||||
if (!sel) return null
|
if (!sel) return null;
|
||||||
// XXX: no damos la selección si esta fuera o _es_ el contentEl, ¿quizás
|
// XXX: no damos la selección si esta fuera o _es_ el contentEl, ¿quizás
|
||||||
// deberíamos mostrar un error?
|
// deberíamos mostrar un error?
|
||||||
if (
|
if (
|
||||||
!editor.contentEl.contains(sel.anchorNode)
|
!editor.contentEl.contains(sel.anchorNode) ||
|
||||||
|| !editor.contentEl.contains(sel.focusNode)
|
!editor.contentEl.contains(sel.focusNode) ||
|
||||||
|| sel.anchorNode == editor.contentEl
|
sel.anchorNode == editor.contentEl ||
|
||||||
|| sel.focusNode == editor.contentEl
|
sel.focusNode == editor.contentEl
|
||||||
) return null
|
)
|
||||||
return sel
|
return null;
|
||||||
|
return sel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeGetRangeAt (selection: Selection, num = 0): Range | null {
|
export function safeGetRangeAt(selection: Selection, num = 0): Range | null {
|
||||||
try {
|
try {
|
||||||
return selection.getRangeAt(num)
|
return selection.getRangeAt(num);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SplitNode {
|
interface SplitNode {
|
||||||
range: Range,
|
range: Range;
|
||||||
node: Node,
|
node: Node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitNode (node: Element, range: Range): [SplitNode, SplitNode] {
|
export function splitNode(node: Element, range: Range): [SplitNode, SplitNode] {
|
||||||
const [left, right] = [
|
const [left, right] = [
|
||||||
{ range: document.createRange(), node: node.cloneNode(false) },
|
{ range: document.createRange(), node: node.cloneNode(false) },
|
||||||
{ range: document.createRange(), node: node.cloneNode(false) },
|
{ range: document.createRange(), node: node.cloneNode(false) },
|
||||||
]
|
];
|
||||||
|
|
||||||
if (node.firstChild) left.range.setStartBefore(node.firstChild)
|
if (node.firstChild) left.range.setStartBefore(node.firstChild);
|
||||||
left.range.setEnd(range.startContainer, range.startOffset)
|
left.range.setEnd(range.startContainer, range.startOffset);
|
||||||
left.range.surroundContents(left.node)
|
left.range.surroundContents(left.node);
|
||||||
|
|
||||||
right.range.setStart(range.endContainer, range.endOffset)
|
right.range.setStart(range.endContainer, range.endOffset);
|
||||||
if (node.lastChild) right.range.setEndAfter(node.lastChild)
|
if (node.lastChild) right.range.setEndAfter(node.lastChild);
|
||||||
right.range.surroundContents(right.node)
|
right.range.surroundContents(right.node);
|
||||||
|
|
||||||
if (!node.parentElement)
|
if (!node.parentElement)
|
||||||
throw new Error('No pude separar los nodos por que no tiene parentNode')
|
throw new Error("No pude separar los nodos por que no tiene parentNode");
|
||||||
|
|
||||||
moveChildren(node, node.parentElement, node)
|
moveChildren(node, node.parentElement, node);
|
||||||
node.parentElement.removeChild(node)
|
node.parentElement.removeChild(node);
|
||||||
|
|
||||||
return [left, right]
|
return [left, right];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAuxiliaryToolbar (editor: Editor, bar: HTMLElement | null): void {
|
export function setAuxiliaryToolbar(
|
||||||
|
editor: Editor,
|
||||||
|
bar: HTMLElement | null
|
||||||
|
): void {
|
||||||
for (const { parentEl } of Object.values(editor.toolbar.auxiliary)) {
|
for (const { parentEl } of Object.values(editor.toolbar.auxiliary)) {
|
||||||
delete parentEl.dataset.editorAuxiliaryActive
|
delete parentEl.dataset.editorAuxiliaryActive;
|
||||||
}
|
}
|
||||||
if (bar) bar.dataset.editorAuxiliaryActive = 'active'
|
if (bar) bar.dataset.editorAuxiliaryActive = "active";
|
||||||
}
|
}
|
||||||
export function clearSelected (editor: Editor): void {
|
export function clearSelected(editor: Editor): void {
|
||||||
const selectedEl = editor.contentEl.querySelector('[data-editor-selected]')
|
const selectedEl = editor.contentEl.querySelector("[data-editor-selected]");
|
||||||
if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected
|
if (selectedEl) delete (selectedEl as HTMLElement).dataset.editorSelected;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue