5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-15 06:51:42 +00:00

Merge branch 'editor-en-webpack' into editor

This commit is contained in:
void 2021-02-02 20:00:32 +00:00
commit db0a32a930
7 changed files with 88 additions and 69 deletions

View file

@ -1,3 +1,13 @@
import {
moveChildren,
marks,
blocks,
parentBlocks,
typesWithProperties,
setAuxiliaryToolbar,
tagNameSetFn
} from 'editor/types'
const origin = location.origin const origin = location.origin
/* /*
@ -27,7 +37,7 @@ const restoreContent = (editorEl, contentEl) => {
/* getRangeAt puede fallar si no hay una selección /* getRangeAt puede fallar si no hay una selección
*/ */
function safeGetRangeAt (num) { const safeGetRangeAt = (num) => {
try { try {
return window.getSelection().getRangeAt(num) return window.getSelection().getRangeAt(num)
} catch (error) { } catch (error) {
@ -36,48 +46,27 @@ function safeGetRangeAt (num) {
} }
} }
function uploadFile (file) {
return new Promise((resolve, reject) => {
const upload = new ActiveStorage.DirectUpload(
file,
origin + '/rails/active_storage/direct_uploads',
)
upload.create((error, blob) => { const isDirectChild = (node, supposedChild) => {
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) { for (const child of node.childNodes) {
if (child == supposedChild) return true if (child == supposedChild) return true
} }
} }
function isChildSelection (sel, el) { const isChildSelection = (sel, el) => {
return ( return (
(el.contains(sel.anchorNode) || el.contains(sel.focusNode)) (el.contains(sel.anchorNode) || el.contains(sel.focusNode))
&& !(sel.anchorNode == el || sel.focusNode == el) && !(sel.anchorNode == el || sel.focusNode == el)
) )
} }
function getElementParent (node) { const getElementParent = (node) => {
let parentEl = node let parentEl = node
while (parentEl.nodeType != Node.ELEMENT_NODE) parentEl = parentEl.parentElement while (parentEl.nodeType != Node.ELEMENT_NODE) parentEl = parentEl.parentElement
return parentEl return parentEl
} }
function splitNode (node, range) { const splitNode = (node, range) => {
const [left, right] = [ const [left, right] = [
{ range: document.createRange(), node: node.cloneNode(false) }, { range: document.createRange(), node: node.cloneNode(false) },
{ range: document.createRange(), node: node.cloneNode(false) }, { range: document.createRange(), node: node.cloneNode(false) },
@ -109,7 +98,7 @@ function splitNode (node, range) {
* * mark: un objeto que representa el tipo de acción (ver types.js) * * mark: un objeto que representa el tipo de acción (ver types.js)
* * contentEl: el elemento de contenido del editor. * * contentEl: el elemento de contenido del editor.
*/ */
function setupMarkButton (button, mark, contentEl) { const setupMarkButton = (button, mark, contentEl) => {
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault() event.preventDefault()
@ -168,7 +157,7 @@ function setupMarkButton (button, mark, contentEl) {
} }
/* Igual que `setupMarkButton` pero para bloques. */ /* Igual que `setupMarkButton` pero para bloques. */
function setupBlockButton (button, block, contentEl, editorEl) { const setupBlockButton = (button, block, contentEl, editorEl) => {
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault() event.preventDefault()
@ -203,7 +192,7 @@ function setupBlockButton (button, block, contentEl, editorEl) {
} }
/* Igual que `setupBlockButton` pero para bloques parientes. */ /* Igual que `setupBlockButton` pero para bloques parientes. */
function setupParentBlockButton (button, parentBlock, contentEl) { const setupParentBlockButton = (button, parentBlock, contentEl) => {
button.addEventListener("click", event => { button.addEventListener("click", event => {
event.preventDefault() event.preventDefault()
@ -246,7 +235,7 @@ const elementIsTypes = types => element => {
const elementIsBlock = elementIsTypes(blocks) const elementIsBlock = elementIsTypes(blocks)
const elementIsParentBlock = elementIsTypes(parentBlocks) const elementIsParentBlock = elementIsTypes(parentBlocks)
function hasContent (element) { const hasContent = (element) => {
if (element.firstElementChild) return true if (element.firstElementChild) return true
for (const child of element.childNodes) { for (const child of element.childNodes) {
if (child.nodeType === Node.TEXT_NODE && child.data.length > 0) return true if (child.nodeType === Node.TEXT_NODE && child.data.length > 0) return true
@ -266,7 +255,7 @@ function hasContent (element) {
* * Cambia el tag de los bloques no reconocidos (ver `elementIsBlock`) * * Cambia el tag de los bloques no reconocidos (ver `elementIsBlock`)
* * Hace lo que hace cleanNode * * Hace lo que hace cleanNode
*/ */
function cleanContent (contentEl) { const cleanContent = (contentEl) => {
const sel = window.getSelection() const sel = window.getSelection()
cleanNode(contentEl, contentEl) cleanNode(contentEl, contentEl)
@ -276,7 +265,10 @@ function cleanContent (contentEl) {
if (elementIsParentBlock(child)) { if (elementIsParentBlock(child)) {
cleanContent(child) cleanContent(child)
} else if (!elementIsBlock(child)) { } else if (!elementIsBlock(child)) {
child.tagName = "P" const el = document.createElement("p")
moveChildren(child, el, null)
contentEl.insertBefore(el, child)
child.parentNode.removeChild(child)
} }
} else if (child.nodeType === Node.TEXT_NODE) { } else if (child.nodeType === Node.TEXT_NODE) {
const el = document.createElement("p") const el = document.createElement("p")
@ -293,7 +285,7 @@ function cleanContent (contentEl) {
* * Crea un p y inserta la selección si no hay elementos * * 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á * * Wrappea el contenido de un UL o OL en un LI si no lo está
*/ */
function fixContent (contentEl) { const fixContent = (contentEl) => {
for (const child of contentEl.childNodes) { for (const child of contentEl.childNodes) {
if (child.tagName) { if (child.tagName) {
if (elementIsParentBlock(child)) { if (elementIsParentBlock(child)) {
@ -323,7 +315,7 @@ function fixContent (contentEl) {
* * Borra propiedades de IMG no autorizadas * * Borra propiedades de IMG no autorizadas
* * Borra <FONT> y <STYLE> * * Borra <FONT> y <STYLE>
*/ */
function cleanNode (node, contentEl) { const cleanNode = (node, contentEl) => {
for (const child of node.childNodes) { for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) { if (child.nodeType === Node.TEXT_NODE) {
if (child.nextSibling && child.nextSibling.nodeType === Node.TEXT_NODE) { if (child.nextSibling && child.nextSibling.nodeType === Node.TEXT_NODE) {
@ -407,10 +399,10 @@ function cleanNode (node, contentEl) {
/* Generar el clickListener para este editor. /* Generar el clickListener para este editor.
*/ */
function generateClickListener (editorEl, contentEl) { const generateClickListener = (editorEl, contentEl) => {
/* El event listener para los typesWithProperties. /* El event listener para los typesWithProperties.
*/ */
return function clickListener (event) { return (event) => {
// Borrar todas las selecciones // Borrar todas las selecciones
for (const el of contentEl.querySelectorAll(".selected")) { for (const el of contentEl.querySelectorAll(".selected")) {
el.classList.remove("selected") el.classList.remove("selected")
@ -441,7 +433,7 @@ function generateClickListener (editorEl, contentEl) {
} }
} }
function setupEditor (editorEl) { const setupEditor = (editorEl) => {
// XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor? // XXX: ¡Esto afecta a todo el documento! ¿Quizás usar un iframe para el editor?
document.execCommand('defaultParagraphSeparator', false, 'p') document.execCommand('defaultParagraphSeparator', false, 'p')
@ -450,9 +442,8 @@ function setupEditor (editorEl) {
contentEl.addEventListener("paste", event => { contentEl.addEventListener("paste", event => {
editorEl.querySelector(".editor-aviso-word").style.display = "block" editorEl.querySelector(".editor-aviso-word").style.display = "block"
}) })
document.addEventListener("selectionchange", event => {
cleanContent(contentEl) document.addEventListener("selectionchange", event => cleanContent(contentEl))
})
const clickListener = generateClickListener(editorEl, contentEl) const clickListener = generateClickListener(editorEl, contentEl)
contentEl.addEventListener("click", clickListener, true) contentEl.addEventListener("click", clickListener, true)
@ -474,15 +465,6 @@ function setupEditor (editorEl) {
const editorBtn = id => editorEl.querySelector(`*[data-button="${id}"]`) const editorBtn = id => editorEl.querySelector(`*[data-button="${id}"]`)
// XXX: Por qué está duplicada de types.js esta función?
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 == // == SETUP BUTTONS ==
for (const [name, mark] of Object.entries(marks)) { for (const [name, mark] of Object.entries(marks)) {
setupMarkButton(editorBtn(name), mark, contentEl) setupMarkButton(editorBtn(name), mark, contentEl)
@ -529,9 +511,7 @@ function setupEditor (editorEl) {
} }
// TODO: por ahora confiamos, quizás queremos filtrar estilos? // TODO: por ahora confiamos, quizás queremos filtrar estilos?
function stringifyAllowedStyle (element) { const stringifyAllowedStyle = (element) => element.style.cssText
return element.style.cssText
}
document.addEventListener("turbolinks:load", () => { document.addEventListener("turbolinks:load", () => {
for (const editorEl of document.querySelectorAll(".editor")) { for (const editorEl of document.querySelectorAll(".editor")) {

View file

@ -1,4 +1,4 @@
function setAuxiliaryToolbar (editorEl, toolbarName) { export const setAuxiliaryToolbar = (editorEl, toolbarName) => {
const toolbarEl = editorEl.querySelector(`*[data-editor-auxiliary-toolbar]`) const toolbarEl = editorEl.querySelector(`*[data-editor-auxiliary-toolbar]`)
for (const otherEl of toolbarEl.childNodes) { for (const otherEl of toolbarEl.childNodes) {
if (otherEl.nodeType !== Node.ELEMENT_NODE) continue if (otherEl.nodeType !== Node.ELEMENT_NODE) continue
@ -10,7 +10,29 @@ function setAuxiliaryToolbar (editorEl, toolbarName) {
} }
} }
const marks = { 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: { bold: {
selector: "strong", selector: "strong",
createFn: () => document.createElement("STRONG"), createFn: () => document.createElement("STRONG"),
@ -45,7 +67,7 @@ const marks = {
} }
} }
const tagNameSetFn = tagName => el => { export const tagNameSetFn = tagName => el => {
const newEl = document.createElement(tagName) const newEl = document.createElement(tagName)
moveChildren(el, newEl, null) moveChildren(el, newEl, null)
el.parentNode.insertBefore(newEl, el) el.parentNode.insertBefore(newEl, el)
@ -53,7 +75,7 @@ const tagNameSetFn = tagName => el => {
window.getSelection().collapse(newEl, 0) window.getSelection().collapse(newEl, 0)
} }
const blocks = { export const blocks = {
p: { p: {
noButton: true, noButton: true,
selector: "P", selector: "P",
@ -158,7 +180,7 @@ const divWithStyleCreateFn = styleFn => () => {
return el return el
} }
const parentBlocks = { export const parentBlocks = {
left: { left: {
selector: "div[data-align=left]", selector: "div[data-align=left]",
createFn: divWithStyleCreateFn(el => el.dataset.align = "left"), createFn: divWithStyleCreateFn(el => el.dataset.align = "left"),
@ -173,19 +195,18 @@ const parentBlocks = {
}, },
} }
const hex = (x) => ("0" + parseInt(x).toString(16)).slice(-2)
// https://stackoverflow.com/a/3627747 // https://stackoverflow.com/a/3627747
// TODO: cambiar por una solución más copada // TODO: cambiar por una solución más copada
function rgb2hex(rgb) { const rgb2hex = (rgb) => {
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); 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]); return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
} }
const getSelected = contentEl => contentEl.querySelector(".selected") const getSelected = contentEl => contentEl.querySelector(".selected")
const typesWithProperties = { export const typesWithProperties = {
a: { a: {
selector: marks.a.selector, selector: marks.a.selector,
updateInput (el, editorEl) { updateInput (el, editorEl) {

View file

@ -26,6 +26,7 @@ window.airbrake = new Notifier({
import 'core-js/stable' import 'core-js/stable'
import 'regenerator-runtime/runtime' import 'regenerator-runtime/runtime'
import 'controllers' import 'controllers'
import 'editor/editor'
import {EditorState} from "prosemirror-state" import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view" import {EditorView} from "prosemirror-view"

View file

@ -59,6 +59,8 @@ class DeployLocal < Deploy
'PATH' => paths.join(':'), 'PATH' => paths.join(':'),
'SPREE_API_KEY' => site.tienda_api_key, 'SPREE_API_KEY' => site.tienda_api_key,
'SPREE_URL' => site.tienda_url, 'SPREE_URL' => site.tienda_url,
'AIRBRAKE_PROJECT_ID' => site.id.to_s,
'AIRBRAKE_PROJECT_KEY' => site.airbrake_api_key,
'JEKYLL_ENV' => Rails.env, 'JEKYLL_ENV' => Rails.env,
'LANG' => ENV['LANG'] 'LANG' => ENV['LANG']
} }

View file

@ -141,10 +141,17 @@ class Post < OpenStruct
end end
alias to_param id alias to_param id
# Fecha de última modificación del archivo
def updated_at def updated_at
File.mtime(path.absolute) File.mtime(path.absolute)
end end
# Obtiene la fecha actual de modificación y la guarda hasta la próxima
# vez.
def modified_at
@modified_at ||= Time.now
end
# Solo ejecuta la magia de OpenStruct si el campo existe en la # Solo ejecuta la magia de OpenStruct si el campo existe en la
# plantilla # plantilla
# #
@ -215,6 +222,7 @@ class Post < OpenStruct
# Y que no se procese liquid # Y que no se procese liquid
yaml['liquid'] = false yaml['liquid'] = false
yaml['usuaries'] = usuaries.map(&:id).uniq yaml['usuaries'] = usuaries.map(&:id).uniq
yaml['last_modified_at'] = modified_at
"#{yaml.to_yaml}---\n\n#{body}" "#{yaml.to_yaml}---\n\n#{body}"
end end

View file

@ -4,23 +4,30 @@ class Site
module Api module Api
extend ActiveSupport::Concern extend ActiveSupport::Concern
AIRBRAKE_SECRET = 'an api key for airbrake'
included do included do
encrypts :api_key encrypts :api_key
before_save :add_api_key_if_missing! before_save :add_api_key_if_missing!
# Genera mensajes secretos que podemos usar para la API de cada sitio. # Genera mensajes secretos que podemos usar para la API de cada
# sitio.
#
# XXX: Si no se configura una API key del sitio o genérica, no
# tenemos forma de verificar los mensajes, pero la generación de
# llaves no va a fallar.
def verifier def verifier
@verifier ||= ActiveSupport::MessageVerifier.new api_key @verifier ||= ActiveSupport::MessageVerifier.new(api_key || Rails.application.credentials.api_key || SecureRandom.hex(64))
end end
def airbrake_api_key def airbrake_api_key
@airbrake_api_key ||= verifier.generate(AIRBRAKE_SECRET, purpose: :airbrake) @airbrake_api_key ||= verifier.generate(airbrake_secret, purpose: :airbrake)
end end
private private
def airbrake_secret
Rails.application.credentials.airbrake || SecureRandom.hex(64)
end
# Asegurarse que el sitio tenga una llave para la API # Asegurarse que el sitio tenga una llave para la API
def add_api_key_if_missing! def add_api_key_if_missing!
self.api_key ||= SecureRandom.hex(64) self.api_key ||= SecureRandom.hex(64)

View file

@ -107,7 +107,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
params: { params: {
post: { post: {
image: { image: {
path: fixture_file_upload('files/logo.png', 'image/png'), path: fixture_file_upload('logo.png', 'image/png'),
description: 'hola' description: 'hola'
} }
} }
@ -132,7 +132,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
params: { params: {
post: { post: {
image: { image: {
path: fixture_file_upload('files/_logo.png', 'image/png'), path: fixture_file_upload('_logo.png', 'image/png'),
description: 'hola' description: 'hola'
} }
} }