Merge branch 'editor-nuevo' of 0xacab.org:sutty/sutty into editor-nuevo
This commit is contained in:
commit
ab502f402e
8 changed files with 127 additions and 22 deletions
|
@ -88,11 +88,25 @@ const blocks = {
|
||||||
return el
|
return el
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
figure: {
|
||||||
|
selector: "FIGURE",
|
||||||
|
noButton: true
|
||||||
|
},
|
||||||
|
figcaption: {
|
||||||
|
selector: "FIGCAPTION",
|
||||||
|
noButton: true,
|
||||||
|
},
|
||||||
audio: {
|
audio: {
|
||||||
selector: "AUDIO",
|
selector: "AUDIO",
|
||||||
createFn: editorEl => {
|
createFn: editorEl => {
|
||||||
const el = document.createElement("AUDIO")
|
const el = document.createElement("FIGURE")
|
||||||
el.controls = true
|
|
||||||
|
el.appendChild(document.createElement("AUDIO"))
|
||||||
|
el.appendChild(document.createElement("FIGCAPTION"))
|
||||||
|
|
||||||
|
el.children[0].controls = true
|
||||||
|
el.children[1].innerText = "Toca el borde para subir un archivo de audio"
|
||||||
|
|
||||||
return el
|
return el
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -100,7 +114,13 @@ const blocks = {
|
||||||
selector: "VIDEO",
|
selector: "VIDEO",
|
||||||
createFn: editorEl => {
|
createFn: editorEl => {
|
||||||
const el = document.createElement("VIDEO")
|
const el = document.createElement("VIDEO")
|
||||||
el.controls = true
|
el.poster = "/public/placeholder.png"
|
||||||
|
// Para poder seleccionar el video tenemos que sacarle los
|
||||||
|
// controles, pero queremos poder verlos para reproducir el video.
|
||||||
|
// Al hacer click le damos los controles y al salir se los sacamos
|
||||||
|
// para poder hacer click de vuelta
|
||||||
|
el.addEventListener('click', event => event.target.controls = true)
|
||||||
|
el.addEventListener('focusout', event => event.target.controls = false)
|
||||||
return el
|
return el
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -108,7 +128,13 @@ const blocks = {
|
||||||
pdf: {
|
pdf: {
|
||||||
selector: "IFRAME",
|
selector: "IFRAME",
|
||||||
createFn: editorEl => {
|
createFn: editorEl => {
|
||||||
const el = document.createElement("IFRAME")
|
const el = document.createElement("FIGURE")
|
||||||
|
|
||||||
|
el.appendChild(document.createElement("IFRAME"))
|
||||||
|
el.appendChild(document.createElement("FIGCAPTION"))
|
||||||
|
|
||||||
|
el.children[1].innerText = "Toca el borde para subir un archivo PDF"
|
||||||
|
|
||||||
return el
|
return el
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -223,6 +249,21 @@ const typesWithProperties = {
|
||||||
}, false)
|
}, false)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
figure: {
|
||||||
|
selector: blocks.figure.selector,
|
||||||
|
actualInput (el) {
|
||||||
|
// TODO: Cuando tengamos otros iframes hay que seleccionarlos de
|
||||||
|
// otra forma.
|
||||||
|
const tag = el.children[0].tagName.toLowerCase()
|
||||||
|
|
||||||
|
return typesWithProperties[(tag === 'iframe' ? 'pdf' : tag)]
|
||||||
|
},
|
||||||
|
updateInput (el, editorEl) {
|
||||||
|
typesWithProperties.figure.actualInput(el).updateInput(el.children[0], editorEl)
|
||||||
|
},
|
||||||
|
disableInput (editorEl) {},
|
||||||
|
setupInput (editorEl, contentEl) {},
|
||||||
|
},
|
||||||
audio: {
|
audio: {
|
||||||
selector: blocks.audio.selector,
|
selector: blocks.audio.selector,
|
||||||
updateInput (el, editorEl) {
|
updateInput (el, editorEl) {
|
||||||
|
@ -239,11 +280,13 @@ const typesWithProperties = {
|
||||||
setupInput (editorEl, contentEl) {
|
setupInput (editorEl, contentEl) {
|
||||||
const audioFileEl = editorEl.querySelector(`*[data-prop="audio-file"]`)
|
const audioFileEl = editorEl.querySelector(`*[data-prop="audio-file"]`)
|
||||||
audioFileEl.addEventListener("input", event => {
|
audioFileEl.addEventListener("input", event => {
|
||||||
const audioEl = getSelected(contentEl)
|
const figureEl = getSelected(contentEl)
|
||||||
if (!audioEl) return
|
if (!figureEl) return
|
||||||
|
|
||||||
const file = audioFileEl.files[0]
|
const file = audioFileEl.files[0]
|
||||||
|
|
||||||
|
const audioEl = figureEl.querySelector('audio')
|
||||||
|
|
||||||
audioEl.src = URL.createObjectURL(file)
|
audioEl.src = URL.createObjectURL(file)
|
||||||
audioEl.dataset.editorLoading = true
|
audioEl.dataset.editorLoading = true
|
||||||
uploadFile(file)
|
uploadFile(file)
|
||||||
|
@ -283,6 +326,7 @@ const typesWithProperties = {
|
||||||
|
|
||||||
const file = videoFileEl.files[0]
|
const file = videoFileEl.files[0]
|
||||||
|
|
||||||
|
videoEl.poster = ""
|
||||||
videoEl.src = URL.createObjectURL(file)
|
videoEl.src = URL.createObjectURL(file)
|
||||||
videoEl.dataset.editorLoading = true
|
videoEl.dataset.editorLoading = true
|
||||||
uploadFile(file)
|
uploadFile(file)
|
||||||
|
@ -317,10 +361,11 @@ const typesWithProperties = {
|
||||||
setupInput (editorEl, contentEl) {
|
setupInput (editorEl, contentEl) {
|
||||||
const pdfFileEl = editorEl.querySelector(`*[data-prop="pdf-file"]`)
|
const pdfFileEl = editorEl.querySelector(`*[data-prop="pdf-file"]`)
|
||||||
pdfFileEl.addEventListener("input", event => {
|
pdfFileEl.addEventListener("input", event => {
|
||||||
const pdfEl = getSelected(contentEl)
|
const figureEl = getSelected(contentEl)
|
||||||
if (!pdfEl) return
|
if (!figureEl) return
|
||||||
|
|
||||||
const file = pdfFileEl.files[0]
|
const file = pdfFileEl.files[0]
|
||||||
|
const pdfEl = figureEl.children[0]
|
||||||
|
|
||||||
pdfEl.src = URL.createObjectURL(file)
|
pdfEl.src = URL.createObjectURL(file)
|
||||||
pdfEl.dataset.editorLoading = true
|
pdfEl.dataset.editorLoading = true
|
||||||
|
|
|
@ -471,6 +471,12 @@ function setupEditor (editorEl) {
|
||||||
document.addEventListener(editorBtn("video"), () => setAuxiliaryToolbar(editorEl, "video"))
|
document.addEventListener(editorBtn("video"), () => setAuxiliaryToolbar(editorEl, "video"))
|
||||||
document.addEventListener(editorBtn("pdf"), () => setAuxiliaryToolbar(editorEl, "pdf"))
|
document.addEventListener(editorBtn("pdf"), () => setAuxiliaryToolbar(editorEl, "pdf"))
|
||||||
|
|
||||||
|
for (const video of document.querySelectorAll('.editor .editor-content video')) {
|
||||||
|
video.addEventListener('click', event => event.target.controls = true)
|
||||||
|
video.addEventListener('focusout', event => event.target.controls = false)
|
||||||
|
video.controls = false
|
||||||
|
}
|
||||||
|
|
||||||
cleanContent(contentEl)
|
cleanContent(contentEl)
|
||||||
htmlEl.value = contentEl.innerHTML
|
htmlEl.value = contentEl.innerHTML
|
||||||
fixContent(contentEl)
|
fixContent(contentEl)
|
||||||
|
@ -483,6 +489,8 @@ function stringifyAllowedStyle (element) {
|
||||||
|
|
||||||
document.addEventListener("turbolinks:load", () => {
|
document.addEventListener("turbolinks:load", () => {
|
||||||
for (const editorEl of document.querySelectorAll(".editor")) {
|
for (const editorEl of document.querySelectorAll(".editor")) {
|
||||||
|
if (!editorEl.querySelector('.editor-toolbar')) continue
|
||||||
|
|
||||||
setupEditor(editorEl)
|
setupEditor(editorEl)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,6 +23,14 @@ $component-active-bg: $magenta;
|
||||||
@import "bootstrap";
|
@import "bootstrap";
|
||||||
@import "editor";
|
@import "editor";
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
.editor-content {
|
||||||
|
figure {
|
||||||
|
border: 1px solid transparentize($magenta, 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--foreground: #{$black};
|
--foreground: #{$black};
|
||||||
--background: #{$white};
|
--background: #{$white};
|
||||||
|
|
|
@ -14,9 +14,14 @@
|
||||||
.selected { outline: #f206f9 solid medium; }
|
.selected { outline: #f206f9 solid medium; }
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
border: 0;
|
||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
img, video, iframe, audio {
|
img, video, iframe, audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
@ -45,12 +50,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-content {
|
word-wrap: break-word;
|
||||||
word-wrap: break-word;
|
div[data-align="left"] { text-align: left; }
|
||||||
div[data-align="left"] { text-align: left; }
|
div[data-align="center"] { text-align: center; }
|
||||||
div[data-align="center"] { text-align: center; }
|
div[data-align="right"] { text-align: right; }
|
||||||
div[data-align="right"] { text-align: right; }
|
|
||||||
}
|
min-height: 480px;
|
||||||
|
|
||||||
*[data-editor-loading] {
|
*[data-editor-loading] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
|
@ -14,4 +14,44 @@ class MetadataContent < MetadataTemplate
|
||||||
def front_matter?
|
def front_matter?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Limpiar el HTML que recibimos
|
||||||
|
#
|
||||||
|
# TODO: En lugar de comprobar el Content Type acá, restringir los
|
||||||
|
# tipos de archivo a aceptar en ActiveStorage.
|
||||||
|
def sanitize(html_string)
|
||||||
|
html = Nokogiri::HTML.fragment(super html_string)
|
||||||
|
elements = 'img,audio,video,iframe'
|
||||||
|
|
||||||
|
# Eliminar elementos sin src y comprobar su origen
|
||||||
|
html.css(elements).each do |element|
|
||||||
|
unless element['src']
|
||||||
|
element.remove
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
uri = URI element['src']
|
||||||
|
|
||||||
|
# No permitimos recursos externos
|
||||||
|
element.remove unless uri.hostname.end_with? Site.domain
|
||||||
|
rescue URI::Error
|
||||||
|
element.remove
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Eliminar figure sin contenido
|
||||||
|
html.css('figure').each do |figure|
|
||||||
|
figure.remove if figure.css(elements).empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Los videos y audios necesitan controles
|
||||||
|
html.css('audio,video').each do |resource|
|
||||||
|
resource['controls'] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
html.to_s.html_safe
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
:value, :help, :required, :errors, :post,
|
:value, :help, :required, :errors, :post,
|
||||||
:layout, keyword_init: true) do
|
:layout, keyword_init: true) do
|
||||||
|
|
||||||
attr_reader :value_was
|
attr_reader :value_was
|
||||||
|
|
||||||
def value=(new_value)
|
def value=(new_value)
|
||||||
|
@ -133,7 +132,7 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||||
end
|
end
|
||||||
|
|
||||||
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].freeze
|
@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].freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
# Decifra el valor
|
# Decifra el valor
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
-#
|
-#
|
||||||
HAML cringe
|
HAML cringe
|
||||||
TODO: generar IDs para labels
|
TODO: generar IDs para labels
|
||||||
.editor-auxiliary-toolbar.scrollbar-black{ data: { editor: { auxiliary: { toolbar: '' } } } }
|
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor: { auxiliary: { toolbar: '' } } } }
|
||||||
.form-group{ data: { editor: { auxiliary: 'mark' } } }
|
.form-group{ data: { editor: { auxiliary: 'mark' } } }
|
||||||
%label{ for: 'mark-color' }= t('editor.color')
|
%label{ for: 'mark-color' }= t('editor.color')
|
||||||
%input{ type: 'color', data: { prop: 'mark-color' } }/
|
%input.form-control{ type: 'color', data: { prop: 'mark-color' } }/
|
||||||
|
|
||||||
%div{ data: { editor: { auxiliary: 'img' } } }
|
%div{ data: { editor: { auxiliary: 'img' } } }
|
||||||
.row
|
.row
|
||||||
|
@ -52,12 +52,12 @@
|
||||||
-# https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
-# https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||||
.form-group{ data: { editor: { auxiliary: 'audio' } } }
|
.form-group{ data: { editor: { auxiliary: 'audio' } } }
|
||||||
.custom-file
|
.custom-file
|
||||||
%input.custom-file-input{ type: 'file', data: { prop: 'audio-file' }, accept: 'audio/flac,audio/mp4,audio/ogg,audio/webm,audio/mp3' }/
|
%input.custom-file-input{ type: 'file', data: { prop: 'audio-file' }, accept: 'audio/*' }/
|
||||||
%label.custom-file-label{ for: 'audio-file' }= t('editor.file.audio')
|
%label.custom-file-label{ for: 'audio-file' }= t('editor.file.audio')
|
||||||
|
|
||||||
.form-group{ data: { editor: { auxiliary: 'video' } } }
|
.form-group{ data: { editor: { auxiliary: 'video' } } }
|
||||||
.custom-file
|
.custom-file
|
||||||
%input.custom-file-input{ type: 'file', data: { prop: 'video-file' }, accept: 'video/mp4,video/ogg,video/webm' }/
|
%input.custom-file-input{ type: 'file', data: { prop: 'video-file' }, accept: 'video/*' }/
|
||||||
%label.custom-file-label{ for: 'video-file' }= t('editor.file.video')
|
%label.custom-file-label{ for: 'video-file' }= t('editor.file.video')
|
||||||
|
|
||||||
.form-group{ data: { editor: { auxiliary: 'pdf' } } }
|
.form-group{ data: { editor: { auxiliary: 'pdf' } } }
|
||||||
|
@ -69,5 +69,5 @@
|
||||||
%label{ for: 'link-href' }= t('editor.url')
|
%label{ for: 'link-href' }= t('editor.url')
|
||||||
%input.form-control{ type: 'url', data: { prop: 'link-href' } }/
|
%input.form-control{ type: 'url', data: { prop: 'link-href' } }/
|
||||||
|
|
||||||
.editor-content.form-control.h-auto{ contenteditable: 'true' }
|
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
|
||||||
= metadata.value.html_safe
|
= metadata.value.html_safe
|
||||||
|
|
|
@ -33,5 +33,5 @@
|
||||||
- @post.attributes.each do |attr|
|
- @post.attributes.each do |attr|
|
||||||
- next if @post.send(attr).front_matter?
|
- next if @post.send(attr).front_matter?
|
||||||
|
|
||||||
%section{ id: attr, dir: dir }
|
%section.editor{ id: attr, dir: dir }
|
||||||
= @post.send(attr).to_s.html_safe
|
= @post.send(attr).to_s.html_safe
|
||||||
|
|
Loading…
Reference in a new issue