mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 04:21:41 +00:00
editor: fixes y links
This commit is contained in:
parent
a45d3c898f
commit
4dfba17941
8 changed files with 148 additions and 46 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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') }
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
32
app/javascript/editor/types/link.ts
Normal file
32
app/javascript/editor/types/link.ts
Normal 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()
|
||||
})
|
||||
}
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue