editor: fixes y links

This commit is contained in:
void 2021-02-13 01:14:36 +00:00
parent a45d3c898f
commit 4dfba17941
8 changed files with 148 additions and 46 deletions

View file

@ -1,17 +1,10 @@
import { storeContent, restoreContent } from 'editor/storage'
import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt } from 'editor/utils'
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'
export interface Editor {
editorEl: HTMLElement,
toolbarEl: HTMLElement,
contentEl: HTMLElement,
wordAlertEl: HTMLElement,
htmlEl: HTMLTextAreaElement,
}
import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from 'editor/types/link'
// Esta funcion corrije errores que pueden haber como:
// * que un nodo que no tiene 'text' permitido no tenga children (se les
@ -50,7 +43,7 @@ function fixContent (editor: Editor, node: Element = editor.contentEl): void {
if (getValidChildren(node, type).length == 0) {
if (typeof type.handleEmpty !== 'string') {
const el = type.handleEmpty.create()
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á
@ -129,25 +122,66 @@ function routine (editor: Editor): void {
}
}
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 toolbarEl = editorEl.querySelector<HTMLElement>('.editor-toolbar')
if (!toolbarEl) throw new Error('No pude encontrar .editor-toolbar')
const contentEl = editorEl.querySelector<HTMLElement>('.editor-content')
if (!contentEl) throw new Error('No pude encontrar .editor-content')
const wordAlertEl = editorEl.querySelector<HTMLElement>('.editor-aviso-word')
if (!wordAlertEl) throw new Error('No pude encontrar .editor-aviso-word')
const htmlEl = editorEl.querySelector<HTMLTextAreaElement>('textarea')
if (!htmlEl) throw new Error('No pude encontrar el textarea para el HTML')
const editor: Editor = {
editorEl,
toolbarEl,
contentEl,
wordAlertEl,
htmlEl,
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)
@ -165,7 +199,7 @@ function setupEditor (editorEl: HTMLElement): void {
// Setup routine listeners
const observer = new MutationObserver(() => routine(editor))
observer.observe(contentEl, {
observer.observe(editor.contentEl, {
childList: true,
attributes: true,
subtree: true,
@ -174,19 +208,43 @@ function setupEditor (editorEl: HTMLElement): void {
document.addEventListener("selectionchange", () => routine(editor))
// 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)
setupLinkAuxiliaryToolbar(editor)
// Finally...
routine(editor)
}
document.addEventListener("turbolinks:load", () => {
for (const editorEl of document.querySelectorAll<HTMLElement>('.editor')) {
if (!editorEl.querySelector('.editor-toolbar')) continue
setupEditor(editorEl)
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)
}
}
})

View file

@ -1,7 +1,8 @@
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 { blockNames } from 'editor/utils'
import { blockNames, parentBlockNames } from 'editor/utils'
export interface EditorNode {
selector: string,
@ -18,7 +19,9 @@ export interface EditorNode {
// ej: ul: { handleNothing: li }
handleEmpty: 'do-nothing' | 'remove' | EditorBlock,
create: () => HTMLElement,
create: (editor: Editor) => HTMLElement,
onClick?: (editor: Editor, target: Element) => void,
}
export const types: { [propName: string]: EditorNode } = {
@ -28,7 +31,7 @@ export const types: { [propName: string]: EditorNode } = {
...parentBlocks,
contentEl: {
selector: '.editor-content',
allowedChildren: [...blockNames, ...Object.keys(parentBlocks)],
allowedChildren: [...blockNames, ...parentBlockNames],
handleEmpty: blocks.paragraph,
create: () => { throw new Error('se intentó crear contentEl') }
},

View file

@ -82,7 +82,7 @@ export function setupButtons (editor: Editor): void {
? blocks.paragraph
: type
const el = replacementType.create()
const el = replacementType.create(editor)
moveChildren(blockEl, el, null)
parentEl.replaceChild(el, blockEl)
sel.collapse(el)

View file

@ -0,0 +1,32 @@
import { Editor } from 'editor/editor'
import { EditorNode } from 'editor/types'
import { markNames, setAuxiliaryToolbar } from 'editor/utils'
export const link: EditorNode = {
selector: 'a',
allowedChildren: [...markNames.filter(n => n !== 'link'), 'text'],
handleEmpty: 'remove',
create: () => document.createElement('a'),
onClick (editor, el) {
if (!(el instanceof HTMLAnchorElement))
throw new Error('oh no')
el.dataset.editorSelected = ''
editor.toolbar.auxiliary.link.urlEl.value = el.href
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.link.parentEl)
}
}
export function setupAuxiliaryToolbar (editor: Editor): void {
editor.toolbar.auxiliary.link.urlEl.addEventListener('input', event => {
const url = editor.toolbar.auxiliary.link.urlEl.value
const selectedEl = editor.contentEl
.querySelector<HTMLAnchorElement>('a[data-editor-selected]')
if (!selectedEl)
throw new Error('No pude encontrar el link para setear el enlace')
selectedEl.href = url
})
editor.toolbar.auxiliary.link.urlEl.addEventListener('keydown', event => {
if (event.keyCode == 13) event.preventDefault()
})
}

View file

@ -4,7 +4,9 @@ import {
safeGetSelection, safeGetRangeAt,
moveChildren,
markNames,
setAuxiliaryToolbar,
} from 'editor/utils'
import { link } from 'editor/types/link'
function makeMark (name: string, tag: string): EditorNode {
return {
@ -24,7 +26,7 @@ export const marks: { [propName: string]: EditorNode } = {
sub: makeMark('sub', 'sub'),
super: makeMark('super', 'sup'),
mark: makeMark('mark', 'mark'),
link: makeMark('link', 'a'),
link,
}
function recursiveFilterSelection (
@ -78,7 +80,7 @@ export function setupButtons (editor: Editor): void {
// TODO: mostrar error
return console.error("No puedo marcar cosas a través de distintos bloques!")
const tagEl = type.create()
const tagEl = type.create(editor)
tagEl.appendChild(range.extractContents())

View file

@ -76,7 +76,7 @@ export function setupButtons (editor: Editor): void {
if (!parentEl)
throw new Error('no')
const replacementEl = type.create()
const replacementEl = type.create(editor)
if (parentEl == editor.contentEl) {
// no está en un parentBlock
editor.contentEl.insertBefore(replacementEl, blockEl)

View file

@ -1,7 +1,7 @@
import { Editor } from 'editor/editor'
export const blockNames = ['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'unordered_list', 'ordered_list']
export const markNames = ['bold', 'italic', 'deleted', 'underline', 'sub', 'super', 'mark', 'a']
export const markNames = ['bold', 'italic', 'deleted', 'underline', 'sub', 'super', 'mark', 'link']
export const parentBlockNames = ['left', 'center', 'right']
export function moveChildren (from: Element, to: Element, toRef: Node | null) {
@ -64,3 +64,10 @@ export function splitNode (node: Element, range: Range): [SplitNode, SplitNode]
return [left, right]
}
export function setAuxiliaryToolbar (editor: Editor, bar: HTMLElement | null): void {
for (const { parentEl } of Object.values(editor.toolbar.auxiliary)) {
delete parentEl.dataset.editorAuxiliaryActive
}
if (bar) bar.dataset.editorAuxiliaryActive = 'active'
}

View file

@ -3,7 +3,7 @@
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata
.editor{ id: attribute }
.editor{ id: attribute, data: { editor: '' } }
-# Esto es para luego decirle al navegador que se olvide estas cosas.
= hidden_field_tag 'storage_keys[]', "#{request.original_url}##{attribute}", data: { target: 'storage-key' }
.alert.alert-info
@ -40,24 +40,24 @@
HAML cringe
TODO: generar IDs para labels
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { 'editor_auxiliary_toolbar': '' } }
%form.form-group{ data: { editor: { auxiliary: 'mark' } } }
.form-group{ data: { editor: { auxiliary: 'mark' } } }
%label{ for: 'mark-color' }= t('editor.color')
%input.form-control{ type: 'color', data: { prop: 'mark-color' } }/
%input.form-control{ type: 'color', name: 'mark-color' }/
%form{ data: { editor: { auxiliary: 'multimedia' } } }
%div{ data: { editor: { auxiliary: 'multimedia' } } }
.row
.col-12.col-lg.form-group.d-flex.align-items-end
.custom-file
%input.custom-file-input{ type: 'file', data: { prop: 'multimedia-file' }, }/
%input.custom-file-input{ type: 'file', name: 'multimedia-file' }/
%label.custom-file-label{ for: 'multimedia-file' }= t('editor.file.multimedia')
%button.btn{ type: 'button', data: { prop: 'multimedia-file-upload' }, }= t('editor.file.multimedia-upload')
%button.btn{ type: 'button', name: 'multimedia-file-upload' }= t('editor.file.multimedia-upload')
.col-12.col-lg.form-group
%label{ for: 'multimedia-alt' }= t('editor.description')
%input.form-control{ type: 'text', data: { prop: 'multimedia-alt' } }/
%input.form-control{ type: 'text', name: 'multimedia-alt' }/
%form.form-group{ data: { editor: { auxiliary: 'link' } } }
%label{ for: 'a-href' }= t('editor.url')
%input.form-control{ type: 'url', data: { prop: 'a-href' } }/
.form-group{ data: { editor: { auxiliary: 'link' } } }
%label{ for: 'link-url' }= t('editor.url')
%input.form-control{ type: 'url', name: 'link-url' }/
.editor-aviso-word.alert.alert-info
%p= t('editor.word')