WIP: multimedia
This commit is contained in:
parent
704616b18e
commit
d5592fffb4
7 changed files with 314 additions and 82 deletions
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
p, h1, h2, h3, h4, h5, h6, ul, li { 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 { background: #0002; }
|
||||||
a { background: #13fefe50; }
|
a { background: #13fefe50; }
|
||||||
[data-editor-selected] { outline: #f206f9 solid thick; }
|
[data-editor-selected] { outline: #f206f9 solid thick; }
|
||||||
|
|
|
@ -5,6 +5,10 @@ import { setupButtons as setupMarksButtons } from 'editor/types/marks'
|
||||||
import { setupButtons as setupBlocksButtons } from 'editor/types/blocks'
|
import { setupButtons as setupBlocksButtons } from 'editor/types/blocks'
|
||||||
import { setupButtons as setupParentBlocksButtons } from 'editor/types/parentBlocks'
|
import { setupButtons as setupParentBlocksButtons } from 'editor/types/parentBlocks'
|
||||||
import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from 'editor/types/link'
|
import { setupAuxiliaryToolbar as setupLinkAuxiliaryToolbar } from 'editor/types/link'
|
||||||
|
import {
|
||||||
|
setupAuxiliaryToolbar as setupMultimediaAuxiliaryToolbar,
|
||||||
|
setupButtons as setupMultimediaButtons,
|
||||||
|
} 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:
|
||||||
|
@ -39,6 +43,7 @@ function fixContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
|
|
||||||
const { typeName, type } = _type
|
const { typeName, type } = _type
|
||||||
|
|
||||||
|
if (type.allowedChildren !== 'ignore-children') {
|
||||||
const sel = safeGetSelection(editor)
|
const sel = safeGetSelection(editor)
|
||||||
const range = sel && safeGetRangeAt(sel)
|
const range = sel && safeGetRangeAt(sel)
|
||||||
|
|
||||||
|
@ -60,6 +65,7 @@ function fixContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
fixContent(editor, child)
|
fixContent(editor, child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Esta funcion hace que los elementos del editor sigan la estructura.
|
// Esta funcion hace que los elementos del editor sigan la estructura.
|
||||||
// TODO: nos falta borrar atributos (style, y básicamente cualquier otra cosa)
|
// TODO: nos falta borrar atributos (style, y básicamente cualquier otra cosa)
|
||||||
|
@ -75,9 +81,10 @@ function cleanContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
|
|
||||||
const { type } = _type
|
const { type } = _type
|
||||||
|
|
||||||
|
if (type.allowedChildren !== 'ignore-children') {
|
||||||
for (const child of node.childNodes) {
|
for (const child of node.childNodes) {
|
||||||
if (child.nodeType === Node.TEXT_NODE
|
if (child.nodeType === Node.TEXT_NODE
|
||||||
&& type.allowedChildren.indexOf('text') === -1
|
&& !type.allowedChildren.includes('text')
|
||||||
) {
|
) {
|
||||||
node.removeChild(child)
|
node.removeChild(child)
|
||||||
continue
|
continue
|
||||||
|
@ -87,7 +94,7 @@ function cleanContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
|
|
||||||
const childType = getType(child)
|
const childType = getType(child)
|
||||||
if (childType?.typeName === 'br') continue
|
if (childType?.typeName === 'br') continue
|
||||||
if (!childType || type.allowedChildren.indexOf(childType.typeName) === -1) {
|
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)
|
||||||
|
@ -110,6 +117,7 @@ function cleanContent (editor: Editor, node: Element = editor.contentEl): void {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function routine (editor: Editor): void {
|
function routine (editor: Editor): void {
|
||||||
try {
|
try {
|
||||||
|
@ -137,6 +145,7 @@ export interface Editor {
|
||||||
fileEl: HTMLInputElement,
|
fileEl: HTMLInputElement,
|
||||||
uploadEl: HTMLButtonElement,
|
uploadEl: HTMLButtonElement,
|
||||||
altEl: HTMLInputElement,
|
altEl: HTMLInputElement,
|
||||||
|
removeEl: HTMLButtonElement,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
parentEl: HTMLElement,
|
parentEl: HTMLElement,
|
||||||
|
@ -173,6 +182,7 @@ function setupEditor (editorEl: HTMLElement): void {
|
||||||
fileEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file]'),
|
fileEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file]'),
|
||||||
uploadEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file-upload]'),
|
uploadEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-file-upload]'),
|
||||||
altEl: getSel(editorEl, '[data-editor-auxiliary=multimedia] [name=multimedia-alt]'),
|
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]'),
|
||||||
|
@ -231,8 +241,10 @@ function setupEditor (editorEl: HTMLElement): void {
|
||||||
setupMarksButtons(editor)
|
setupMarksButtons(editor)
|
||||||
setupBlocksButtons(editor)
|
setupBlocksButtons(editor)
|
||||||
setupParentBlocksButtons(editor)
|
setupParentBlocksButtons(editor)
|
||||||
|
setupMultimediaButtons(editor)
|
||||||
|
|
||||||
setupLinkAuxiliaryToolbar(editor)
|
setupLinkAuxiliaryToolbar(editor)
|
||||||
|
setupMultimediaAuxiliaryToolbar(editor)
|
||||||
setupMarkAuxiliaryToolbar(editor)
|
setupMarkAuxiliaryToolbar(editor)
|
||||||
|
|
||||||
// Finally...
|
// Finally...
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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 { blockNames, parentBlockNames } from 'editor/utils'
|
import { blockNames, parentBlockNames } from 'editor/utils'
|
||||||
|
|
||||||
export interface EditorNode {
|
export interface EditorNode {
|
||||||
|
@ -9,7 +10,7 @@ export interface EditorNode {
|
||||||
// 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[],
|
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)
|
||||||
|
@ -31,7 +32,7 @@ export const types: { [propName: string]: EditorNode } = {
|
||||||
...parentBlocks,
|
...parentBlocks,
|
||||||
contentEl: {
|
contentEl: {
|
||||||
selector: '.editor-content',
|
selector: '.editor-content',
|
||||||
allowedChildren: [...blockNames, ...parentBlockNames],
|
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') }
|
||||||
},
|
},
|
||||||
|
@ -41,6 +42,7 @@ export const types: { [propName: string]: EditorNode } = {
|
||||||
handleEmpty: 'do-nothing',
|
handleEmpty: 'do-nothing',
|
||||||
create: () => { throw new Error('se intentó crear br') }
|
create: () => { throw new Error('se intentó crear br') }
|
||||||
},
|
},
|
||||||
|
multimedia,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getType (node: Element): { typeName: string, type: EditorNode } | null {
|
export function getType (node: Element): { typeName: string, type: EditorNode } | null {
|
||||||
|
@ -51,6 +53,8 @@ export function getType (node: Element): { typeName: string, type: EditorNode }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getValidChildren (node: Element, type: EditorNode): Node[] {
|
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 => {
|
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)
|
||||||
|
|
|
@ -6,10 +6,6 @@ import {
|
||||||
} from 'editor/utils'
|
} from 'editor/utils'
|
||||||
import { EditorNode, getType } from 'editor/types'
|
import { EditorNode, getType } from 'editor/types'
|
||||||
|
|
||||||
// TODO: implementar multimedia como otro tipo! ya que no puede tener children
|
|
||||||
// debe ser tratado distinto que los bloques (quizás EditorBlockWithText y
|
|
||||||
// EditorBlock o algo así)
|
|
||||||
|
|
||||||
export interface EditorBlock extends EditorNode {
|
export interface EditorBlock extends EditorNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
220
app/javascript/editor/types/multimedia.ts
Normal file
220
app/javascript/editor/types/multimedia.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import { Editor } from 'editor/editor'
|
||||||
|
import { EditorNode, getType } from 'editor/types'
|
||||||
|
import {
|
||||||
|
safeGetSelection, safeGetRangeAt,
|
||||||
|
markNames, parentBlockNames,
|
||||||
|
setAuxiliaryToolbar,
|
||||||
|
} from 'editor/utils'
|
||||||
|
|
||||||
|
// TODO: tener ActiveStorage como import así no hacemos hacks
|
||||||
|
declare global {
|
||||||
|
const ActiveStorage: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile (file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const upload = new ActiveStorage.DirectUpload(
|
||||||
|
file,
|
||||||
|
origin + '/rails/active_storage/direct_uploads',
|
||||||
|
)
|
||||||
|
|
||||||
|
upload.create((error: any, blob: any) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
const url = `${origin}/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`
|
||||||
|
resolve(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlt (multimediaInnerEl: HTMLElement): string | null {
|
||||||
|
switch (multimediaInnerEl.tagName) {
|
||||||
|
case 'VIDEO':
|
||||||
|
case 'AUDIO':
|
||||||
|
return multimediaInnerEl.getAttribute('aria-label')
|
||||||
|
case 'IMG':
|
||||||
|
return (multimediaInnerEl as HTMLImageElement).alt
|
||||||
|
case 'IFRAME':
|
||||||
|
return multimediaInnerEl.title
|
||||||
|
default:
|
||||||
|
throw new Error('no pude conseguir el alt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setAlt (multimediaInnerEl: HTMLElement, value: string): void {
|
||||||
|
switch (multimediaInnerEl.tagName) {
|
||||||
|
case 'VIDEO':
|
||||||
|
case 'AUDIO':
|
||||||
|
multimediaInnerEl.setAttribute('aria-label', value)
|
||||||
|
break
|
||||||
|
case 'IMG':
|
||||||
|
(multimediaInnerEl as HTMLImageElement).alt = value
|
||||||
|
break
|
||||||
|
case 'IFRAME':
|
||||||
|
multimediaInnerEl.title = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('no pude setear el alt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const multimedia: EditorNode = {
|
||||||
|
selector: 'figure[data-multimedia]',
|
||||||
|
allowedChildren: 'ignore-children',
|
||||||
|
handleEmpty: 'remove',
|
||||||
|
create: () => {
|
||||||
|
const figureEl = document.createElement('figure')
|
||||||
|
figureEl.dataset.multimedia = ''
|
||||||
|
figureEl.contentEditable = 'false'
|
||||||
|
|
||||||
|
const placeholderEl = document.createElement('p')
|
||||||
|
placeholderEl.dataset.multimediaInner = ''
|
||||||
|
// TODO i18n
|
||||||
|
placeholderEl.append('¡Clickeame para subir un archivo!')
|
||||||
|
figureEl.appendChild(placeholderEl)
|
||||||
|
|
||||||
|
const descriptionEl = document.createElement('figcaption')
|
||||||
|
descriptionEl.contentEditable = 'true'
|
||||||
|
// TODO i18n
|
||||||
|
descriptionEl.append('Escribí acá la descripción del archivo.')
|
||||||
|
figureEl.appendChild(descriptionEl)
|
||||||
|
|
||||||
|
return figureEl
|
||||||
|
},
|
||||||
|
onClick (editor, el) {
|
||||||
|
if (!(el instanceof HTMLElement))
|
||||||
|
throw new Error('oh no')
|
||||||
|
el.dataset.editorSelected = ''
|
||||||
|
|
||||||
|
const innerEl = el.querySelector<HTMLElement>('[data-multimedia-inner]')
|
||||||
|
if (!innerEl) throw new Error('No hay multimedia válida')
|
||||||
|
if (innerEl.tagName !== 'P')
|
||||||
|
editor.toolbar.auxiliary.multimedia.altEl.value = getAlt(innerEl) || ''
|
||||||
|
|
||||||
|
setAuxiliaryToolbar(editor, editor.toolbar.auxiliary.multimedia.parentEl)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
function createElementWithFile (url: string, type: string): HTMLElement {
|
||||||
|
if (type.match(/^image\/.+$/)) {
|
||||||
|
const el = document.createElement('img')
|
||||||
|
el.dataset.multimediaInner = ''
|
||||||
|
el.src = url
|
||||||
|
return el
|
||||||
|
} else if (type.match(/^video\/.+$/)) {
|
||||||
|
const el = document.createElement('video')
|
||||||
|
el.controls = true
|
||||||
|
el.dataset.multimediaInner = ''
|
||||||
|
el.src = url
|
||||||
|
return el
|
||||||
|
} else if (type.match(/^audio\/.+$/)) {
|
||||||
|
const el = document.createElement('audio')
|
||||||
|
el.controls = true
|
||||||
|
el.dataset.multimediaInner = ''
|
||||||
|
el.src = url
|
||||||
|
return el
|
||||||
|
} else if (type.match(/^application\/pdf$/)) {
|
||||||
|
const el = document.createElement('iframe')
|
||||||
|
el.dataset.multimediaInner = ''
|
||||||
|
el.src = url
|
||||||
|
return el
|
||||||
|
} else {
|
||||||
|
// TODO: chequear si el archivo es válido antes de subir
|
||||||
|
throw new Error('Tipo de archivo no reconocido')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupAuxiliaryToolbar (editor: Editor): void {
|
||||||
|
editor.toolbar.auxiliary.multimedia.uploadEl.addEventListener('click', event => {
|
||||||
|
const files = editor.toolbar.auxiliary.multimedia.fileEl.files
|
||||||
|
if (!files) throw new Error('no hay archivos para subir')
|
||||||
|
const file = files[0]
|
||||||
|
|
||||||
|
const selectedEl = editor.contentEl
|
||||||
|
.querySelector<HTMLElement>('figure[data-editor-selected]')
|
||||||
|
if (!selectedEl)
|
||||||
|
throw new Error('No pude encontrar el elemento para setear el archivo')
|
||||||
|
|
||||||
|
selectedEl.dataset.editorLoading = ''
|
||||||
|
uploadFile(file)
|
||||||
|
.then(url => {
|
||||||
|
const innerEl = selectedEl.querySelector('[data-multimedia-inner]')
|
||||||
|
if (!innerEl) throw new Error('No hay multimedia a reemplazar')
|
||||||
|
|
||||||
|
const el = createElementWithFile(url, file.type)
|
||||||
|
setAlt(el, editor.toolbar.auxiliary.multimedia.altEl.value)
|
||||||
|
selectedEl.replaceChild(el, innerEl)
|
||||||
|
|
||||||
|
delete selectedEl.dataset.editorError
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
// TODO: mostrar error
|
||||||
|
selectedEl.dataset.editorError = ''
|
||||||
|
})
|
||||||
|
.finally(() => { delete selectedEl.dataset.editorLoading })
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.toolbar.auxiliary.multimedia.removeEl.addEventListener('click', event => {
|
||||||
|
const selectedEl = editor.contentEl
|
||||||
|
.querySelector<HTMLElement>('figure[data-editor-selected]')
|
||||||
|
if (!selectedEl)
|
||||||
|
throw new Error('No pude encontrar el elemento para borrar')
|
||||||
|
|
||||||
|
selectedEl.parentElement?.removeChild(selectedEl)
|
||||||
|
setAuxiliaryToolbar(editor, null)
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.toolbar.auxiliary.multimedia.altEl.addEventListener('input', event => {
|
||||||
|
const selectedEl = editor.contentEl
|
||||||
|
.querySelector<HTMLAnchorElement>('figure[data-editor-selected]')
|
||||||
|
if (!selectedEl)
|
||||||
|
throw new Error('No pude encontrar el multimedia para setear el alt')
|
||||||
|
|
||||||
|
const innerEl = selectedEl.querySelector<HTMLElement>('[data-multimedia-inner]')
|
||||||
|
if (!innerEl) throw new Error('No hay multimedia a para setear el alt')
|
||||||
|
|
||||||
|
setAlt(innerEl, editor.toolbar.auxiliary.multimedia.altEl.value)
|
||||||
|
})
|
||||||
|
editor.toolbar.auxiliary.multimedia.altEl.addEventListener('keydown', event => {
|
||||||
|
if (event.keyCode == 13) event.preventDefault()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupButtons (editor: Editor): void {
|
||||||
|
const buttonEl = editor.toolbarEl.querySelector('[data-editor-button="multimedia"]')
|
||||||
|
if (!buttonEl) throw new Error('No encontre el botón de multimedia')
|
||||||
|
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 el = multimedia.create(editor)
|
||||||
|
parentEl.insertBefore(el, blockEl.nextElementSibling)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { EditorNode, getType } 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,
|
allowedChildren: [...blockNames, 'multimedia'],
|
||||||
handleEmpty: 'remove',
|
handleEmpty: 'remove',
|
||||||
create,
|
create,
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,26 +15,26 @@
|
||||||
|
|
||||||
.editor-toolbar
|
.editor-toolbar
|
||||||
.editor-primary-toolbar.scrollbar-black
|
.editor-primary-toolbar.scrollbar-black
|
||||||
%button.btn{ data: { editor: { button: 'mark-bold' } } }= t('editor.bold')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-bold' } } }= t('editor.bold')
|
||||||
%button.btn{ data: { editor: { button: 'mark-italic' } } }= t('editor.italic')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-italic' } } }= t('editor.italic')
|
||||||
%button.btn{ data: { editor: { button: 'mark-deleted' } } }= t('editor.deleted')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-deleted' } } }= t('editor.deleted')
|
||||||
%button.btn{ data: { editor: { button: 'mark-underline' } } }= t('editor.underline')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-underline' } } }= t('editor.underline')
|
||||||
%button.btn{ data: { editor: { button: 'mark-super' } } }= t('editor.super')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-super' } } }= t('editor.super')
|
||||||
%button.btn{ data: { editor: { button: 'mark-sub' } } }= t('editor.sub')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-sub' } } }= t('editor.sub')
|
||||||
%button.btn{ data: { editor: { button: 'mark-mark' } } }= t('editor.mark')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-mark' } } }= t('editor.mark')
|
||||||
%button.btn{ data: { editor: { button: 'mark-link' } } }= t('editor.link')
|
%button.btn{ type: 'button', data: { editor: { button: 'mark-link' } } }= t('editor.link')
|
||||||
%button.btn{ data: { editor: { button: 'block-h1' } } }= t('editor.h1')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h1' } } }= t('editor.h1')
|
||||||
%button.btn{ data: { editor: { button: 'block-h2' } } }= t('editor.h2')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h2' } } }= t('editor.h2')
|
||||||
%button.btn{ data: { editor: { button: 'block-h3' } } }= t('editor.h3')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h3' } } }= t('editor.h3')
|
||||||
%button.btn{ data: { editor: { button: 'block-h4' } } }= t('editor.h4')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h4' } } }= t('editor.h4')
|
||||||
%button.btn{ data: { editor: { button: 'block-h5' } } }= t('editor.h5')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h5' } } }= t('editor.h5')
|
||||||
%button.btn{ data: { editor: { button: 'block-h6' } } }= t('editor.h6')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-h6' } } }= t('editor.h6')
|
||||||
%button.btn{ data: { editor: { button: 'block-unordered_list' } } }= t('editor.ul')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-unordered_list' } } }= t('editor.ul')
|
||||||
%button.btn{ data: { editor: { button: 'block-ordered_list' } } }= t('editor.ol')
|
%button.btn{ type: 'button', data: { editor: { button: 'block-ordered_list' } } }= t('editor.ol')
|
||||||
%button.btn{ data: { editor: { button: 'parentBlock-left' } } }= t('editor.left')
|
%button.btn{ type: 'button', data: { editor: { button: 'parentBlock-left' } } }= t('editor.left')
|
||||||
%button.btn{ data: { editor: { button: 'parentBlock-center' } } }= t('editor.center')
|
%button.btn{ type: 'button', data: { editor: { button: 'parentBlock-center' } } }= t('editor.center')
|
||||||
%button.btn{ data: { editor: { button: 'parentBlock-right' } } }= t('editor.right')
|
%button.btn{ type: 'button', data: { editor: { button: 'parentBlock-right' } } }= t('editor.right')
|
||||||
-#%button.btn{ data: { editor: { button: 'multimedia' } } }= t('editor.multimedia')
|
%button.btn{ type: 'button', data: { editor: { button: 'multimedia' } } }= t('editor.multimedia')
|
||||||
|
|
||||||
-#
|
-#
|
||||||
HAML cringe
|
HAML cringe
|
||||||
|
@ -45,7 +45,6 @@
|
||||||
%input.form-control{ type: 'color', name: 'mark-color' }/
|
%input.form-control{ type: 'color', name: 'mark-color' }/
|
||||||
|
|
||||||
%div{ data: { editor: { auxiliary: 'multimedia' } } }
|
%div{ data: { editor: { auxiliary: 'multimedia' } } }
|
||||||
.row
|
|
||||||
.col-12.col-lg.form-group.d-flex.align-items-end
|
.col-12.col-lg.form-group.d-flex.align-items-end
|
||||||
.custom-file
|
.custom-file
|
||||||
%input.custom-file-input{ type: 'file', name: 'multimedia-file' }/
|
%input.custom-file-input{ type: 'file', name: 'multimedia-file' }/
|
||||||
|
@ -54,6 +53,7 @@
|
||||||
.col-12.col-lg.form-group
|
.col-12.col-lg.form-group
|
||||||
%label{ for: 'multimedia-alt' }= t('editor.description')
|
%label{ for: 'multimedia-alt' }= t('editor.description')
|
||||||
%input.form-control{ type: 'text', name: 'multimedia-alt' }/
|
%input.form-control{ type: 'text', name: 'multimedia-alt' }/
|
||||||
|
%button.btn{ type: 'button', name: 'multimedia-remove' }= t('editor.file.multimedia-remove')
|
||||||
|
|
||||||
.form-group{ data: { editor: { auxiliary: 'link' } } }
|
.form-group{ data: { editor: { auxiliary: 'link' } } }
|
||||||
%label{ for: 'link-url' }= t('editor.url')
|
%label{ for: 'link-url' }= t('editor.url')
|
||||||
|
|
Loading…
Reference in a new issue