diff --git a/app/javascript/editor/types.js b/app/javascript/editor/types.js index f5ffefca..af796754 100644 --- a/app/javascript/editor/types.js +++ b/app/javascript/editor/types.js @@ -1,3 +1,5 @@ +import multimedia from './types/multimedia' + export const setAuxiliaryToolbar = (editorEl, toolbarName) => { const toolbarEl = editorEl.querySelector(`*[data-editor-auxiliary-toolbar]`) for (const otherEl of toolbarEl.childNodes) { @@ -14,24 +16,6 @@ export const moveChildren = (from, to, toRef) => { while (from.firstChild) to.insertBefore(from.firstChild, toRef); } -const uploadFile = (file) => { - return new Promise((resolve, reject) => { - const upload = new ActiveStorage.DirectUpload( - file, - origin + '/rails/active_storage/direct_uploads', - ) - - upload.create((error, blob) => { - if (error) { - reject(error) - } else { - const url = `${origin}/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}` - resolve(url) - } - }) - }) -} - export const marks = { bold: { selector: "strong", @@ -113,65 +97,7 @@ export const blocks = { selector: "OL", setFn: tagNameSetFn("OL"), }, - img: { - selector: "IMG", - createFn: editorEl => { - const el = document.createElement("IMG") - el.src = "/placeholder.png" - el.alt = "" - return el - }, - }, - figure: { - selector: "FIGURE", - noButton: true - }, - figcaption: { - selector: "FIGCAPTION", - noButton: true, - }, - audio: { - selector: "AUDIO", - createFn: editorEl => { - const el = document.createElement("FIGURE") - - 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 - }, - }, - video: { - selector: "VIDEO", - createFn: editorEl => { - const el = document.createElement("VIDEO") - el.poster = "/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 - }, - }, - // PDF - pdf: { - selector: "IFRAME", - createFn: editorEl => { - 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 - }, - }, + multimedia: multimedia.block, } const divWithStyleCreateFn = styleFn => () => { @@ -251,189 +177,5 @@ export const typesWithProperties = { }, false) }, }, - img: { - selector: blocks.img.selector, - updateInput (el, editorEl) { - setAuxiliaryToolbar(editorEl, "img") - - const imgFileEl = editorEl.querySelector(`*[data-prop="img-file"]`) - imgFileEl.disabled = false - // XXX: No se puede cambiar el texto, ¡esto puede ser confuso! - - const imgAltEl = editorEl.querySelector(`*[data-prop="img-alt"]`) - imgAltEl.disabled = false - imgAltEl.value = el.alt - }, - disableInput (editorEl) { - const imgFileEl = editorEl.querySelector(`*[data-prop="img-file"]`) - imgFileEl.disabled = true - - const imgAltEl = editorEl.querySelector(`*[data-prop="img-alt"]`) - imgAltEl.disabled = true - imgAltEl.value = "" - }, - setupInput (editorEl, contentEl) { - const imgFileEl = editorEl.querySelector(`*[data-prop="img-file"]`) - imgFileEl.addEventListener("input", event => { - const imgEl = contentEl.querySelector("img.selected") - if (!imgEl) return - - const file = imgFileEl.files[0] - - imgEl.dataset.editorLoading = true - uploadFile(file) - .then(url => { - imgEl.src = url - delete imgEl.dataset.editorError - }) - .catch(err => { - // TODO: mostrar error - console.error(err) - imgEl.dataset.editorError = true - }) - .finally(() => { - delete imgEl.dataset.editorLoading - }) - }, false) - - const imgAltEl = editorEl.querySelector(`*[data-prop="img-alt"]`) - imgAltEl.addEventListener("input", event => { - const imgEl = contentEl.querySelector("img.selected") - if (imgEl) imgEl.alt = imgAltEl.value - }, 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: { - selector: blocks.audio.selector, - updateInput (el, editorEl) { - setAuxiliaryToolbar(editorEl, "audio") - - const audioFileEl = editorEl.querySelector(`*[data-prop="audio-file"]`) - audioFileEl.disabled = false - // XXX: No se puede cambiar el texto, ¡esto puede ser confuso! - }, - disableInput (editorEl) { - const audioFileEl = editorEl.querySelector(`*[data-prop="audio-file"]`) - audioFileEl.disabled = true - }, - setupInput (editorEl, contentEl) { - const audioFileEl = editorEl.querySelector(`*[data-prop="audio-file"]`) - audioFileEl.addEventListener("input", event => { - const figureEl = getSelected(contentEl) - if (!figureEl) return - - const file = audioFileEl.files[0] - - const audioEl = figureEl.querySelector('audio') - - audioEl.dataset.editorLoading = true - uploadFile(file) - .then(url => { - audioEl.src = url - delete audioEl.dataset.editorError - }) - .catch(err => { - // TODO: mostrar error - console.error(err) - audioEl.dataset.editorError = true - }) - .finally(() => { - delete audioEl.dataset.editorLoading - }) - }, false) - }, - }, - video: { - selector: blocks.video.selector, - updateInput (el, editorEl) { - setAuxiliaryToolbar(editorEl, "video") - - const videoFileEl = editorEl.querySelector(`*[data-prop="video-file"]`) - videoFileEl.disabled = false - // XXX: No se puede cambiar el texto, ¡esto puede ser confuso! - }, - disableInput (editorEl) { - const videoFileEl = editorEl.querySelector(`*[data-prop="video-file"]`) - videoFileEl.disabled = true - }, - setupInput (editorEl, contentEl) { - const videoFileEl = editorEl.querySelector(`*[data-prop="video-file"]`) - videoFileEl.addEventListener("input", event => { - const videoEl = getSelected(contentEl) - if (!videoEl) return - - const file = videoFileEl.files[0] - - videoEl.poster = "" - videoEl.dataset.editorLoading = true - uploadFile(file) - .then(url => { - videoEl.src = url - delete videoEl.dataset.editorError - }) - .catch(err => { - // TODO: mostrar error - console.error(err) - videoEl.dataset.editorError = true - }) - .finally(() => { - delete videoEl.dataset.editorLoading - }) - }, false) - }, - }, - pdf: { - selector: blocks.pdf.selector, - updateInput (el, editorEl) { - setAuxiliaryToolbar(editorEl, "pdf") - - const pdfFileEl = editorEl.querySelector(`*[data-prop="pdf-file"]`) - pdfFileEl.disabled = false - // XXX: No se puede cambiar el texto, ¡esto puede ser confuso! - }, - disableInput (editorEl) { - const pdfFileEl = editorEl.querySelector(`*[data-prop="pdf-file"]`) - pdfFileEl.disabled = true - }, - setupInput (editorEl, contentEl) { - const pdfFileEl = editorEl.querySelector(`*[data-prop="pdf-file"]`) - pdfFileEl.addEventListener("input", event => { - const figureEl = getSelected(contentEl) - if (!figureEl) return - - const file = pdfFileEl.files[0] - const pdfEl = figureEl.children[0] - - pdfEl.dataset.editorLoading = true - uploadFile(file) - .then(url => { - pdfEl.src = url - delete pdfEl.dataset.editorError - }) - .catch(err => { - // TODO: mostrar error - console.error(err) - pdfEl.dataset.editorError = true - }) - .finally(() => { - delete pdfEl.dataset.editorLoading - }) - }, false) - }, - }, + multimedia: multimedia.typeWithProperty, } diff --git a/app/javascript/editor/types/multimedia.js b/app/javascript/editor/types/multimedia.js new file mode 100644 index 00000000..7546a893 --- /dev/null +++ b/app/javascript/editor/types/multimedia.js @@ -0,0 +1,148 @@ +import { setAuxiliaryToolbar } from '../types' + +const uploadFile = (file) => { + return new Promise((resolve, reject) => { + const upload = new ActiveStorage.DirectUpload( + file, + origin + '/rails/active_storage/direct_uploads', + ) + + upload.create((error, blob) => { + if (error) { + reject(error) + } else { + const url = `${origin}/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}` + resolve(url) + } + }) + }) +} + +const srcMakeElement = tag => url => { + const el = document.createElement(tag) + el.src = url + return el +} + +const multimediaKinds = { + img: { + mime: /^image\/.+$/, + makeElement: srcMakeElement('img'), + }, + audio: { + mime: /^audio\/.+$/, + makeElement: srcMakeElement('audio'), + }, + video: { + mime: /^video\/.+$/, + makeElement: srcMakeElement('video'), + }, + pdf: { + mime: /^application\/pdf$/, + makeElement: srcMakeElement('iframe'), + }, +} + +const setMultimedia = (figureEl, url, mimeType) => { + let kind + for (const _kind of Object.values(multimediaKinds)) { + if (mimeType.match(_kind.mime)) { + kind = _kind + break + } + } + // TODO: mostrar error + if (!kind) + throw new Error('¡Archivo no soportado!') + + const currentMultimedia = figureEl.querySelector('*[data-editor-multimedia-element]') + const newMultimedia = kind.makeElement(url) + newMultimedia.dataset.editorMultimediaElement = true + if (currentMultimedia) + figureEl.replaceChild(newMultimedia, currentMultimedia) + else figureEl.appendChild(newMultimedia) +} + +const block = { + selector: "FIGURE[data-editor-multimedia]", + createFn: editorEl => { + const el = document.createElement("FIGURE") + el.dataset.editorMultimedia = true + + const placeholderEl = document.createElement("p") + placeholderEl.dataset.editorMultimediaElement = true + placeholderEl.append("Toca el borde para subir un archivo") + el.appendChild(placeholderEl) + + const figcaptionEl = document.createElement("FIGCAPTION") + figcaptionEl.append("Escribí una descripción del archivo") + figcaptionEl.controls = true + el.appendChild(figcaptionEl) + + return el + }, +} +const typeWithProperty = { + selector: block.selector, + updateInput (el, editorEl) { + setAuxiliaryToolbar(editorEl, "multimedia") + + const fileEl = editorEl.querySelector(`*[data-prop="multimedia-file"]`) + fileEl.disabled = false + // XXX: No se puede cambiar el texto del archivo seleccionado, + // ¡esto puede ser confuso! + + const altEl = editorEl.querySelector(`*[data-prop="multimedia-alt"]`) + altEl.disabled = false + altEl.value = el.alt + + const uploadEl = editorEl.querySelector(`*[data-prop="multimedia-file-upload"]`) + uploadEl.disabled = false + }, + disableInput (editorEl) { + const fileEl = editorEl.querySelector(`*[data-prop="multimedia-file"]`) + fileEl.disabled = true + + const altEl = editorEl.querySelector(`*[data-prop="multimedia-alt"]`) + altEl.disabled = true + altEl.value = "" + + const uploadEl = editorEl.querySelector(`*[data-prop="multimedia-file-upload"]`) + uploadEl.disabled = true + }, + setupInput (editorEl, contentEl) { + const fileEl = editorEl.querySelector(`*[data-prop="multimedia-file"]`) + const uploadEl = editorEl.querySelector(`*[data-prop="multimedia-file-upload"]`) + + uploadEl.addEventListener("click", event => { + const selectedEl = contentEl.querySelector("figure.selected") + if (!selectedEl) return + + const file = fileEl.files[0] + + selectedEl.dataset.editorLoading = true + uploadFile(file) + .then(url => { + setMultimedia(selectedEl, url, file.type) + delete selectedEl.dataset.editorError + }) + .catch(err => { + // TODO: mostrar error + console.error(err) + selectedEl.dataset.editorError = true + }) + .finally(() => { + delete selectedEl.dataset.editorLoading + }) + }, false) + + // TODO + const altEl = editorEl.querySelector(`*[data-prop="multimedia-alt"]`) + altEl.addEventListener("input", event => { + const imgEl = contentEl.querySelector("img.selected") + if (imgEl) imgEl.alt = imgAltEl.value + }, false) + }, +} + +export default { block, typeWithProperty } diff --git a/app/views/posts/attributes/_content.haml b/app/views/posts/attributes/_content.haml index 1161e6e3..e5d327d9 100644 --- a/app/views/posts/attributes/_content.haml +++ b/app/views/posts/attributes/_content.haml @@ -34,10 +34,7 @@ %button.btn{ data: { button: 'left' } }= t('editor.left') %button.btn{ data: { button: 'center' } }= t('editor.center') %button.btn{ data: { button: 'right' } }= t('editor.right') - %button.btn{ data: { button: 'img' } }= t('editor.img') - %button.btn{ data: { button: 'video' } }= t('editor.video') - %button.btn{ data: { button: 'audio' } }= t('editor.audio') - %button.btn{ data: { button: 'pdf' } }= t('editor.pdf') + %button.btn{ data: { button: 'multimedia' } }= t('editor.multimedia') -# HAML cringe @@ -47,31 +44,16 @@ %label{ for: 'mark-color' }= t('editor.color') %input.form-control{ type: 'color', data: { prop: 'mark-color' } }/ - %div{ data: { editor: { auxiliary: 'img' } } } + %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: 'img-file' }, accept: 'image/*' }/ - %label.custom-file-label{ for: 'img-file' }= t('editor.file.img') + %input.custom-file-input{ type: 'file', data: { prop: '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') .col-12.col-lg.form-group - %label{ for: 'img-alt' }= t('editor.description') - %input.form-control{ type: 'text', data: { prop: 'img-alt' } }/ - - -# https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers - .form-group{ data: { editor: { auxiliary: 'audio' } } } - .custom-file - %input.custom-file-input{ type: 'file', data: { prop: 'audio-file' }, accept: 'audio/*' }/ - %label.custom-file-label{ for: 'audio-file' }= t('editor.file.audio') - - .form-group{ data: { editor: { auxiliary: 'video' } } } - .custom-file - %input.custom-file-input{ type: 'file', data: { prop: 'video-file' }, accept: 'video/*' }/ - %label.custom-file-label{ for: 'video-file' }= t('editor.file.video') - - .form-group{ data: { editor: { auxiliary: 'pdf' } } } - .custom-file - %input.custom-file-input{ type: 'file', data: { prop: 'pdf-file' }, accept: 'application/pdf' }/ - %label.custom-file-label{ for: 'pdf-file' }= t('editor.file.pdf') + %label{ for: 'multimedia-alt' }= t('editor.description') + %input.form-control{ type: 'text', data: { prop: 'multimedia-alt' } }/ .form-group{ data: { editor: { auxiliary: 'a' } } } %label{ for: 'a-href' }= t('editor.url')