mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 18:56:21 +00:00
Merge branch 'void/editor' into rails
This commit is contained in:
commit
71be841dfc
13 changed files with 94 additions and 94 deletions
|
@ -61,7 +61,7 @@
|
||||||
.editor-content {
|
.editor-content {
|
||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
p, h1, h2, h3, h4, h5, h6, ul, li, figcaption { outline: #ccc solid thin; }
|
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; }
|
a { background: #13fefe50; }
|
||||||
[data-editor-selected] { outline: #f206f9 solid thick; }
|
[data-editor-selected] { outline: #f206f9 solid thick; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { storeContent, restoreContent, forgetContent } from 'editor/storage'
|
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 { types, getValidChildren, getType } from 'editor/types'
|
||||||
import { setupButtons as setupMarksButtons } from 'editor/types/marks'
|
import { setupButtons as setupMarksButtons } from 'editor/types/marks'
|
||||||
import { setupButtons as setupBlocksButtons } from 'editor/types/blocks'
|
import { setupButtons as setupBlocksButtons } from 'editor/types/blocks'
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 } from 'editor/utils'
|
import { blockNames, parentBlockNames, safeGetRangeAt, safeGetSelection } from 'editor/utils'
|
||||||
|
|
||||||
export interface EditorNode {
|
export interface EditorNode {
|
||||||
selector: string,
|
selector: string,
|
||||||
|
@ -51,18 +51,69 @@ export const types: { [propName: string]: EditorNode } = {
|
||||||
|
|
||||||
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)) return { typeName, type }
|
if (node.matches(type.selector)) {
|
||||||
|
return { typeName, type }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
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[] {
|
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('se llamó a getValidChildren con un type que no lo permite!')
|
||||||
return [...node.childNodes].filter(n => {
|
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')
|
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))
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { Editor } from 'editor/editor'
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection, safeGetRangeAt,
|
||||||
moveChildren,
|
moveChildren,
|
||||||
markNames, parentBlockNames,
|
markNames, blockNames, parentBlockNames,
|
||||||
} from 'editor/utils'
|
} from 'editor/utils'
|
||||||
import { EditorNode, getType } from 'editor/types'
|
import { EditorNode, getType, getValidParentInSelection } from 'editor/types'
|
||||||
|
|
||||||
export interface EditorBlock extends EditorNode {
|
export interface EditorBlock extends EditorNode {
|
||||||
}
|
}
|
||||||
|
@ -49,40 +49,22 @@ export function setupButtons (editor: Editor): void {
|
||||||
buttonEl.addEventListener("click", event => {
|
buttonEl.addEventListener("click", event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const sel = safeGetSelection(editor)
|
const list = getValidParentInSelection({ editor, type: name })
|
||||||
if (!sel) return
|
|
||||||
const range = safeGetRangeAt(sel)
|
|
||||||
if (!range) return
|
|
||||||
|
|
||||||
let blockEl = sel.anchorNode
|
// No borrar cosas como multimedia
|
||||||
while (true) {
|
if (blockNames.indexOf(getType(list[1])!.typeName) === -1) {
|
||||||
if (!blockEl) throw new Error('WTF')
|
return
|
||||||
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
|
let replacementType = list[1].matches(type.selector)
|
||||||
if (!parentEl) throw new Error('Inesperado')
|
|
||||||
|
|
||||||
let replacementType = blockEl.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(blockEl, el, null)
|
moveChildren(list[1], el, null)
|
||||||
parentEl.replaceChild(el, blockEl)
|
list[0].replaceChild(el, list[1])
|
||||||
sel.collapse(el)
|
window.getSelection()?.collapse(el)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const marks: { [propName: string]: EditorNode } = {
|
||||||
super: makeMark('super', 'sup'),
|
super: makeMark('super', 'sup'),
|
||||||
mark,
|
mark,
|
||||||
link,
|
link,
|
||||||
|
small: makeMark('small', 'small'),
|
||||||
}
|
}
|
||||||
|
|
||||||
function recursiveFilterSelection (
|
function recursiveFilterSelection (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Editor } from 'editor/editor'
|
import { Editor } from 'editor/editor'
|
||||||
import { EditorNode, getType } from 'editor/types'
|
import { EditorNode, getValidParentInSelection } from 'editor/types'
|
||||||
import {
|
import {
|
||||||
safeGetSelection, safeGetRangeAt,
|
safeGetSelection, safeGetRangeAt,
|
||||||
markNames, parentBlockNames,
|
markNames, parentBlockNames,
|
||||||
|
@ -192,34 +192,11 @@ export function setupButtons (editor: Editor): void {
|
||||||
buttonEl.addEventListener('click', event => {
|
buttonEl.addEventListener('click', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const sel = safeGetSelection(editor)
|
const list = getValidParentInSelection({ editor, type: 'multimedia' })
|
||||||
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 el = multimedia.create(editor)
|
const el = multimedia.create(editor)
|
||||||
multimedia.onClick!(editor, el)
|
list[0].insertBefore(el, list[1].nextElementSibling)
|
||||||
parentEl.insertBefore(el, blockEl.nextElementSibling)
|
select(editor, el)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
moveChildren,
|
moveChildren,
|
||||||
blockNames, parentBlockNames,
|
blockNames, parentBlockNames,
|
||||||
} from 'editor/utils'
|
} from 'editor/utils'
|
||||||
import { EditorNode, getType } from 'editor/types'
|
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 {
|
||||||
|
@ -45,48 +45,24 @@ export function setupButtons (editor: Editor): void {
|
||||||
buttonEl.addEventListener("click", event => {
|
buttonEl.addEventListener("click", event => {
|
||||||
event.preventDefault()
|
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
|
// 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)
|
||||||
|
|
||||||
let blockEl = sel.anchorNode
|
const list = getValidParentInSelection({ editor, type: name })
|
||||||
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 replacementEl = type.create(editor)
|
const replacementEl = type.create(editor)
|
||||||
if (parentEl == editor.contentEl) {
|
if (list[0] == editor.contentEl) {
|
||||||
// no está en un parentBlock
|
// no está en un parentBlock
|
||||||
editor.contentEl.insertBefore(replacementEl, blockEl)
|
editor.contentEl.insertBefore(replacementEl, list[1])
|
||||||
replacementEl.appendChild(blockEl)
|
replacementEl.appendChild(list[1])
|
||||||
} else {
|
} else {
|
||||||
// está en un parentBlock
|
// está en un parentBlock
|
||||||
moveChildren(parentEl, replacementEl, null)
|
moveChildren(list[0], replacementEl, null)
|
||||||
editor.contentEl.replaceChild(replacementEl, parentEl)
|
editor.contentEl.replaceChild(replacementEl, list[0])
|
||||||
}
|
}
|
||||||
sel.collapse(replacementEl)
|
window.getSelection()?.collapse(replacementEl)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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 = ['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 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) {
|
||||||
|
|
|
@ -193,7 +193,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
|
|
||||||
def allowed_tags
|
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
|
@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
|
end
|
||||||
|
|
||||||
# Decifra el valor
|
# Decifra el valor
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
%button.btn{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
|
%button.btn{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
|
||||||
%i.fa.fa-fw.fa-subscript>
|
%i.fa.fa-fw.fa-subscript>
|
||||||
%span.sr-only>= t('editor.sub')
|
%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' } }>
|
%button.btn.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }>
|
||||||
%i.fa.fa-fw.fa-heading>
|
%i.fa.fa-fw.fa-heading>
|
||||||
1
|
1
|
||||||
|
|
|
@ -586,6 +586,7 @@ en:
|
||||||
link: Link
|
link: Link
|
||||||
super: Superscript
|
super: Superscript
|
||||||
sub: Subscript
|
sub: Subscript
|
||||||
|
small: Small
|
||||||
h1: Heading 1
|
h1: Heading 1
|
||||||
h2: Heading 2
|
h2: Heading 2
|
||||||
h3: Heading 3
|
h3: Heading 3
|
||||||
|
|
|
@ -598,6 +598,7 @@ es:
|
||||||
mark: Resaltado
|
mark: Resaltado
|
||||||
super: Índice
|
super: Índice
|
||||||
sub: Subíndice
|
sub: Subíndice
|
||||||
|
small: Chico
|
||||||
link: Vínculo
|
link: Vínculo
|
||||||
h1: Título 1
|
h1: Título 1
|
||||||
h2: Título 2
|
h2: Título 2
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "es2015",
|
"module": "es2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": ["node_modules/*", "app/javascript/*"]
|
||||||
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
Loading…
Reference in a new issue