mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-26 00:06:21 +00:00
Merge branch 'editor-nuevo' into rails
This commit is contained in:
commit
3516d910b2
20 changed files with 2343 additions and 150 deletions
388
app/assets/javascripts/01-types.js
Normal file
388
app/assets/javascripts/01-types.js
Normal file
|
@ -0,0 +1,388 @@
|
|||
function setAuxiliaryToolbar (editorEl, toolbarName) {
|
||||
const toolbarEl = editorEl.querySelector(`*[data-editor-auxiliary-toolbar]`)
|
||||
for (const otherEl of toolbarEl.childNodes) {
|
||||
if (otherEl.nodeType !== Node.ELEMENT_NODE) continue
|
||||
otherEl.classList.remove("editor-auxiliary-tool-active")
|
||||
}
|
||||
if (toolbarName) {
|
||||
const auxEl = editorEl.querySelector(`*[data-editor-auxiliary="${toolbarName}"]`)
|
||||
auxEl.classList.add("editor-auxiliary-tool-active")
|
||||
}
|
||||
}
|
||||
|
||||
const marks = {
|
||||
bold: {
|
||||
selector: "strong",
|
||||
createFn: () => document.createElement("STRONG"),
|
||||
},
|
||||
italic: {
|
||||
selector: "em",
|
||||
createFn: () => document.createElement("EM"),
|
||||
},
|
||||
deleted: {
|
||||
selector: "del",
|
||||
createFn: () => document.createElement("DEL"),
|
||||
},
|
||||
underline: {
|
||||
selector: "u",
|
||||
createFn: () => document.createElement("U"),
|
||||
},
|
||||
mark: {
|
||||
selector: "mark",
|
||||
createFn: () => document.createElement("MARK"),
|
||||
},
|
||||
}
|
||||
|
||||
const tagNameSetFn = tagName => el => {
|
||||
const newEl = document.createElement(tagName)
|
||||
moveChildren(el, newEl, null)
|
||||
el.parentNode.insertBefore(newEl, el)
|
||||
el.parentNode.removeChild(el)
|
||||
window.getSelection().collapse(newEl, 0)
|
||||
}
|
||||
|
||||
const blocks = {
|
||||
p: {
|
||||
noButton: true,
|
||||
selector: "P",
|
||||
setFn: tagNameSetFn("P"),
|
||||
},
|
||||
h1: {
|
||||
selector: "H1",
|
||||
setFn: tagNameSetFn("H1"),
|
||||
},
|
||||
h2: {
|
||||
selector: "H2",
|
||||
setFn: tagNameSetFn("H2"),
|
||||
},
|
||||
h3: {
|
||||
selector: "H3",
|
||||
setFn: tagNameSetFn("H3"),
|
||||
},
|
||||
h4: {
|
||||
selector: "H4",
|
||||
setFn: tagNameSetFn("H4"),
|
||||
},
|
||||
h5: {
|
||||
selector: "H5",
|
||||
setFn: tagNameSetFn("H5"),
|
||||
},
|
||||
h6: {
|
||||
selector: "H6",
|
||||
setFn: tagNameSetFn("H6"),
|
||||
},
|
||||
ul: {
|
||||
selector: "UL",
|
||||
setFn: tagNameSetFn("UL"),
|
||||
},
|
||||
ol: {
|
||||
selector: "OL",
|
||||
setFn: tagNameSetFn("OL"),
|
||||
},
|
||||
img: {
|
||||
selector: "IMG",
|
||||
createFn: editorEl => {
|
||||
const el = document.createElement("IMG")
|
||||
el.src = "/public/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 = "/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
|
||||
},
|
||||
},
|
||||
// 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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const divWithStyleCreateFn = styleFn => () => {
|
||||
const el = document.createElement("DIV")
|
||||
styleFn(el)
|
||||
return el
|
||||
}
|
||||
|
||||
const parentBlocks = {
|
||||
left: {
|
||||
selector: "div[data-align=left]",
|
||||
createFn: divWithStyleCreateFn(el => el.dataset.align = "left"),
|
||||
},
|
||||
center: {
|
||||
selector: "div[data-align=center]",
|
||||
createFn: divWithStyleCreateFn(el => el.dataset.align = "center"),
|
||||
},
|
||||
right: {
|
||||
selector: "div[data-align=right]",
|
||||
createFn: divWithStyleCreateFn(el => el.dataset.align = "right"),
|
||||
},
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/3627747
|
||||
// TODO: cambiar por una solución más copada
|
||||
function rgb2hex(rgb) {
|
||||
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||||
function hex(x) {
|
||||
return ("0" + parseInt(x).toString(16)).slice(-2);
|
||||
}
|
||||
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
|
||||
}
|
||||
|
||||
const getSelected = contentEl => contentEl.querySelector(".selected")
|
||||
|
||||
const typesWithProperties = {
|
||||
mark: {
|
||||
selector: marks.mark.selector,
|
||||
updateInput (el, editorEl) {
|
||||
setAuxiliaryToolbar(editorEl, "mark")
|
||||
|
||||
const markColorInputEl = editorEl.querySelector(`*[data-prop="mark-color"]`)
|
||||
markColorInputEl.disabled = false
|
||||
markColorInputEl.value = el.style.backgroundColor ? rgb2hex(el.style.backgroundColor) : "#f206f9"
|
||||
},
|
||||
disableInput (editorEl) {
|
||||
const markColorInputEl = editorEl.querySelector(`*[data-prop="mark-color"]`)
|
||||
markColorInputEl.disabled = true
|
||||
markColorInputEl.value = "#000000"
|
||||
},
|
||||
setupInput (editorEl, contentEl) {
|
||||
const markColorInputEl = editorEl.querySelector(`*[data-prop="mark-color"]`)
|
||||
markColorInputEl.addEventListener("change", event => {
|
||||
const markEl = contentEl.querySelector(marks.mark.selector + ".selected")
|
||||
if (markEl) markEl.style.backgroundColor = markColorInputEl.value
|
||||
}, 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.src = URL.createObjectURL(file)
|
||||
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.src = URL.createObjectURL(file)
|
||||
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.src = URL.createObjectURL(file)
|
||||
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.src = URL.createObjectURL(file)
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
488
app/assets/javascripts/02-editor.js
Normal file
488
app/assets/javascripts/02-editor.js
Normal file
|
@ -0,0 +1,488 @@
|
|||
const origin = location.origin
|
||||
|
||||
function 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function moveChildren (from, to, toRef) {
|
||||
while (from.firstChild) to.insertBefore(from.firstChild, toRef);
|
||||
}
|
||||
|
||||
function isDirectChild (node, supposedChild) {
|
||||
for (const child of node.childNodes) {
|
||||
if (child == supposedChild) return true
|
||||
}
|
||||
}
|
||||
|
||||
function isChildSelection (sel, el) {
|
||||
return (
|
||||
(el.contains(sel.anchorNode) || el.contains(sel.focusNode))
|
||||
&& !(sel.anchorNode == el || sel.focusNode == el)
|
||||
)
|
||||
}
|
||||
|
||||
function getElementParent (node) {
|
||||
let parentEl = node
|
||||
while (parentEl.nodeType != Node.ELEMENT_NODE) parentEl = parentEl.parentElement
|
||||
return parentEl
|
||||
}
|
||||
|
||||
function splitNode (node, range) {
|
||||
const [left, right] = [
|
||||
{ range: document.createRange(), node: node.cloneNode(false) },
|
||||
{ range: document.createRange(), node: node.cloneNode(false) },
|
||||
]
|
||||
|
||||
left.range.setStartBefore(node.firstChild)
|
||||
left.range.setEnd(range.startContainer, range.startOffset)
|
||||
left.range.surroundContents(left.node)
|
||||
|
||||
right.range.setStart(range.endContainer, range.endOffset)
|
||||
right.range.setEndAfter(node.lastChild)
|
||||
right.range.surroundContents(right.node)
|
||||
|
||||
//left.node.appendChild(left.range.extractContents())
|
||||
//left.range.insertNode(left.node)
|
||||
|
||||
//right.node.appendChild(right.range.extractContents())
|
||||
//right.range.insertNode(right.node)
|
||||
|
||||
moveChildren(node, node.parentNode, node)
|
||||
node.parentNode.removeChild(node)
|
||||
|
||||
return [left, right]
|
||||
}
|
||||
|
||||
/* Configura un botón que hace una acción inline (ej: negrita).
|
||||
* Parametros:
|
||||
* * button: el botón
|
||||
* * mark: un objeto que representa el tipo de acción (ver types.js)
|
||||
* * contentEl: el elemento de contenido del editor.
|
||||
*/
|
||||
function setupMarkButton (button, mark, contentEl) {
|
||||
button.addEventListener("click", event => {
|
||||
event.preventDefault()
|
||||
|
||||
const sel = window.getSelection()
|
||||
if (!isChildSelection(sel, contentEl)) return
|
||||
|
||||
let parentEl = getElementParent(sel.anchorNode)
|
||||
//if (sel.anchorNode == parentEl) parentEl = parentEl.firstChild
|
||||
|
||||
const range = sel.getRangeAt(0)
|
||||
|
||||
if (parentEl.matches(mark.selector)) {
|
||||
const [left, right] = splitNode(parentEl, range)
|
||||
right.range.insertNode(range.extractContents())
|
||||
|
||||
const selectionRange = document.createRange()
|
||||
selectionRange.setStartAfter(left.node)
|
||||
selectionRange.setEndBefore(right.node)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(selectionRange)
|
||||
} else {
|
||||
for (const child of parentEl.childNodes) {
|
||||
if (
|
||||
(child instanceof Element)
|
||||
&& child.matches(mark.selector)
|
||||
&& sel.containsNode(child)
|
||||
) {
|
||||
moveChildren(child, parentEl, child)
|
||||
parentEl.removeChild(child)
|
||||
// TODO: agregar a selección
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const tagEl = mark.createFn()
|
||||
|
||||
try {
|
||||
range.surroundContents(tagEl)
|
||||
} catch (error) {
|
||||
// TODO: mostrar error
|
||||
return console.error("No puedo marcar cosas a través de distintos bloques!")
|
||||
}
|
||||
|
||||
for (const child of tagEl.childNodes) {
|
||||
if (child instanceof Element && child.matches(mark.selector)) {
|
||||
moveChildren(child, tagEl, child)
|
||||
tagEl.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
range.insertNode(tagEl)
|
||||
range.selectNode(tagEl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Igual que `setupMarkButton` pero para bloques. */
|
||||
function setupBlockButton (button, block, contentEl, editorEl) {
|
||||
button.addEventListener("click", event => {
|
||||
event.preventDefault()
|
||||
|
||||
const sel = window.getSelection()
|
||||
// TODO: mostrar error
|
||||
if (
|
||||
!contentEl.contains(sel.anchorNode)
|
||||
|| !contentEl.contains(sel.focusNode)
|
||||
|| sel.anchorNode == contentEl
|
||||
|| sel.focusNode == contentEl
|
||||
) return
|
||||
|
||||
const range = sel.getRangeAt(0)
|
||||
|
||||
let parentEl = sel.anchorNode
|
||||
while (!isDirectChild(contentEl, parentEl)) parentEl = parentEl.parentElement
|
||||
|
||||
if (block.setFn) {
|
||||
if (parentEl.matches(block.selector)) {
|
||||
tagNameSetFn("P")(parentEl)
|
||||
} else {
|
||||
block.setFn(parentEl)
|
||||
}
|
||||
} else if (block.createFn) {
|
||||
const newEl = block.createFn(editorEl)
|
||||
parentEl.parentElement.insertBefore(newEl, parentEl.nextSibling)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* Igual que `setupBlockButton` pero para bloques parientes. */
|
||||
function setupParentBlockButton (button, parentBlock, contentEl) {
|
||||
button.addEventListener("click", event => {
|
||||
event.preventDefault()
|
||||
|
||||
const sel = window.getSelection()
|
||||
if (
|
||||
!contentEl.contains(sel.anchorNode)
|
||||
|| !contentEl.contains(sel.focusNode)
|
||||
|| sel.anchorNode == contentEl
|
||||
|| sel.focusNode == contentEl
|
||||
) return
|
||||
|
||||
const range = sel.getRangeAt(0)
|
||||
|
||||
let parentEl = sel.anchorNode
|
||||
while (!isDirectChild(contentEl, parentEl)) parentEl = parentEl.parentElement
|
||||
|
||||
if (parentEl.matches(parentBlock.selector)) {
|
||||
moveChildren(parentEl, parentEl.parentElement, parentEl)
|
||||
parentEl.parentElement.removeChild(parentEl)
|
||||
} else if (elementIsParentBlock(parentEl)) {
|
||||
const el = parentBlock.createFn()
|
||||
moveChildren(parentEl, el, null)
|
||||
parentEl.parentElement.insertBefore(el, parentEl)
|
||||
parentEl.parentElement.removeChild(parentEl)
|
||||
} else {
|
||||
const el = parentBlock.createFn()
|
||||
parentEl.parentElement.insertBefore(el, parentEl)
|
||||
el.appendChild(parentEl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const elementIsTypes = types => element => {
|
||||
for (const type of Object.values(types)) {
|
||||
if (element.matches(type.selector)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const elementIsBlock = elementIsTypes(blocks)
|
||||
const elementIsParentBlock = elementIsTypes(parentBlocks)
|
||||
|
||||
function hasContent (element) {
|
||||
if (element.firstElementChild) return true
|
||||
for (const child of element.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE && child.data.length > 0) return true
|
||||
else if (child.hasChildNodes() && hasContent(child)) return true
|
||||
}
|
||||
// TODO: verificar que los elementos tiene contenido
|
||||
if (element.tagName === "IMG"
|
||||
|| element.tagName === "AUDIO"
|
||||
|| element.tagName === "VIDEO"
|
||||
|| element.tagName === "IFRAME") return true
|
||||
return false
|
||||
}
|
||||
|
||||
/* Limpia el elemento de contenido del editor
|
||||
* Por ahora:
|
||||
* * Cambia el tag de los bloques no reconocidos (ver `elementIsBlock`)
|
||||
* * Hace lo que hace cleanNode
|
||||
*/
|
||||
function cleanContent (contentEl) {
|
||||
const sel = window.getSelection()
|
||||
|
||||
cleanNode(contentEl, contentEl)
|
||||
|
||||
for (const child of contentEl.childNodes) {
|
||||
if (child.tagName) {
|
||||
if (elementIsParentBlock(child)) {
|
||||
cleanContent(child)
|
||||
} else if (!elementIsBlock(child)) {
|
||||
child.tagName = "P"
|
||||
}
|
||||
} else if (child.nodeType === Node.TEXT_NODE) {
|
||||
const wasSelected = sel.getRangeAt(0).intersectsNode(child)
|
||||
|
||||
const el = document.createElement("p")
|
||||
el.appendChild(child)
|
||||
contentEl.insertBefore(el, child.nextSibling)
|
||||
|
||||
if (wasSelected) sel.collapse(el, child.data.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Arregla cosas en el elemento de contendo del editor
|
||||
* Por ahora:
|
||||
* * Crea un p y inserta la selección si no hay elementos
|
||||
* * Wrappea el contenido de un UL o OL en un LI si no lo está
|
||||
*/
|
||||
function fixContent (contentEl) {
|
||||
for (const child of contentEl.childNodes) {
|
||||
if (child.tagName) {
|
||||
if (elementIsParentBlock(child)) {
|
||||
fixContent(child)
|
||||
} else if (child.tagName === "UL" || child.tagName === "OL") {
|
||||
let notItems = []
|
||||
for (const item of child.childNodes) {
|
||||
if (item.tagName !== "LI") notItems.push(item)
|
||||
}
|
||||
|
||||
if (notItems.length) {
|
||||
const item = document.createElement("li")
|
||||
item.append(...notItems)
|
||||
child.appendChild(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Recursivamente "limpia" los nodos a partir del llamado.
|
||||
* Por ahora:
|
||||
* * Junta nodos de texto que están al lado
|
||||
* * Junta nodos de la misma "mark" que están al lado
|
||||
* * Borra elementos sin contenido (ver `hasContent`) y no están seleccionados
|
||||
* * Borra inline styles no autorizados
|
||||
* * Borra propiedades de IMG no autorizadas
|
||||
* * Borra <FONT> y <STYLE>
|
||||
*/
|
||||
function cleanNode (node, contentEl) {
|
||||
for (const child of node.childNodes) {
|
||||
if (child.nodeType === Node.TEXT_NODE) {
|
||||
if (child.nextSibling && child.nextSibling.nodeType === Node.TEXT_NODE) {
|
||||
// Juntar nodos
|
||||
child.data += child.nextSibling.data
|
||||
child.parentNode.removeChild(child.nextSibling)
|
||||
}
|
||||
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||
if (!hasContent(child) && !window.getSelection().getRangeAt(0).intersectsNode(child))
|
||||
child.parentNode.removeChild(child)
|
||||
|
||||
for (const mark of Object.values(marks)) {
|
||||
if (
|
||||
child.matches(mark.selector)
|
||||
&& child.nextSibling
|
||||
&& (child.nextSibling instanceof Element)
|
||||
&& child.nextSibling.matches(mark.selector)
|
||||
) {
|
||||
moveChildren(child.nextSibling, child, null)
|
||||
child.nextSibling.parentNode.removeChild(child.nextSibling)
|
||||
}
|
||||
}
|
||||
|
||||
if (child.tagName === "LI") {
|
||||
let parentEl = child
|
||||
while (
|
||||
parentEl
|
||||
&& !(parentEl.nodeType == Node.ELEMENT_NODE && elementIsBlock(parentEl))
|
||||
&& contentEl.contains(parentEl)
|
||||
)
|
||||
parentEl = parentEl.parentElement
|
||||
|
||||
if (
|
||||
parentEl
|
||||
&& contentEl.contains(parentEl)
|
||||
&& parentEl.tagName !== "UL"
|
||||
&& parentEl.tagName !== "OL"
|
||||
)
|
||||
moveChildren(child, parentEl, child.nextSibling)
|
||||
} else if (child.tagName === "IMG") {
|
||||
if (child.getAttribute("width")) child.removeAttribute("width")
|
||||
if (child.getAttribute("height")) child.removeAttribute("height")
|
||||
if (child.getAttribute("vspace")) child.removeAttribute("vspace")
|
||||
if (child.getAttribute("hspace")) child.removeAttribute("hspace")
|
||||
if (child.align.length) child.removeAttribute("align")
|
||||
if (child.name.length) child.removeAttribute("name")
|
||||
if (!child.src.length) child.src = "/public/placeholder.png"
|
||||
} else if (child.tagName === "FONT") {
|
||||
moveChildren(child, child.parentElement, child.nextSiling)
|
||||
child.parentElement.removeChild(child)
|
||||
return
|
||||
} else if (child.tagName === "STYLE") {
|
||||
child.parentElement.removeChild(child)
|
||||
return
|
||||
}
|
||||
|
||||
for (const style of Object.values(child.style)) {
|
||||
const value = child.style[style]
|
||||
|
||||
switch (style) {
|
||||
case 'background-color':
|
||||
if (child.tagName === "MARK") break
|
||||
default:
|
||||
child.style[style] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanNode(child, contentEl)
|
||||
}
|
||||
}
|
||||
|
||||
/* Generar el clickListener para este editor.
|
||||
*/
|
||||
function generateClickListener (editorEl, contentEl) {
|
||||
/* El event listener para los typesWithProperties.
|
||||
*/
|
||||
return function clickListener (event) {
|
||||
// Borrar todas las selecciones
|
||||
for (const el of contentEl.querySelectorAll(".selected")) {
|
||||
el.classList.remove("selected")
|
||||
}
|
||||
|
||||
setAuxiliaryToolbar(editorEl)
|
||||
|
||||
let selectedType
|
||||
let selectedEl
|
||||
|
||||
for (const [name, type] of Object.entries(typesWithProperties)) {
|
||||
type.disableInput(editorEl)
|
||||
|
||||
let el = event.target
|
||||
while (el && !el.matches(type.selector)) el = el.parentElement
|
||||
if (el && contentEl.contains(el)) {
|
||||
selectedType = type
|
||||
selectedEl = el
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedType) {
|
||||
event.preventDefault()
|
||||
selectedType.updateInput(selectedEl, editorEl)
|
||||
event.target.classList.add("selected")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupEditor (editorEl) {
|
||||
// XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor?
|
||||
document.execCommand('defaultParagraphSeparator', false, 'p')
|
||||
|
||||
const contentEl = editorEl.querySelector(".editor-content")
|
||||
|
||||
contentEl.addEventListener("keydown", event => {
|
||||
if (event.keyCode === 32) { // Espacio
|
||||
event.preventDefault()
|
||||
const sel = window.getSelection()
|
||||
const range = sel.getRangeAt(0)
|
||||
range.insertNode(document.createTextNode("\xa0"))
|
||||
cleanContent(contentEl)
|
||||
range.collapse()
|
||||
}
|
||||
})
|
||||
document.addEventListener("selectionchange", event => {
|
||||
cleanContent(contentEl)
|
||||
})
|
||||
|
||||
const clickListener = generateClickListener(editorEl, contentEl)
|
||||
contentEl.addEventListener("click", clickListener, true)
|
||||
|
||||
const htmlEl = editorEl.querySelector("textarea")
|
||||
const observer = new MutationObserver((mutationList, observer) => {
|
||||
cleanContent(contentEl)
|
||||
htmlEl.value = contentEl.innerHTML
|
||||
fixContent(contentEl)
|
||||
})
|
||||
observer.observe(contentEl, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
})
|
||||
|
||||
const editorBtn = id => editorEl.querySelector(`*[data-button="${id}"]`)
|
||||
|
||||
const tagNameSetFn = tagName => el => {
|
||||
const newEl = document.createElement(tagName)
|
||||
moveChildren(el, newEl, null)
|
||||
el.parentNode.insertBefore(newEl, el)
|
||||
el.parentNode.removeChild(el)
|
||||
window.getSelection().collapse(newEl, 0)
|
||||
}
|
||||
|
||||
// == SETUP BUTTONS ==
|
||||
for (const [name, mark] of Object.entries(marks)) {
|
||||
setupMarkButton(editorBtn(name), mark, contentEl)
|
||||
}
|
||||
|
||||
for (const [name, block] of Object.entries(blocks)) {
|
||||
if (block.noButton) continue
|
||||
setupBlockButton(editorBtn(name), block, contentEl, editorEl)
|
||||
}
|
||||
|
||||
for (const [name, parentBlock] of Object.entries(parentBlocks)) {
|
||||
if (parentBlock.noButton) continue
|
||||
setupParentBlockButton(editorBtn(name), parentBlock, contentEl)
|
||||
}
|
||||
|
||||
for (const [name, type] of Object.entries(typesWithProperties)) {
|
||||
type.setupInput(editorEl, contentEl)
|
||||
}
|
||||
|
||||
document.addEventListener(editorBtn("mark"), () => setAuxiliaryToolbar(editorEl, "mark"))
|
||||
document.addEventListener(editorBtn("img"), () => setAuxiliaryToolbar(editorEl, "img"))
|
||||
document.addEventListener(editorBtn("audio"), () => setAuxiliaryToolbar(editorEl, "audio"))
|
||||
document.addEventListener(editorBtn("video"), () => setAuxiliaryToolbar(editorEl, "video"))
|
||||
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)
|
||||
htmlEl.value = contentEl.innerHTML
|
||||
fixContent(contentEl)
|
||||
}
|
||||
|
||||
// TODO: por ahora confiamos, quizás queremos filtrar estilos?
|
||||
function stringifyAllowedStyle (element) {
|
||||
return element.style.cssText
|
||||
}
|
||||
|
||||
document.addEventListener("turbolinks:load", () => {
|
||||
for (const editorEl of document.querySelectorAll(".editor")) {
|
||||
if (!editorEl.querySelector('.editor-toolbar')) continue
|
||||
|
||||
setupEditor(editorEl)
|
||||
}
|
||||
})
|
||||
|
942
app/assets/javascripts/activestorage.js
Normal file
942
app/assets/javascripts/activestorage.js
Normal file
|
@ -0,0 +1,942 @@
|
|||
(function(global, factory) {
|
||||
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActiveStorage = {});
|
||||
})(this, function(exports) {
|
||||
"use strict";
|
||||
function createCommonjsModule(fn, module) {
|
||||
return module = {
|
||||
exports: {}
|
||||
}, fn(module, module.exports), module.exports;
|
||||
}
|
||||
var sparkMd5 = createCommonjsModule(function(module, exports) {
|
||||
(function(factory) {
|
||||
{
|
||||
module.exports = factory();
|
||||
}
|
||||
})(function(undefined) {
|
||||
var hex_chr = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" ];
|
||||
function md5cycle(x, k) {
|
||||
var a = x[0], b = x[1], c = x[2], d = x[3];
|
||||
a += (b & c | ~b & d) + k[0] - 680876936 | 0;
|
||||
a = (a << 7 | a >>> 25) + b | 0;
|
||||
d += (a & b | ~a & c) + k[1] - 389564586 | 0;
|
||||
d = (d << 12 | d >>> 20) + a | 0;
|
||||
c += (d & a | ~d & b) + k[2] + 606105819 | 0;
|
||||
c = (c << 17 | c >>> 15) + d | 0;
|
||||
b += (c & d | ~c & a) + k[3] - 1044525330 | 0;
|
||||
b = (b << 22 | b >>> 10) + c | 0;
|
||||
a += (b & c | ~b & d) + k[4] - 176418897 | 0;
|
||||
a = (a << 7 | a >>> 25) + b | 0;
|
||||
d += (a & b | ~a & c) + k[5] + 1200080426 | 0;
|
||||
d = (d << 12 | d >>> 20) + a | 0;
|
||||
c += (d & a | ~d & b) + k[6] - 1473231341 | 0;
|
||||
c = (c << 17 | c >>> 15) + d | 0;
|
||||
b += (c & d | ~c & a) + k[7] - 45705983 | 0;
|
||||
b = (b << 22 | b >>> 10) + c | 0;
|
||||
a += (b & c | ~b & d) + k[8] + 1770035416 | 0;
|
||||
a = (a << 7 | a >>> 25) + b | 0;
|
||||
d += (a & b | ~a & c) + k[9] - 1958414417 | 0;
|
||||
d = (d << 12 | d >>> 20) + a | 0;
|
||||
c += (d & a | ~d & b) + k[10] - 42063 | 0;
|
||||
c = (c << 17 | c >>> 15) + d | 0;
|
||||
b += (c & d | ~c & a) + k[11] - 1990404162 | 0;
|
||||
b = (b << 22 | b >>> 10) + c | 0;
|
||||
a += (b & c | ~b & d) + k[12] + 1804603682 | 0;
|
||||
a = (a << 7 | a >>> 25) + b | 0;
|
||||
d += (a & b | ~a & c) + k[13] - 40341101 | 0;
|
||||
d = (d << 12 | d >>> 20) + a | 0;
|
||||
c += (d & a | ~d & b) + k[14] - 1502002290 | 0;
|
||||
c = (c << 17 | c >>> 15) + d | 0;
|
||||
b += (c & d | ~c & a) + k[15] + 1236535329 | 0;
|
||||
b = (b << 22 | b >>> 10) + c | 0;
|
||||
a += (b & d | c & ~d) + k[1] - 165796510 | 0;
|
||||
a = (a << 5 | a >>> 27) + b | 0;
|
||||
d += (a & c | b & ~c) + k[6] - 1069501632 | 0;
|
||||
d = (d << 9 | d >>> 23) + a | 0;
|
||||
c += (d & b | a & ~b) + k[11] + 643717713 | 0;
|
||||
c = (c << 14 | c >>> 18) + d | 0;
|
||||
b += (c & a | d & ~a) + k[0] - 373897302 | 0;
|
||||
b = (b << 20 | b >>> 12) + c | 0;
|
||||
a += (b & d | c & ~d) + k[5] - 701558691 | 0;
|
||||
a = (a << 5 | a >>> 27) + b | 0;
|
||||
d += (a & c | b & ~c) + k[10] + 38016083 | 0;
|
||||
d = (d << 9 | d >>> 23) + a | 0;
|
||||
c += (d & b | a & ~b) + k[15] - 660478335 | 0;
|
||||
c = (c << 14 | c >>> 18) + d | 0;
|
||||
b += (c & a | d & ~a) + k[4] - 405537848 | 0;
|
||||
b = (b << 20 | b >>> 12) + c | 0;
|
||||
a += (b & d | c & ~d) + k[9] + 568446438 | 0;
|
||||
a = (a << 5 | a >>> 27) + b | 0;
|
||||
d += (a & c | b & ~c) + k[14] - 1019803690 | 0;
|
||||
d = (d << 9 | d >>> 23) + a | 0;
|
||||
c += (d & b | a & ~b) + k[3] - 187363961 | 0;
|
||||
c = (c << 14 | c >>> 18) + d | 0;
|
||||
b += (c & a | d & ~a) + k[8] + 1163531501 | 0;
|
||||
b = (b << 20 | b >>> 12) + c | 0;
|
||||
a += (b & d | c & ~d) + k[13] - 1444681467 | 0;
|
||||
a = (a << 5 | a >>> 27) + b | 0;
|
||||
d += (a & c | b & ~c) + k[2] - 51403784 | 0;
|
||||
d = (d << 9 | d >>> 23) + a | 0;
|
||||
c += (d & b | a & ~b) + k[7] + 1735328473 | 0;
|
||||
c = (c << 14 | c >>> 18) + d | 0;
|
||||
b += (c & a | d & ~a) + k[12] - 1926607734 | 0;
|
||||
b = (b << 20 | b >>> 12) + c | 0;
|
||||
a += (b ^ c ^ d) + k[5] - 378558 | 0;
|
||||
a = (a << 4 | a >>> 28) + b | 0;
|
||||
d += (a ^ b ^ c) + k[8] - 2022574463 | 0;
|
||||
d = (d << 11 | d >>> 21) + a | 0;
|
||||
c += (d ^ a ^ b) + k[11] + 1839030562 | 0;
|
||||
c = (c << 16 | c >>> 16) + d | 0;
|
||||
b += (c ^ d ^ a) + k[14] - 35309556 | 0;
|
||||
b = (b << 23 | b >>> 9) + c | 0;
|
||||
a += (b ^ c ^ d) + k[1] - 1530992060 | 0;
|
||||
a = (a << 4 | a >>> 28) + b | 0;
|
||||
d += (a ^ b ^ c) + k[4] + 1272893353 | 0;
|
||||
d = (d << 11 | d >>> 21) + a | 0;
|
||||
c += (d ^ a ^ b) + k[7] - 155497632 | 0;
|
||||
c = (c << 16 | c >>> 16) + d | 0;
|
||||
b += (c ^ d ^ a) + k[10] - 1094730640 | 0;
|
||||
b = (b << 23 | b >>> 9) + c | 0;
|
||||
a += (b ^ c ^ d) + k[13] + 681279174 | 0;
|
||||
a = (a << 4 | a >>> 28) + b | 0;
|
||||
d += (a ^ b ^ c) + k[0] - 358537222 | 0;
|
||||
d = (d << 11 | d >>> 21) + a | 0;
|
||||
c += (d ^ a ^ b) + k[3] - 722521979 | 0;
|
||||
c = (c << 16 | c >>> 16) + d | 0;
|
||||
b += (c ^ d ^ a) + k[6] + 76029189 | 0;
|
||||
b = (b << 23 | b >>> 9) + c | 0;
|
||||
a += (b ^ c ^ d) + k[9] - 640364487 | 0;
|
||||
a = (a << 4 | a >>> 28) + b | 0;
|
||||
d += (a ^ b ^ c) + k[12] - 421815835 | 0;
|
||||
d = (d << 11 | d >>> 21) + a | 0;
|
||||
c += (d ^ a ^ b) + k[15] + 530742520 | 0;
|
||||
c = (c << 16 | c >>> 16) + d | 0;
|
||||
b += (c ^ d ^ a) + k[2] - 995338651 | 0;
|
||||
b = (b << 23 | b >>> 9) + c | 0;
|
||||
a += (c ^ (b | ~d)) + k[0] - 198630844 | 0;
|
||||
a = (a << 6 | a >>> 26) + b | 0;
|
||||
d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0;
|
||||
d = (d << 10 | d >>> 22) + a | 0;
|
||||
c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0;
|
||||
c = (c << 15 | c >>> 17) + d | 0;
|
||||
b += (d ^ (c | ~a)) + k[5] - 57434055 | 0;
|
||||
b = (b << 21 | b >>> 11) + c | 0;
|
||||
a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0;
|
||||
a = (a << 6 | a >>> 26) + b | 0;
|
||||
d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0;
|
||||
d = (d << 10 | d >>> 22) + a | 0;
|
||||
c += (a ^ (d | ~b)) + k[10] - 1051523 | 0;
|
||||
c = (c << 15 | c >>> 17) + d | 0;
|
||||
b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0;
|
||||
b = (b << 21 | b >>> 11) + c | 0;
|
||||
a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0;
|
||||
a = (a << 6 | a >>> 26) + b | 0;
|
||||
d += (b ^ (a | ~c)) + k[15] - 30611744 | 0;
|
||||
d = (d << 10 | d >>> 22) + a | 0;
|
||||
c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0;
|
||||
c = (c << 15 | c >>> 17) + d | 0;
|
||||
b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0;
|
||||
b = (b << 21 | b >>> 11) + c | 0;
|
||||
a += (c ^ (b | ~d)) + k[4] - 145523070 | 0;
|
||||
a = (a << 6 | a >>> 26) + b | 0;
|
||||
d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0;
|
||||
d = (d << 10 | d >>> 22) + a | 0;
|
||||
c += (a ^ (d | ~b)) + k[2] + 718787259 | 0;
|
||||
c = (c << 15 | c >>> 17) + d | 0;
|
||||
b += (d ^ (c | ~a)) + k[9] - 343485551 | 0;
|
||||
b = (b << 21 | b >>> 11) + c | 0;
|
||||
x[0] = a + x[0] | 0;
|
||||
x[1] = b + x[1] | 0;
|
||||
x[2] = c + x[2] | 0;
|
||||
x[3] = d + x[3] | 0;
|
||||
}
|
||||
function md5blk(s) {
|
||||
var md5blks = [], i;
|
||||
for (i = 0; i < 64; i += 4) {
|
||||
md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
|
||||
}
|
||||
return md5blks;
|
||||
}
|
||||
function md5blk_array(a) {
|
||||
var md5blks = [], i;
|
||||
for (i = 0; i < 64; i += 4) {
|
||||
md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24);
|
||||
}
|
||||
return md5blks;
|
||||
}
|
||||
function md51(s) {
|
||||
var n = s.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
|
||||
for (i = 64; i <= n; i += 64) {
|
||||
md5cycle(state, md5blk(s.substring(i - 64, i)));
|
||||
}
|
||||
s = s.substring(i - 64);
|
||||
length = s.length;
|
||||
tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
|
||||
for (i = 0; i < length; i += 1) {
|
||||
tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
|
||||
}
|
||||
tail[i >> 2] |= 128 << (i % 4 << 3);
|
||||
if (i > 55) {
|
||||
md5cycle(state, tail);
|
||||
for (i = 0; i < 16; i += 1) {
|
||||
tail[i] = 0;
|
||||
}
|
||||
}
|
||||
tmp = n * 8;
|
||||
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
||||
lo = parseInt(tmp[2], 16);
|
||||
hi = parseInt(tmp[1], 16) || 0;
|
||||
tail[14] = lo;
|
||||
tail[15] = hi;
|
||||
md5cycle(state, tail);
|
||||
return state;
|
||||
}
|
||||
function md51_array(a) {
|
||||
var n = a.length, state = [ 1732584193, -271733879, -1732584194, 271733878 ], i, length, tail, tmp, lo, hi;
|
||||
for (i = 64; i <= n; i += 64) {
|
||||
md5cycle(state, md5blk_array(a.subarray(i - 64, i)));
|
||||
}
|
||||
a = i - 64 < n ? a.subarray(i - 64) : new Uint8Array(0);
|
||||
length = a.length;
|
||||
tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
|
||||
for (i = 0; i < length; i += 1) {
|
||||
tail[i >> 2] |= a[i] << (i % 4 << 3);
|
||||
}
|
||||
tail[i >> 2] |= 128 << (i % 4 << 3);
|
||||
if (i > 55) {
|
||||
md5cycle(state, tail);
|
||||
for (i = 0; i < 16; i += 1) {
|
||||
tail[i] = 0;
|
||||
}
|
||||
}
|
||||
tmp = n * 8;
|
||||
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
||||
lo = parseInt(tmp[2], 16);
|
||||
hi = parseInt(tmp[1], 16) || 0;
|
||||
tail[14] = lo;
|
||||
tail[15] = hi;
|
||||
md5cycle(state, tail);
|
||||
return state;
|
||||
}
|
||||
function rhex(n) {
|
||||
var s = "", j;
|
||||
for (j = 0; j < 4; j += 1) {
|
||||
s += hex_chr[n >> j * 8 + 4 & 15] + hex_chr[n >> j * 8 & 15];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function hex(x) {
|
||||
var i;
|
||||
for (i = 0; i < x.length; i += 1) {
|
||||
x[i] = rhex(x[i]);
|
||||
}
|
||||
return x.join("");
|
||||
}
|
||||
if (hex(md51("hello")) !== "5d41402abc4b2a76b9719d911017c592") ;
|
||||
if (typeof ArrayBuffer !== "undefined" && !ArrayBuffer.prototype.slice) {
|
||||
(function() {
|
||||
function clamp(val, length) {
|
||||
val = val | 0 || 0;
|
||||
if (val < 0) {
|
||||
return Math.max(val + length, 0);
|
||||
}
|
||||
return Math.min(val, length);
|
||||
}
|
||||
ArrayBuffer.prototype.slice = function(from, to) {
|
||||
var length = this.byteLength, begin = clamp(from, length), end = length, num, target, targetArray, sourceArray;
|
||||
if (to !== undefined) {
|
||||
end = clamp(to, length);
|
||||
}
|
||||
if (begin > end) {
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
num = end - begin;
|
||||
target = new ArrayBuffer(num);
|
||||
targetArray = new Uint8Array(target);
|
||||
sourceArray = new Uint8Array(this, begin, num);
|
||||
targetArray.set(sourceArray);
|
||||
return target;
|
||||
};
|
||||
})();
|
||||
}
|
||||
function toUtf8(str) {
|
||||
if (/[\u0080-\uFFFF]/.test(str)) {
|
||||
str = unescape(encodeURIComponent(str));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function utf8Str2ArrayBuffer(str, returnUInt8Array) {
|
||||
var length = str.length, buff = new ArrayBuffer(length), arr = new Uint8Array(buff), i;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
arr[i] = str.charCodeAt(i);
|
||||
}
|
||||
return returnUInt8Array ? arr : buff;
|
||||
}
|
||||
function arrayBuffer2Utf8Str(buff) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buff));
|
||||
}
|
||||
function concatenateArrayBuffers(first, second, returnUInt8Array) {
|
||||
var result = new Uint8Array(first.byteLength + second.byteLength);
|
||||
result.set(new Uint8Array(first));
|
||||
result.set(new Uint8Array(second), first.byteLength);
|
||||
return returnUInt8Array ? result : result.buffer;
|
||||
}
|
||||
function hexToBinaryString(hex) {
|
||||
var bytes = [], length = hex.length, x;
|
||||
for (x = 0; x < length - 1; x += 2) {
|
||||
bytes.push(parseInt(hex.substr(x, 2), 16));
|
||||
}
|
||||
return String.fromCharCode.apply(String, bytes);
|
||||
}
|
||||
function SparkMD5() {
|
||||
this.reset();
|
||||
}
|
||||
SparkMD5.prototype.append = function(str) {
|
||||
this.appendBinary(toUtf8(str));
|
||||
return this;
|
||||
};
|
||||
SparkMD5.prototype.appendBinary = function(contents) {
|
||||
this._buff += contents;
|
||||
this._length += contents.length;
|
||||
var length = this._buff.length, i;
|
||||
for (i = 64; i <= length; i += 64) {
|
||||
md5cycle(this._hash, md5blk(this._buff.substring(i - 64, i)));
|
||||
}
|
||||
this._buff = this._buff.substring(i - 64);
|
||||
return this;
|
||||
};
|
||||
SparkMD5.prototype.end = function(raw) {
|
||||
var buff = this._buff, length = buff.length, i, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], ret;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
tail[i >> 2] |= buff.charCodeAt(i) << (i % 4 << 3);
|
||||
}
|
||||
this._finish(tail, length);
|
||||
ret = hex(this._hash);
|
||||
if (raw) {
|
||||
ret = hexToBinaryString(ret);
|
||||
}
|
||||
this.reset();
|
||||
return ret;
|
||||
};
|
||||
SparkMD5.prototype.reset = function() {
|
||||
this._buff = "";
|
||||
this._length = 0;
|
||||
this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
|
||||
return this;
|
||||
};
|
||||
SparkMD5.prototype.getState = function() {
|
||||
return {
|
||||
buff: this._buff,
|
||||
length: this._length,
|
||||
hash: this._hash
|
||||
};
|
||||
};
|
||||
SparkMD5.prototype.setState = function(state) {
|
||||
this._buff = state.buff;
|
||||
this._length = state.length;
|
||||
this._hash = state.hash;
|
||||
return this;
|
||||
};
|
||||
SparkMD5.prototype.destroy = function() {
|
||||
delete this._hash;
|
||||
delete this._buff;
|
||||
delete this._length;
|
||||
};
|
||||
SparkMD5.prototype._finish = function(tail, length) {
|
||||
var i = length, tmp, lo, hi;
|
||||
tail[i >> 2] |= 128 << (i % 4 << 3);
|
||||
if (i > 55) {
|
||||
md5cycle(this._hash, tail);
|
||||
for (i = 0; i < 16; i += 1) {
|
||||
tail[i] = 0;
|
||||
}
|
||||
}
|
||||
tmp = this._length * 8;
|
||||
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
||||
lo = parseInt(tmp[2], 16);
|
||||
hi = parseInt(tmp[1], 16) || 0;
|
||||
tail[14] = lo;
|
||||
tail[15] = hi;
|
||||
md5cycle(this._hash, tail);
|
||||
};
|
||||
SparkMD5.hash = function(str, raw) {
|
||||
return SparkMD5.hashBinary(toUtf8(str), raw);
|
||||
};
|
||||
SparkMD5.hashBinary = function(content, raw) {
|
||||
var hash = md51(content), ret = hex(hash);
|
||||
return raw ? hexToBinaryString(ret) : ret;
|
||||
};
|
||||
SparkMD5.ArrayBuffer = function() {
|
||||
this.reset();
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.append = function(arr) {
|
||||
var buff = concatenateArrayBuffers(this._buff.buffer, arr, true), length = buff.length, i;
|
||||
this._length += arr.byteLength;
|
||||
for (i = 64; i <= length; i += 64) {
|
||||
md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i)));
|
||||
}
|
||||
this._buff = i - 64 < length ? new Uint8Array(buff.buffer.slice(i - 64)) : new Uint8Array(0);
|
||||
return this;
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.end = function(raw) {
|
||||
var buff = this._buff, length = buff.length, tail = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], i, ret;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
tail[i >> 2] |= buff[i] << (i % 4 << 3);
|
||||
}
|
||||
this._finish(tail, length);
|
||||
ret = hex(this._hash);
|
||||
if (raw) {
|
||||
ret = hexToBinaryString(ret);
|
||||
}
|
||||
this.reset();
|
||||
return ret;
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.reset = function() {
|
||||
this._buff = new Uint8Array(0);
|
||||
this._length = 0;
|
||||
this._hash = [ 1732584193, -271733879, -1732584194, 271733878 ];
|
||||
return this;
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.getState = function() {
|
||||
var state = SparkMD5.prototype.getState.call(this);
|
||||
state.buff = arrayBuffer2Utf8Str(state.buff);
|
||||
return state;
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.setState = function(state) {
|
||||
state.buff = utf8Str2ArrayBuffer(state.buff, true);
|
||||
return SparkMD5.prototype.setState.call(this, state);
|
||||
};
|
||||
SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy;
|
||||
SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish;
|
||||
SparkMD5.ArrayBuffer.hash = function(arr, raw) {
|
||||
var hash = md51_array(new Uint8Array(arr)), ret = hex(hash);
|
||||
return raw ? hexToBinaryString(ret) : ret;
|
||||
};
|
||||
return SparkMD5;
|
||||
});
|
||||
});
|
||||
var classCallCheck = function(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
};
|
||||
var createClass = function() {
|
||||
function defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
return function(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
};
|
||||
}();
|
||||
var fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
|
||||
var FileChecksum = function() {
|
||||
createClass(FileChecksum, null, [ {
|
||||
key: "create",
|
||||
value: function create(file, callback) {
|
||||
var instance = new FileChecksum(file);
|
||||
instance.create(callback);
|
||||
}
|
||||
} ]);
|
||||
function FileChecksum(file) {
|
||||
classCallCheck(this, FileChecksum);
|
||||
this.file = file;
|
||||
this.chunkSize = 2097152;
|
||||
this.chunkCount = Math.ceil(this.file.size / this.chunkSize);
|
||||
this.chunkIndex = 0;
|
||||
}
|
||||
createClass(FileChecksum, [ {
|
||||
key: "create",
|
||||
value: function create(callback) {
|
||||
var _this = this;
|
||||
this.callback = callback;
|
||||
this.md5Buffer = new sparkMd5.ArrayBuffer();
|
||||
this.fileReader = new FileReader();
|
||||
this.fileReader.addEventListener("load", function(event) {
|
||||
return _this.fileReaderDidLoad(event);
|
||||
});
|
||||
this.fileReader.addEventListener("error", function(event) {
|
||||
return _this.fileReaderDidError(event);
|
||||
});
|
||||
this.readNextChunk();
|
||||
}
|
||||
}, {
|
||||
key: "fileReaderDidLoad",
|
||||
value: function fileReaderDidLoad(event) {
|
||||
this.md5Buffer.append(event.target.result);
|
||||
if (!this.readNextChunk()) {
|
||||
var binaryDigest = this.md5Buffer.end(true);
|
||||
var base64digest = btoa(binaryDigest);
|
||||
this.callback(null, base64digest);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "fileReaderDidError",
|
||||
value: function fileReaderDidError(event) {
|
||||
this.callback("Error reading " + this.file.name);
|
||||
}
|
||||
}, {
|
||||
key: "readNextChunk",
|
||||
value: function readNextChunk() {
|
||||
if (this.chunkIndex < this.chunkCount || this.chunkIndex == 0 && this.chunkCount == 0) {
|
||||
var start = this.chunkIndex * this.chunkSize;
|
||||
var end = Math.min(start + this.chunkSize, this.file.size);
|
||||
var bytes = fileSlice.call(this.file, start, end);
|
||||
this.fileReader.readAsArrayBuffer(bytes);
|
||||
this.chunkIndex++;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} ]);
|
||||
return FileChecksum;
|
||||
}();
|
||||
function getMetaValue(name) {
|
||||
var element = findElement(document.head, 'meta[name="' + name + '"]');
|
||||
if (element) {
|
||||
return element.getAttribute("content");
|
||||
}
|
||||
}
|
||||
function findElements(root, selector) {
|
||||
if (typeof root == "string") {
|
||||
selector = root;
|
||||
root = document;
|
||||
}
|
||||
var elements = root.querySelectorAll(selector);
|
||||
return toArray$1(elements);
|
||||
}
|
||||
function findElement(root, selector) {
|
||||
if (typeof root == "string") {
|
||||
selector = root;
|
||||
root = document;
|
||||
}
|
||||
return root.querySelector(selector);
|
||||
}
|
||||
function dispatchEvent(element, type) {
|
||||
var eventInit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
||||
var disabled = element.disabled;
|
||||
var bubbles = eventInit.bubbles, cancelable = eventInit.cancelable, detail = eventInit.detail;
|
||||
var event = document.createEvent("Event");
|
||||
event.initEvent(type, bubbles || true, cancelable || true);
|
||||
event.detail = detail || {};
|
||||
try {
|
||||
element.disabled = false;
|
||||
element.dispatchEvent(event);
|
||||
} finally {
|
||||
element.disabled = disabled;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
function toArray$1(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
} else if (Array.from) {
|
||||
return Array.from(value);
|
||||
} else {
|
||||
return [].slice.call(value);
|
||||
}
|
||||
}
|
||||
var BlobRecord = function() {
|
||||
function BlobRecord(file, checksum, url) {
|
||||
var _this = this;
|
||||
classCallCheck(this, BlobRecord);
|
||||
this.file = file;
|
||||
this.attributes = {
|
||||
filename: file.name,
|
||||
content_type: file.type || "application/octet-stream",
|
||||
byte_size: file.size,
|
||||
checksum: checksum
|
||||
};
|
||||
this.xhr = new XMLHttpRequest();
|
||||
this.xhr.open("POST", url, true);
|
||||
this.xhr.responseType = "json";
|
||||
this.xhr.setRequestHeader("Content-Type", "application/json");
|
||||
this.xhr.setRequestHeader("Accept", "application/json");
|
||||
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||
var csrfToken = getMetaValue("csrf-token");
|
||||
if (csrfToken != undefined) {
|
||||
this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
|
||||
}
|
||||
this.xhr.addEventListener("load", function(event) {
|
||||
return _this.requestDidLoad(event);
|
||||
});
|
||||
this.xhr.addEventListener("error", function(event) {
|
||||
return _this.requestDidError(event);
|
||||
});
|
||||
}
|
||||
createClass(BlobRecord, [ {
|
||||
key: "create",
|
||||
value: function create(callback) {
|
||||
this.callback = callback;
|
||||
this.xhr.send(JSON.stringify({
|
||||
blob: this.attributes
|
||||
}));
|
||||
}
|
||||
}, {
|
||||
key: "requestDidLoad",
|
||||
value: function requestDidLoad(event) {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
var response = this.response;
|
||||
var direct_upload = response.direct_upload;
|
||||
delete response.direct_upload;
|
||||
this.attributes = response;
|
||||
this.directUploadData = direct_upload;
|
||||
this.callback(null, this.toJSON());
|
||||
} else {
|
||||
this.requestDidError(event);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "requestDidError",
|
||||
value: function requestDidError(event) {
|
||||
this.callback('Error creating Blob for "' + this.file.name + '". Status: ' + this.status);
|
||||
}
|
||||
}, {
|
||||
key: "toJSON",
|
||||
value: function toJSON() {
|
||||
var result = {};
|
||||
for (var key in this.attributes) {
|
||||
result[key] = this.attributes[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}, {
|
||||
key: "status",
|
||||
get: function get$$1() {
|
||||
return this.xhr.status;
|
||||
}
|
||||
}, {
|
||||
key: "response",
|
||||
get: function get$$1() {
|
||||
var _xhr = this.xhr, responseType = _xhr.responseType, response = _xhr.response;
|
||||
if (responseType == "json") {
|
||||
return response;
|
||||
} else {
|
||||
return JSON.parse(response);
|
||||
}
|
||||
}
|
||||
} ]);
|
||||
return BlobRecord;
|
||||
}();
|
||||
var BlobUpload = function() {
|
||||
function BlobUpload(blob) {
|
||||
var _this = this;
|
||||
classCallCheck(this, BlobUpload);
|
||||
this.blob = blob;
|
||||
this.file = blob.file;
|
||||
var _blob$directUploadDat = blob.directUploadData, url = _blob$directUploadDat.url, headers = _blob$directUploadDat.headers;
|
||||
this.xhr = new XMLHttpRequest();
|
||||
this.xhr.open("PUT", url, true);
|
||||
this.xhr.responseType = "text";
|
||||
for (var key in headers) {
|
||||
this.xhr.setRequestHeader(key, headers[key]);
|
||||
}
|
||||
this.xhr.addEventListener("load", function(event) {
|
||||
return _this.requestDidLoad(event);
|
||||
});
|
||||
this.xhr.addEventListener("error", function(event) {
|
||||
return _this.requestDidError(event);
|
||||
});
|
||||
}
|
||||
createClass(BlobUpload, [ {
|
||||
key: "create",
|
||||
value: function create(callback) {
|
||||
this.callback = callback;
|
||||
this.xhr.send(this.file.slice());
|
||||
}
|
||||
}, {
|
||||
key: "requestDidLoad",
|
||||
value: function requestDidLoad(event) {
|
||||
var _xhr = this.xhr, status = _xhr.status, response = _xhr.response;
|
||||
if (status >= 200 && status < 300) {
|
||||
this.callback(null, response);
|
||||
} else {
|
||||
this.requestDidError(event);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "requestDidError",
|
||||
value: function requestDidError(event) {
|
||||
this.callback('Error storing "' + this.file.name + '". Status: ' + this.xhr.status);
|
||||
}
|
||||
} ]);
|
||||
return BlobUpload;
|
||||
}();
|
||||
var id = 0;
|
||||
var DirectUpload = function() {
|
||||
function DirectUpload(file, url, delegate) {
|
||||
classCallCheck(this, DirectUpload);
|
||||
this.id = ++id;
|
||||
this.file = file;
|
||||
this.url = url;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
createClass(DirectUpload, [ {
|
||||
key: "create",
|
||||
value: function create(callback) {
|
||||
var _this = this;
|
||||
FileChecksum.create(this.file, function(error, checksum) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
var blob = new BlobRecord(_this.file, checksum, _this.url);
|
||||
notify(_this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
|
||||
blob.create(function(error) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else {
|
||||
var upload = new BlobUpload(blob);
|
||||
notify(_this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr);
|
||||
upload.create(function(error) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else {
|
||||
callback(null, blob.toJSON());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} ]);
|
||||
return DirectUpload;
|
||||
}();
|
||||
function notify(object, methodName) {
|
||||
if (object && typeof object[methodName] == "function") {
|
||||
for (var _len = arguments.length, messages = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
|
||||
messages[_key - 2] = arguments[_key];
|
||||
}
|
||||
return object[methodName].apply(object, messages);
|
||||
}
|
||||
}
|
||||
var DirectUploadController = function() {
|
||||
function DirectUploadController(input, file) {
|
||||
classCallCheck(this, DirectUploadController);
|
||||
this.input = input;
|
||||
this.file = file;
|
||||
this.directUpload = new DirectUpload(this.file, this.url, this);
|
||||
this.dispatch("initialize");
|
||||
}
|
||||
createClass(DirectUploadController, [ {
|
||||
key: "start",
|
||||
value: function start(callback) {
|
||||
var _this = this;
|
||||
var hiddenInput = document.createElement("input");
|
||||
hiddenInput.type = "hidden";
|
||||
hiddenInput.name = this.input.name;
|
||||
this.input.insertAdjacentElement("beforebegin", hiddenInput);
|
||||
this.dispatch("start");
|
||||
this.directUpload.create(function(error, attributes) {
|
||||
if (error) {
|
||||
hiddenInput.parentNode.removeChild(hiddenInput);
|
||||
_this.dispatchError(error);
|
||||
} else {
|
||||
hiddenInput.value = attributes.signed_id;
|
||||
}
|
||||
_this.dispatch("end");
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "uploadRequestDidProgress",
|
||||
value: function uploadRequestDidProgress(event) {
|
||||
var progress = event.loaded / event.total * 100;
|
||||
if (progress) {
|
||||
this.dispatch("progress", {
|
||||
progress: progress
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "dispatch",
|
||||
value: function dispatch(name) {
|
||||
var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
detail.file = this.file;
|
||||
detail.id = this.directUpload.id;
|
||||
return dispatchEvent(this.input, "direct-upload:" + name, {
|
||||
detail: detail
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "dispatchError",
|
||||
value: function dispatchError(error) {
|
||||
var event = this.dispatch("error", {
|
||||
error: error
|
||||
});
|
||||
if (!event.defaultPrevented) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "directUploadWillCreateBlobWithXHR",
|
||||
value: function directUploadWillCreateBlobWithXHR(xhr) {
|
||||
this.dispatch("before-blob-request", {
|
||||
xhr: xhr
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "directUploadWillStoreFileWithXHR",
|
||||
value: function directUploadWillStoreFileWithXHR(xhr) {
|
||||
var _this2 = this;
|
||||
this.dispatch("before-storage-request", {
|
||||
xhr: xhr
|
||||
});
|
||||
xhr.upload.addEventListener("progress", function(event) {
|
||||
return _this2.uploadRequestDidProgress(event);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
key: "url",
|
||||
get: function get$$1() {
|
||||
return this.input.getAttribute("data-direct-upload-url");
|
||||
}
|
||||
} ]);
|
||||
return DirectUploadController;
|
||||
}();
|
||||
var inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
|
||||
var DirectUploadsController = function() {
|
||||
function DirectUploadsController(form) {
|
||||
classCallCheck(this, DirectUploadsController);
|
||||
this.form = form;
|
||||
this.inputs = findElements(form, inputSelector).filter(function(input) {
|
||||
return input.files.length;
|
||||
});
|
||||
}
|
||||
createClass(DirectUploadsController, [ {
|
||||
key: "start",
|
||||
value: function start(callback) {
|
||||
var _this = this;
|
||||
var controllers = this.createDirectUploadControllers();
|
||||
var startNextController = function startNextController() {
|
||||
var controller = controllers.shift();
|
||||
if (controller) {
|
||||
controller.start(function(error) {
|
||||
if (error) {
|
||||
callback(error);
|
||||
_this.dispatch("end");
|
||||
} else {
|
||||
startNextController();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
_this.dispatch("end");
|
||||
}
|
||||
};
|
||||
this.dispatch("start");
|
||||
startNextController();
|
||||
}
|
||||
}, {
|
||||
key: "createDirectUploadControllers",
|
||||
value: function createDirectUploadControllers() {
|
||||
var controllers = [];
|
||||
this.inputs.forEach(function(input) {
|
||||
toArray$1(input.files).forEach(function(file) {
|
||||
var controller = new DirectUploadController(input, file);
|
||||
controllers.push(controller);
|
||||
});
|
||||
});
|
||||
return controllers;
|
||||
}
|
||||
}, {
|
||||
key: "dispatch",
|
||||
value: function dispatch(name) {
|
||||
var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
return dispatchEvent(this.form, "direct-uploads:" + name, {
|
||||
detail: detail
|
||||
});
|
||||
}
|
||||
} ]);
|
||||
return DirectUploadsController;
|
||||
}();
|
||||
var processingAttribute = "data-direct-uploads-processing";
|
||||
var submitButtonsByForm = new WeakMap();
|
||||
var started = false;
|
||||
function start() {
|
||||
if (!started) {
|
||||
started = true;
|
||||
document.addEventListener("click", didClick, true);
|
||||
document.addEventListener("submit", didSubmitForm);
|
||||
document.addEventListener("ajax:before", didSubmitRemoteElement);
|
||||
}
|
||||
}
|
||||
function didClick(event) {
|
||||
var target = event.target;
|
||||
if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
|
||||
submitButtonsByForm.set(target.form, target);
|
||||
}
|
||||
}
|
||||
function didSubmitForm(event) {
|
||||
handleFormSubmissionEvent(event);
|
||||
}
|
||||
function didSubmitRemoteElement(event) {
|
||||
if (event.target.tagName == "FORM") {
|
||||
handleFormSubmissionEvent(event);
|
||||
}
|
||||
}
|
||||
function handleFormSubmissionEvent(event) {
|
||||
var form = event.target;
|
||||
if (form.hasAttribute(processingAttribute)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
var controller = new DirectUploadsController(form);
|
||||
var inputs = controller.inputs;
|
||||
if (inputs.length) {
|
||||
event.preventDefault();
|
||||
form.setAttribute(processingAttribute, "");
|
||||
inputs.forEach(disable);
|
||||
controller.start(function(error) {
|
||||
form.removeAttribute(processingAttribute);
|
||||
if (error) {
|
||||
inputs.forEach(enable);
|
||||
} else {
|
||||
submitForm(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function submitForm(form) {
|
||||
var button = submitButtonsByForm.get(form) || findElement(form, "input[type=submit], button[type=submit]");
|
||||
if (button) {
|
||||
var _button = button, disabled = _button.disabled;
|
||||
button.disabled = false;
|
||||
button.focus();
|
||||
button.click();
|
||||
button.disabled = disabled;
|
||||
} else {
|
||||
button = document.createElement("input");
|
||||
button.type = "submit";
|
||||
button.style.display = "none";
|
||||
form.appendChild(button);
|
||||
button.click();
|
||||
form.removeChild(button);
|
||||
}
|
||||
submitButtonsByForm.delete(form);
|
||||
}
|
||||
function disable(input) {
|
||||
input.disabled = true;
|
||||
}
|
||||
function enable(input) {
|
||||
input.disabled = false;
|
||||
}
|
||||
function autostart() {
|
||||
if (window.ActiveStorage) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
setTimeout(autostart, 1);
|
||||
exports.start = start;
|
||||
exports.DirectUpload = DirectUpload;
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// Provides a drop-in pointer for the default Trix stylesheet that will
|
||||
// format the toolbar and the trix-editor content (whether displayed or
|
||||
// under editing). Feel free to incorporate this inclusion directly in
|
||||
// any other asset bundle and remove this file.
|
||||
//
|
||||
//= require trix/dist/trix
|
||||
|
||||
// We need to override trix.css’s image gallery styles to accommodate
|
||||
// the <action-text-attachment> element we wrap around attachments.
|
||||
// Otherwise, images in galleries will be squished by the max-width:
|
||||
// 33%; rule.
|
||||
.trix-content {
|
||||
.attachment-gallery {
|
||||
> action-text-attachment,
|
||||
> .attachment {
|
||||
flex: 1 0 33%;
|
||||
padding: 0 0.5em;
|
||||
max-width: 33%;
|
||||
}
|
||||
|
||||
&.attachment-gallery--2,
|
||||
&.attachment-gallery--4 {
|
||||
> action-text-attachment,
|
||||
> .attachment {
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action-text-attachment {
|
||||
.attachment {
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
//= require_tree .
|
||||
|
||||
$black: black;
|
||||
$white: white;
|
||||
$grey: grey;
|
||||
$cyan: #13fefe;
|
||||
$magenta: #f206f9;
|
||||
|
||||
$colors: (
|
||||
"black": $black,
|
||||
"white": $white,
|
||||
"cyan": $cyan,
|
||||
"magenta": $magenta
|
||||
);
|
||||
|
||||
// Redefinir variables de Bootstrap
|
||||
$primary: $magenta;
|
||||
$jumbotron-bg: transparent;
|
||||
|
@ -16,6 +21,15 @@ $form-feedback-icon-valid-color: $black;
|
|||
$component-active-bg: $magenta;
|
||||
|
||||
@import "bootstrap";
|
||||
@import "editor";
|
||||
|
||||
.editor {
|
||||
.editor-content {
|
||||
figure {
|
||||
border: 1px solid transparentize($magenta, 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground: #{$black};
|
||||
|
@ -29,18 +43,6 @@ $component-active-bg: $magenta;
|
|||
--background: #{$black};
|
||||
--color: #{$cyan};
|
||||
}
|
||||
trix-toolbar {
|
||||
.trix-button--icon {
|
||||
background-color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trix-toolbar {
|
||||
background-color: var(--background);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
}
|
||||
|
||||
// TODO: Encontrar la forma de generar esto desde los locales de Rails
|
||||
|
@ -298,3 +300,191 @@ svg {
|
|||
.vh-100 {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
// Viene de sutty-base-jekyll-theme
|
||||
$prefixes: ("", "-webkit-", "-ms-", "-o-", "-moz-");
|
||||
$overflows: auto, hidden, scroll;
|
||||
|
||||
/*
|
||||
* Usar en animaciones, empiezan rápido y desaceleran hacia el final.
|
||||
*/
|
||||
$bezier: cubic-bezier(0.75, 0, 0.25, 1);
|
||||
|
||||
/*
|
||||
* Ocultar la barra de scroll, útil para sliders horizontales.
|
||||
*/
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
@each $cursor in (pointer none) {
|
||||
.cursor-#{$cursor} {
|
||||
cursor: $cursor;
|
||||
}
|
||||
}
|
||||
|
||||
@each $direction in (top, right, bottom, left) {
|
||||
.#{$direction}-0 {
|
||||
#{$direction}: 0
|
||||
}
|
||||
}
|
||||
|
||||
@each $value in $overflows {
|
||||
.overflow-#{$value} { overflow: $value !important; }
|
||||
}
|
||||
|
||||
@each $axis in (y, x) {
|
||||
@each $value in $overflows {
|
||||
.overflow-#{$axis}-#{$value} { overflow-#{$axis}: $value !important; }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Poder aumentar o disminuir el alto de la tipografía, se usa de la
|
||||
* misma forma que los modificadores de padding y margin.
|
||||
*/
|
||||
@each $size, $length in $spacers {
|
||||
.f-#{$size} {
|
||||
font-size: $length !important;
|
||||
}
|
||||
|
||||
.text-column-#{$size} {
|
||||
column-count: $size;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Modificadores de Bootstrap que no tienen versión responsive.
|
||||
*/
|
||||
@each $grid-breakpoint, $_ in $grid-breakpoints {
|
||||
@include media-breakpoint-up($grid-breakpoint) {
|
||||
// border
|
||||
.border-#{$grid-breakpoint} { border: $border-width solid $border-color !important; }
|
||||
.border-#{$grid-breakpoint}-top { border-top: $border-width solid $border-color !important; }
|
||||
.border-#{$grid-breakpoint}-right { border-right: $border-width solid $border-color !important; }
|
||||
.border-#{$grid-breakpoint}-bottom { border-bottom: $border-width solid $border-color !important; }
|
||||
.border-#{$grid-breakpoint}-left { border-left: $border-width solid $border-color !important; }
|
||||
.border-#{$grid-breakpoint}-0 { border: 0 !important; }
|
||||
.border-#{$grid-breakpoint}-top-0 { border-top: 0 !important; }
|
||||
.border-#{$grid-breakpoint}-right-0 { border-right: 0 !important; }
|
||||
.border-#{$grid-breakpoint}-bottom-0 { border-bottom: 0 !important; }
|
||||
.border-#{$grid-breakpoint}-left-0 { border-left: 0 !important; }
|
||||
|
||||
// alineación
|
||||
.text-#{$grid-breakpoint}-left { text-align: left !important; }
|
||||
.text-#{$grid-breakpoint}-right { text-align: right !important; }
|
||||
.text-#{$grid-breakpoint}-center { text-align: center !important; }
|
||||
|
||||
// posición
|
||||
@each $position in $positions {
|
||||
.position-#{$grid-breakpoint}-#{$position} { position: $position !important; }
|
||||
}
|
||||
|
||||
// anchos y altos
|
||||
@each $prop, $abbrev in (width: w, height: h) {
|
||||
@each $size, $length in $sizes {
|
||||
.#{$abbrev}-#{$grid-breakpoint}-#{$size} { #{$prop}: $length !important; }
|
||||
}
|
||||
}
|
||||
|
||||
// versión responsive de f
|
||||
@each $size, $length in $spacers {
|
||||
.f-#{$grid-breakpoint}-#{$size} {
|
||||
font-size: $length !important;
|
||||
}
|
||||
|
||||
.text-column-#{$grid-breakpoint}-#{$size} {
|
||||
column-count: $size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Crea una propiedad con prefijos de navegador
|
||||
*/
|
||||
@mixin vendor-prefix($property, $definition...) {
|
||||
@each $prefix in $prefixes {
|
||||
#{$prefix}$property: $definition;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Crea clases para asignar colores según la lista de colores.
|
||||
*/
|
||||
@each $color, $_ in $colors {
|
||||
.background-#{$color} {
|
||||
background-color: var(--#{$color});
|
||||
|
||||
&:focus {
|
||||
background-color: var(--#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-#{$color} {
|
||||
scrollbar-color: var(--#{$color}) transparent;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.border-#{$color} {
|
||||
border-color: var(--#{$color}) !important;
|
||||
}
|
||||
|
||||
.hover-bg-#{$color} {
|
||||
&:hover {
|
||||
background-color: var(--#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.hover-#{$color} {
|
||||
&:hover {
|
||||
color: var(--#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.#{$color} {
|
||||
color: var(--#{$color});
|
||||
|
||||
&:focus {
|
||||
color: var(--#{$color});
|
||||
}
|
||||
|
||||
::-moz-selection,
|
||||
::selection {
|
||||
background: var(--#{$color});
|
||||
color: white;
|
||||
}
|
||||
|
||||
svg {
|
||||
* {
|
||||
fill: var(--#{$color});
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-color: var(--#{$color});
|
||||
color: var(--#{$color});
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--#{$color});
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--#{$color});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
67
app/assets/stylesheets/editor.scss
Normal file
67
app/assets/stylesheets/editor.scss
Normal file
|
@ -0,0 +1,67 @@
|
|||
.editor {
|
||||
box-sizing: border-box;
|
||||
*, *::before, *::after { box-sizing: inherit; }
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p, li {
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
mark {
|
||||
background: #f206f9;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected { outline: #f206f9 solid medium; }
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
figure {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
img, video, iframe, audio {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.editor-primary-toolbar, .editor-auxiliary-toolbar {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-auxiliary-toolbar {
|
||||
& > * {
|
||||
display: none;
|
||||
}
|
||||
.editor-auxiliary-tool-active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
word-wrap: break-word;
|
||||
div[data-align="left"] { text-align: left; }
|
||||
div[data-align="center"] { text-align: center; }
|
||||
div[data-align="right"] { text-align: right; }
|
||||
|
||||
min-height: 480px;
|
||||
|
||||
*[data-editor-loading] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
*[data-editor-error] {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
|
@ -15,9 +15,6 @@
|
|||
// const images = require.context('../images', true)
|
||||
// const imagePath = (name) => images(name, true)
|
||||
|
||||
require("trix")
|
||||
require("@rails/actiontext")
|
||||
|
||||
import { Application } from 'stimulus'
|
||||
import { definitionsFromContext } from "stimulus/webpack-helpers"
|
||||
|
||||
|
|
|
@ -14,4 +14,44 @@ class MetadataContent < MetadataTemplate
|
|||
def front_matter?
|
||||
false
|
||||
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
|
||||
|
|
|
@ -15,7 +15,6 @@ class MetadataString < MetadataTemplate
|
|||
|
||||
sanitizer.sanitize(string.strip,
|
||||
tags: [],
|
||||
attributes: [],
|
||||
scrubber: scrubber).strip.html_safe
|
||||
attributes: []).strip.html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
||||
:value, :help, :required, :errors, :post,
|
||||
:layout, keyword_init: true) do
|
||||
include ActionText::ContentHelper
|
||||
|
||||
attr_reader :value_was
|
||||
|
||||
# Queremos que los artículos nuevos siempre cacheen, si usamos el UUID
|
||||
|
@ -144,8 +142,19 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
|
|||
|
||||
sanitizer.sanitize(string.tr("\r", ''),
|
||||
tags: allowed_tags,
|
||||
attributes: allowed_attributes + %w[data-trix-attachment],
|
||||
scrubber: scrubber).strip.html_safe
|
||||
attributes: allowed_attributes).strip.html_safe
|
||||
end
|
||||
|
||||
def sanitizer
|
||||
@sanitizer ||= Rails::Html::Sanitizer.safe_list_sanitizer.new
|
||||
end
|
||||
|
||||
def allowed_attributes
|
||||
@allowed_attributes ||= %w[style href src alt controls data-align].freeze
|
||||
end
|
||||
|
||||
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].freeze
|
||||
end
|
||||
|
||||
# Decifra el valor
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
- if request.referer
|
||||
= render 'layouts/breadcrumb', crumbs: [ link_to(t('help.markdown.back'), request.referer) ]
|
||||
|
||||
.row
|
||||
.col
|
||||
%table.table.table-responsive-md
|
||||
%thead
|
||||
%tr
|
||||
%th= t('help.markdown.input')
|
||||
%th= t('help.markdown.output')
|
||||
%tbody
|
||||
%tr
|
||||
%td
|
||||
%code= "**#{t('help.markdown.bold')}**"
|
||||
%td
|
||||
%strong= t('help.markdown.bold')
|
||||
%tr
|
||||
%td
|
||||
%code= "_#{t('help.markdown.italic')}_"
|
||||
%td
|
||||
%em= t('help.markdown.italic')
|
||||
- 7.times do |i|
|
||||
- next if i == 0
|
||||
%tr
|
||||
%td
|
||||
%code= "#{'#' * i} #{t('help.markdown.heading')} #{i}"
|
||||
%td= raw "<h#{i}>#{t('help.markdown.heading')} #{i}</h#{i}>"
|
||||
%tr
|
||||
%td
|
||||
%code= "[#{t('help.markdown.link.text')}](#{t('help.markdown.link.url')})"
|
||||
%td= link_to t('help.markdown.link.text'), t('help.markdown.link.url')
|
||||
%tr
|
||||
%td
|
||||
%code= "> #{t('help.markdown.quote')}"
|
||||
%td
|
||||
%blockquote.blockquote= t('help.markdown.quote')
|
||||
%tr
|
||||
%td
|
||||
%code
|
||||
- 3.times do
|
||||
= "* #{t('help.markdown.ul')}"
|
||||
%br
|
||||
%td
|
||||
%ul
|
||||
- 3.times do
|
||||
%li= t('help.markdown.ul')
|
||||
%tr
|
||||
%td
|
||||
%code
|
||||
- 3.times do |i|
|
||||
= "#{i}. #{t('help.markdown.ol')}"
|
||||
%br
|
||||
%td
|
||||
%ol
|
||||
- 3.times do
|
||||
%li= t('help.markdown.ol')
|
||||
%tr
|
||||
%td{colspan: 2}= t('help.markdown.dir')
|
||||
%tr
|
||||
%td
|
||||
%code= "[#{t('help.markdown.ltr')}]{dir=ltr lang=es}"
|
||||
%td
|
||||
%span{dir: 'ltr', lang: 'es'}= t('help.markdown.ltr')
|
||||
%tr
|
||||
%td
|
||||
%code= "[#{t('help.markdown.rtl')}]{dir=rtl lang=es}"
|
||||
%td
|
||||
%span{dir: 'rtl', lang: 'es'}= t('help.markdown.rtl')
|
||||
|
||||
%tr
|
||||
%td
|
||||
%code= "![#{t('help.markdown.img.text')}](#{t('help.markdown.img.url')})"
|
||||
%td
|
||||
%img{alt: t('help.markdown.img.text'), src: t('help.markdown.img.url')}
|
|
@ -2,6 +2,72 @@
|
|||
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
|
||||
= render 'posts/attribute_feedback',
|
||||
post: post, attribute: attribute, metadata: metadata
|
||||
= rich_text_area_tag "post[#{attribute}]", metadata.value,
|
||||
**field_options(attribute, metadata), class: '', dir: dir,
|
||||
lang: locale
|
||||
|
||||
.editor
|
||||
= text_area_tag "post[#{attribute}]", '',
|
||||
dir: dir, lang: locale,
|
||||
**field_options(attribute, metadata), class: 'd-none'
|
||||
|
||||
.editor-toolbar
|
||||
.editor-primary-toolbar.scrollbar-black
|
||||
%button.btn{ data: { button: 'bold' } }= t('editor.bold')
|
||||
%button.btn{ data: { button: 'italic' } }= t('editor.italic')
|
||||
%button.btn{ data: { button: 'deleted' } }= t('editor.deleted')
|
||||
%button.btn{ data: { button: 'underline' } }= t('editor.underline')
|
||||
%button.btn{ data: { button: 'mark' } }= t('editor.mark')
|
||||
%button.btn{ data: { button: 'h1' } }= t('editor.h1')
|
||||
%button.btn{ data: { button: 'h2' } }= t('editor.h2')
|
||||
%button.btn{ data: { button: 'h3' } }= t('editor.h3')
|
||||
%button.btn{ data: { button: 'h4' } }= t('editor.h4')
|
||||
%button.btn{ data: { button: 'h5' } }= t('editor.h5')
|
||||
%button.btn{ data: { button: 'h6' } }= t('editor.h6')
|
||||
%button.btn{ data: { button: 'ul' } }= t('editor.ul')
|
||||
%button.btn{ data: { button: 'ol' } }= t('editor.ol')
|
||||
%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')
|
||||
|
||||
-#
|
||||
HAML cringe
|
||||
TODO: generar IDs para labels
|
||||
.editor-auxiliary-toolbar.mt-1.scrollbar-black{ data: { editor: { auxiliary: { toolbar: '' } } } }
|
||||
.form-group{ data: { editor: { auxiliary: 'mark' } } }
|
||||
%label{ for: 'mark-color' }= t('editor.color')
|
||||
%input.form-control{ type: 'color', data: { prop: 'mark-color' } }/
|
||||
|
||||
%div{ data: { editor: { auxiliary: 'img' } } }
|
||||
.row
|
||||
.col.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')
|
||||
.col.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')
|
||||
|
||||
.form-group{ data: { editor: { auxiliary: 'link' } } }
|
||||
%label{ for: 'link-href' }= t('editor.url')
|
||||
%input.form-control{ type: 'url', data: { prop: 'link-href' } }/
|
||||
|
||||
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
|
||||
= metadata.value.html_safe
|
||||
|
|
|
@ -1,8 +1,72 @@
|
|||
.form-group.markdown-content
|
||||
.form-group
|
||||
= label_tag "post_#{attribute}", post_label_t(attribute, post: post)
|
||||
= render 'posts/attribute_feedback',
|
||||
post: post, attribute: attribute, metadata: metadata
|
||||
= text_area_tag "post[#{attribute}]", metadata.value,
|
||||
dir: dir, lang: locale,
|
||||
**field_options(attribute, metadata, class: 'content')
|
||||
.editor.mt-1
|
||||
|
||||
.editor
|
||||
= text_area_tag "post[#{attribute}]", metadata.value,
|
||||
dir: dir, lang: locale,
|
||||
**field_options(attribute, metadata), class: 'd-none'
|
||||
|
||||
.editor-toolbar
|
||||
.editor-primary-toolbar.scrollbar-black
|
||||
%button.btn{ data: { button: 'bold' } }= t('editor.bold')
|
||||
%button.btn{ data: { button: 'italic' } }= t('editor.italic')
|
||||
%button.btn{ data: { button: 'deleted' } }= t('editor.deleted')
|
||||
%button.btn{ data: { button: 'underline' } }= t('editor.underline')
|
||||
%button.btn{ data: { button: 'mark' } }= t('editor.mark')
|
||||
%button.btn{ data: { button: 'h1' } }= t('editor.h1')
|
||||
%button.btn{ data: { button: 'h2' } }= t('editor.h2')
|
||||
%button.btn{ data: { button: 'h3' } }= t('editor.h3')
|
||||
%button.btn{ data: { button: 'h4' } }= t('editor.h4')
|
||||
%button.btn{ data: { button: 'h5' } }= t('editor.h5')
|
||||
%button.btn{ data: { button: 'h6' } }= t('editor.h6')
|
||||
%button.btn{ data: { button: 'ul' } }= t('editor.ul')
|
||||
%button.btn{ data: { button: 'ol' } }= t('editor.ol')
|
||||
%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')
|
||||
|
||||
-#
|
||||
HAML cringe
|
||||
TODO: generar IDs para labels
|
||||
.editor-auxiliary-toolbar.scrollbar-black{ data: { editor: { auxiliary: { toolbar: '' } } } }
|
||||
.form-group{ data: { editor: { auxiliary: 'mark' } } }
|
||||
%label{ for: 'mark-color' }= t('editor.color')
|
||||
%input{ type: 'color', data: { prop: 'mark-color' } }/
|
||||
|
||||
%div{ data: { editor: { auxiliary: 'img' } } }
|
||||
.row
|
||||
.col.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')
|
||||
.col.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/flac,audio/mp4,audio/ogg,audio/webm,audio/mp3' }/
|
||||
%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/mp4,video/ogg,video/webm' }/
|
||||
%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')
|
||||
|
||||
.form-group{ data: { editor: { auxiliary: 'link' } } }
|
||||
%label{ for: 'link-href' }= t('editor.url')
|
||||
%input.form-control{ type: 'url', data: { prop: 'link-href' } }/
|
||||
|
||||
.editor-content.form-control.h-auto{ contenteditable: 'true' }
|
||||
|
|
|
@ -41,5 +41,5 @@
|
|||
- next if metadata.front_matter?
|
||||
|
||||
- cache metadata do
|
||||
%section{ id: attr, dir: dir }
|
||||
%section.editor{ id: attr, dir: dir }
|
||||
= @post.public_send(attr).to_s.html_safe
|
||||
|
|
|
@ -14,7 +14,7 @@ Rails.application.config.content_security_policy do |policy|
|
|||
policy.script_src :self
|
||||
policy.font_src :self, :https
|
||||
# XXX: Los íconos de Trix se cargan vía data:
|
||||
policy.img_src :self, :data, :https
|
||||
policy.img_src :self, :data, :https, :blob
|
||||
# Ya no usamos applets!
|
||||
policy.object_src :none
|
||||
if Rails.env.development?
|
||||
|
|
|
@ -553,3 +553,33 @@ en:
|
|||
title: Encrypted content
|
||||
description: The field contents are encrypted before being stored and won't be available on the public website or its source code. You can save private information here and it will only be readable to this site's users through Sutty's panel.
|
||||
decryption_error: There was an error trying to decrypt the content, Sutty's team has been notified!
|
||||
editor:
|
||||
bold: Bold
|
||||
italic: Emphasis
|
||||
deleted: Strikethrough
|
||||
underline: Underline
|
||||
mark: Mark
|
||||
h1: Heading 1
|
||||
h2: Heading 2
|
||||
h3: Heading 3
|
||||
h4: Heading 4
|
||||
h5: Heading 5
|
||||
h6: Heading 6
|
||||
ul: Unordered list
|
||||
ol: Ordered list
|
||||
left: Left
|
||||
right: Right
|
||||
center: Center
|
||||
img: Image
|
||||
video: Video
|
||||
audio: Audio
|
||||
pdf: PDF
|
||||
color: Color
|
||||
img: Image
|
||||
file:
|
||||
img: Select and upload image
|
||||
video: Select and upload video
|
||||
audio: Select and upload audio
|
||||
pdf: Select and upload PDF
|
||||
description: Description for blind people and search engines
|
||||
url: Address
|
||||
|
|
|
@ -572,3 +572,33 @@ es:
|
|||
title: Contenido cifrado
|
||||
description: El contenido de este campo se guarda cifrado y no estará disponible en el sitio ni en su código fuente. Puedes guardar información privada aquí y sólo estará disponible para quienes tengan acceso a ese sitio en el panel de Sutty.
|
||||
decryption_error: Hubo un error al decifrar la información, ¡el equipo de Sutty ya fue notificado!
|
||||
editor:
|
||||
bold: Fuerte
|
||||
italic: Énfasis
|
||||
deleted: Tachado
|
||||
underline: Subrayado
|
||||
mark: Resaltado
|
||||
h1: Título 1
|
||||
h2: Título 2
|
||||
h3: Título 3
|
||||
h4: Título 4
|
||||
h5: Título 5
|
||||
h6: Título 6
|
||||
ul: Lista itemizada
|
||||
ol: Lista numerada
|
||||
left: Izquierda
|
||||
right: Derecha
|
||||
center: Centro
|
||||
img: Imágen
|
||||
video: Video
|
||||
audio: Audio
|
||||
pdf: PDF
|
||||
color: Color
|
||||
img: Imágen
|
||||
file:
|
||||
img: Seleccionar y subir imágen
|
||||
video: Seleccionar y subir video
|
||||
audio: Seleccionar y subir audio
|
||||
pdf: Seleccionar y subir archivo PDF
|
||||
description: Descripción para personas no videntes y buscadores
|
||||
url: Dirección
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"prosemirror-schema-basic": "^1.1.2",
|
||||
"stimulus": "^1.1.1",
|
||||
"table-dragger": "git+https://0xacab.org/sutty/table-dragger.git",
|
||||
"trix": "git+https://0xacab.org/sutty/trix.git",
|
||||
"zepto": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
BIN
public/placeholder.png
Normal file
BIN
public/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -6990,10 +6990,6 @@ trim-right@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
||||
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
|
||||
|
||||
"trix@git+https://0xacab.org/sutty/trix.git":
|
||||
version "1.2.0"
|
||||
resolved "git+https://0xacab.org/sutty/trix.git#0a53f75dbef0d650acbd73344f7d50ac4aedbe16"
|
||||
|
||||
"true-case-path@^1.0.2":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
|
||||
|
|
Loading…
Reference in a new issue