diff --git a/app/assets/stylesheets/editor.scss b/app/assets/stylesheets/editor.scss index c7b50db..5d218c7 100644 --- a/app/assets/stylesheets/editor.scss +++ b/app/assets/stylesheets/editor.scss @@ -61,7 +61,7 @@ .editor-content { min-height: 480px; p, h1, h2, h3, h4, h5, h6, ul, li, figcaption { outline: #ccc solid thin; } - strong, em, del, u, sub, sup { background: #0002; } + strong, em, del, u, sub, sup, small { background: #0002; } a { background: #13fefe50; } [data-editor-selected] { outline: #f206f9 solid thick; } } diff --git a/app/javascript/editor/editor.ts b/app/javascript/editor/editor.ts index 6bc18df..167d40d 100644 --- a/app/javascript/editor/editor.ts +++ b/app/javascript/editor/editor.ts @@ -1,5 +1,8 @@ import { storeContent, restoreContent, forgetContent } from 'editor/storage' -import { isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt, setAuxiliaryToolbar, parentBlockNames, clearSelected } from 'editor/utils' +import { + isDirectChild, moveChildren, safeGetSelection, safeGetRangeAt, + setAuxiliaryToolbar, parentBlockNames, clearSelected, +} 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' diff --git a/app/javascript/editor/types.ts b/app/javascript/editor/types.ts index 3c215a8..8034e3e 100644 --- a/app/javascript/editor/types.ts +++ b/app/javascript/editor/types.ts @@ -3,7 +3,7 @@ import { marks } from 'editor/types/marks' import { blocks, li, EditorBlock } from 'editor/types/blocks' import { parentBlocks } from 'editor/types/parentBlocks' import { multimedia } from 'editor/types/multimedia' -import { blockNames, parentBlockNames } from 'editor/utils' +import { blockNames, parentBlockNames, safeGetRangeAt, safeGetSelection } from 'editor/utils' export interface EditorNode { selector: string, @@ -51,18 +51,69 @@ export const types: { [propName: string]: EditorNode } = { export function getType (node: Element): { typeName: string, type: EditorNode } | null { for (let [typeName, type] of Object.entries(types)) { - if (node.matches(type.selector)) return { typeName, type } + if (node.matches(type.selector)) { + return { typeName, type } + } } + return null } +// encuentra el primer pariente que pueda tener al type, y retorna un array +// donde +// array[0] = elemento que matchea el type +// array[array.len - 1] = primer elemento seleccionado +export function getValidParentInSelection (args: { + editor: Editor, + type: string, +}): Element[] { + const sel = safeGetSelection(args.editor) + if (!sel) throw new Error('No se donde insertar esto') + const range = safeGetRangeAt(sel) + if (!range) throw new Error('No se donde insertar esto') + + let list: Element[] = [] + + if (!sel.anchorNode) { + throw new Error('No se donde insertar esto') + } else if (sel.anchorNode instanceof Element) { + list = [sel.anchorNode] + } else if (sel.anchorNode.parentElement) { + list = [sel.anchorNode.parentElement] + } else { + throw new Error('No se donde insertar esto') + } + + while (true) { + const el = list[0] + if (!args.editor.contentEl.contains(el) + && el != args.editor.contentEl) + throw new Error('No se donde insertar esto') + const type = getType(el) + + if (type) { + //if (type.typeName === 'contentEl') break + //if (parentBlockNames.includes(type.typeName)) break + if ((type.type.allowedChildren instanceof Array) + && type.type.allowedChildren.includes(args.type)) break + } + if (el.parentElement) { + list = [el.parentElement, ...list] + } else { + throw new Error('No se donde insertar esto') + } + } + + return list +} + export function getValidChildren (node: Element, type: EditorNode): Node[] { if (type.allowedChildren === 'ignore-children') throw new Error('se llamó a getValidChildren con un type que no lo permite!') return [...node.childNodes].filter(n => { // si permite texto y esto es un texto, es válido if (n.nodeType === Node.TEXT_NODE) - return type.allowedChildren.includes('text') + return type.allowedChildren.includes('text') && n.textContent?.length // si no es un elemento, no es válido if (!(n instanceof Element)) diff --git a/app/javascript/editor/types/blocks.ts b/app/javascript/editor/types/blocks.ts index 28f6384..52ad157 100644 --- a/app/javascript/editor/types/blocks.ts +++ b/app/javascript/editor/types/blocks.ts @@ -2,9 +2,9 @@ import { Editor } from 'editor/editor' import { safeGetSelection, safeGetRangeAt, moveChildren, - markNames, parentBlockNames, + markNames, blockNames, parentBlockNames, } from 'editor/utils' -import { EditorNode, getType } from 'editor/types' +import { EditorNode, getType, getValidParentInSelection } from 'editor/types' export interface EditorBlock extends EditorNode { } @@ -49,40 +49,22 @@ export function setupButtons (editor: Editor): void { buttonEl.addEventListener("click", event => { event.preventDefault() - const sel = safeGetSelection(editor) - if (!sel) return - const range = safeGetRangeAt(sel) - if (!range) return + const list = getValidParentInSelection({ editor, type: name }) - let blockEl = sel.anchorNode - while (true) { - if (!blockEl) throw new Error('WTF') - if (!blockEl.parentElement) throw new Error('No pude encontrar contentEl!') - - let type = getType(blockEl.parentElement) - if (!type) throw new Error('La selección está en algo que no es un type!') - - if (type.typeName === 'contentEl' - || parentBlockNames.includes(type.typeName) - ) break - - blockEl = blockEl.parentElement + // No borrar cosas como multimedia + if (blockNames.indexOf(getType(list[1])!.typeName) === -1) { + return } - if (!(blockEl instanceof Element)) - throw new Error('La selección no está en un elemento!') - const parentEl = blockEl.parentElement - if (!parentEl) throw new Error('Inesperado') - - let replacementType = blockEl.matches(type.selector) + let replacementType = list[1].matches(type.selector) ? blocks.paragraph : type const el = replacementType.create(editor) replacementType.onClick && replacementType.onClick(editor, el) - moveChildren(blockEl, el, null) - parentEl.replaceChild(el, blockEl) - sel.collapse(el) + moveChildren(list[1], el, null) + list[0].replaceChild(el, list[1]) + window.getSelection()?.collapse(el) return false }) diff --git a/app/javascript/editor/types/marks.ts b/app/javascript/editor/types/marks.ts index ac2493a..3790c74 100644 --- a/app/javascript/editor/types/marks.ts +++ b/app/javascript/editor/types/marks.ts @@ -27,6 +27,7 @@ export const marks: { [propName: string]: EditorNode } = { super: makeMark('super', 'sup'), mark, link, + small: makeMark('small', 'small'), } function recursiveFilterSelection ( diff --git a/app/javascript/editor/types/multimedia.ts b/app/javascript/editor/types/multimedia.ts index 7fdc0ca..7ebf8e8 100644 --- a/app/javascript/editor/types/multimedia.ts +++ b/app/javascript/editor/types/multimedia.ts @@ -1,5 +1,5 @@ import { Editor } from 'editor/editor' -import { EditorNode, getType } from 'editor/types' +import { EditorNode, getValidParentInSelection } from 'editor/types' import { safeGetSelection, safeGetRangeAt, markNames, parentBlockNames, @@ -192,34 +192,11 @@ export function setupButtons (editor: Editor): void { buttonEl.addEventListener('click', event => { event.preventDefault() - const sel = safeGetSelection(editor) - if (!sel) return - const range = safeGetRangeAt(sel) - if (!range) return - - let blockEl = sel.anchorNode - while (true) { - if (!blockEl) throw new Error('WTF') - if (!blockEl.parentElement) throw new Error('No pude encontrar contentEl!') - - let type = getType(blockEl.parentElement) - if (!type) throw new Error('La selección está en algo que no es un type!') - - if (type.typeName === 'contentEl' - || parentBlockNames.includes(type.typeName) - ) break - - blockEl = blockEl.parentElement - } - if (!(blockEl instanceof Element)) - throw new Error('La selección no está en un elemento!') - - const parentEl = blockEl.parentElement - if (!parentEl) throw new Error('Inesperado') + const list = getValidParentInSelection({ editor, type: 'multimedia' }) const el = multimedia.create(editor) - multimedia.onClick!(editor, el) - parentEl.insertBefore(el, blockEl.nextElementSibling) + list[0].insertBefore(el, list[1].nextElementSibling) + select(editor, el) return false }) diff --git a/app/javascript/editor/types/parentBlocks.ts b/app/javascript/editor/types/parentBlocks.ts index 9f78042..55a8c3d 100644 --- a/app/javascript/editor/types/parentBlocks.ts +++ b/app/javascript/editor/types/parentBlocks.ts @@ -4,7 +4,7 @@ import { moveChildren, blockNames, parentBlockNames, } from 'editor/utils' -import { EditorNode, getType } from 'editor/types' +import { EditorNode, getType, getValidParentInSelection } from 'editor/types' function makeParentBlock (tag: string, create: EditorNode["create"]): EditorNode { return { @@ -45,48 +45,24 @@ export function setupButtons (editor: Editor): void { buttonEl.addEventListener("click", event => { event.preventDefault() - const sel = safeGetSelection(editor) - if (!sel) return - const range = safeGetRangeAt(sel) - if (!range) return - // 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 // para encontrar los bloques que están seleccionados y moverlos/cambiarles // el parentBlock) - let blockEl = sel.anchorNode - while (true) { - if (!blockEl) throw new Error('WTF') - if (!blockEl.parentElement) throw new Error('No pude encontrar contentEl!') - - let type = getType(blockEl.parentElement) - if (!type) throw new Error('La selección está en algo que no es un type!') - - if (type.typeName === 'contentEl' - || parentBlockNames.includes(type.typeName) - ) break - - blockEl = blockEl.parentElement - } - if (!(blockEl instanceof Element)) - throw new Error('La selección no está en un elemento!') - - const parentEl = blockEl.parentElement - if (!parentEl) - throw new Error('no') + const list = getValidParentInSelection({ editor, type: name }) const replacementEl = type.create(editor) - if (parentEl == editor.contentEl) { + if (list[0] == editor.contentEl) { // no está en un parentBlock - editor.contentEl.insertBefore(replacementEl, blockEl) - replacementEl.appendChild(blockEl) + editor.contentEl.insertBefore(replacementEl, list[1]) + replacementEl.appendChild(list[1]) } else { // está en un parentBlock - moveChildren(parentEl, replacementEl, null) - editor.contentEl.replaceChild(replacementEl, parentEl) + moveChildren(list[0], replacementEl, null) + editor.contentEl.replaceChild(replacementEl, list[0]) } - sel.collapse(replacementEl) + window.getSelection()?.collapse(replacementEl) return false }) diff --git a/app/javascript/editor/utils.ts b/app/javascript/editor/utils.ts index fd3b082..7ac4c18 100644 --- a/app/javascript/editor/utils.ts +++ b/app/javascript/editor/utils.ts @@ -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', 'link'] +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) { diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index 49e432c..5800059 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -193,7 +193,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, def allowed_tags @allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure - figcaption a].freeze + figcaption a sub sup small].freeze end # Decifra el valor diff --git a/app/views/posts/attributes/_content.haml b/app/views/posts/attributes/_content.haml index 50da7e9..235dca3 100644 --- a/app/views/posts/attributes/_content.haml +++ b/app/views/posts/attributes/_content.haml @@ -47,6 +47,9 @@ %button.btn{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }> %i.fa.fa-fw.fa-subscript> %span.sr-only>= t('editor.sub') + %button.btn{ type: 'button', title: t('editor.small'), data: { editor_button: 'mark-small' } }> + %i.fa.fa-fw.fa-subscript> + %span.sr-only>= t('editor.small') %button.btn.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }> %i.fa.fa-fw.fa-heading> 1 diff --git a/config/locales/en.yml b/config/locales/en.yml index cfcaffd..02570b1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -586,6 +586,7 @@ en: link: Link super: Superscript sub: Subscript + small: Small h1: Heading 1 h2: Heading 2 h3: Heading 3 diff --git a/config/locales/es.yml b/config/locales/es.yml index dc25d04..5bb4a22 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -598,6 +598,7 @@ es: mark: Resaltado super: Índice sub: Subíndice + small: Chico link: Vínculo h1: Título 1 h2: Título 2 diff --git a/tsconfig.json b/tsconfig.json index 1cb735a..f3d5544 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,11 @@ "compilerOptions": { "target": "es6", "module": "es2015", + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "*": ["node_modules/*", "app/javascript/*"] + }, "strict": true, "esModuleInterop": true, "skipLibCheck": true,