Editando SVG na mão pra pedir café
A propósito, quero um café, compra um pra mim?
Quero café.
O post de hoje tem tudo a ver com café. Na real, tudo começou com uma treta quando atacaram a AlertPix acusando injustamente baseado no valor mais competitivo.
Falei com o fundador da AlertPix para por as doações no Computaria e ele me deu a ideia de usar o Pix me a Coffe, já que o AlertPix é mais voltado para streamers justamente dando o alerta na live ao receber um pix. Já o foco do Pix me a Coffe é justamente essa de ter uma porta aberta e bonitinha para que a pessoa possa me pagar um café.
Então pedi o ícone da Pix me a Coffe em svg para o fundador e ele prontamente me entregou! E ainda entregou com a coloração no tom de cinza adequada prevendo que eu não iria me atentar ao esquema de cores e que provavelmente iria fazer besteira ao embarcar aqui no Computaria o logo do cafezinho.
Porém… o Daniel Limae me entregou em 23 x 27, e o padrão que está sendo usado é 16 x 16… então chegou o ponto de customizar o SVG para se adequar a mim.
Antes, o embed
Antes de entrar no tema principal, abordar primeiro aqui um tema secundário rápido: como fazer o embed do cartão do Pix me a Coffe.
A minha primeira ideia foi por um <iframe>
, pois
através da tag iframe se consegue colocar informações
de um HTML provido por outro site dentro do meu site:
<iframe src="https://www.pixme.bio/jeffquesado"></iframe>
Só que ficou mais feio do que eu imaginei que ia ficar:
Não me restou muita alternativa além de deixar bonito. A primeira coisa que fiz foi abrir a referência sobre iframe: documentação da MDN
Primeiras coisas que eu percebi que eu desejava:
- remover a borda (propriedade
frameboder="0"
) - impedir o scroll (propriedade
scrolling="no"
)
Fui pela alternativa clássica de remover a borda e de impedir o scroll. Ainda precisava resolver a questão do tamanho:
<iframe src="https://www.pixme.bio/jeffquesado" frameboder="0" scrolling="no"></iframe>
Inspecionei o conteúdo do iframe e cutuquei as propriedade width
e
height
pelo próprio browser até chegar no tamanho adequado:
400 x 800:
<iframe src="https://www.pixme.bio/jeffquesado" frameborder="0" width="400"
height="800" scrolling="no">
</iframe>
Ficou assim:
Porém, parecia que o iframe embarcado no blog não estava no lugar adequado.
Tinha muito espaço para a direita e o iframe impedia o fluxo natural de leitura
do artigo. E se desse para aproveitar o lado da direita? Bem, por que nào?
Usar como se fosse um objeto flutuante a direita? Daí achei
a propriedade CSS float
.
Coloquei para testar um estilo inline com float: right
, e eis que ficou
assim:
<iframe src="https://www.pixme.bio/jeffquesado" frameborder="0" width="400"
height="800" scrolling="no" style="float: right">
</iframe>
Ótimo, tudo no lugar. Uma revisada na documentação pra saber se estava tudo
perfeitinho e percebo que tanto o scrolling
quanto o frameborder
estão
marcados como deprecados. Hmmmm, para a borda recomendou usar propriedades
CSS, mas para o scroll recomendou apenas remover. Ok, então:
<iframe src="https://www.pixme.bio/jeffquesado" width="400"
height="800" style="float: right; border-width: 0px">
</iframe>
O caso para mobile
Bem, ao abrir no modo mobile ficou horrível o post. Primeiro precisava fazer um pouco mais de um scroll completo de tela para sair do banner pedindo café. E segundo porque o banner não ficou centralizado.
Nada como usar as propriedades do browser para fazer testes antes de lançar o artigo, não é?
Bem, e se eu sumir com o iframe no caso de ser mobile? Já temos um ponto de quebra no blog que é saindo de 800px de largura para 801px de largura.
Podemos fazer isso usando @media
query.
Para usar as @media
queries preciso estar dentro da tag <style>
para
escrever CSS localmente, e consigo determinar valor de clases CSS
dentro das @media
queries. Como o ponto de partida é <= 800px
e > 800, podemos fazer as seguintes queries:
@media (max-width: 800px) {
.pixmeacoffe {
display: none;
}
}
@media (min-width: 801px) {
.pixmeacoffe {
float: right;
border-width: 0px;
}
}
Assim, podemos remover o estilo inline do iframe e dizer
que ele é da classe pixmeacoffe
:
<iframe src="https://www.pixme.bio/jeffquesado" width="400"
height="800" class="pixmeacoffe">
</iframe>
Só que isso tem um revés… a carga do iframe irá ocorrer mesmo que o usuário de mobile em nenhuma hipótese abra o iframe. Seria bom se tivesse uma alternativa que permitisse lazy loading,,,
Bem, veja só! Existe! Atributo load="lazy"
!!
Olhando o console da web o Firefox ainda me mostrou a seguinte mensagem:
Partitioned cookie or storage access was provided to “https://www.pixme.bio/jeffquesado” because it is loaded in the third-party context and dynamic state partitioning is enabled. [Learn more]
Ok, então como posso desabilitar acesso ao storage e cookies?
Bem, usando o modo de sandbox
.
A primeira alternativa é deixar em branco, mas isso não foi bom para o banner,
já que ele se monta usando JS. Então dei a permissão allow-scripts
:
<iframe src="https://www.pixme.bio/jeffquesado" width="400"
height="800" class="pixmeacoffe" loading="lazy" sandbox="allow-scripts">
</iframe>
Ok, mas agora o café sumiu totalmente do radar. E se eu adicionasse o copinho de café na frase? E fizesse ele aparecer no final da frase? E ao clicar no clique no cafezinho e o banner ficar visível?
Bem, vamos deixar o ambiente arrumado? Adicionar o logo foi simples:
<span class="icon">{% include icon-pixme.svg %}</span>
Ele precisa estar em
/_includes/
.Por algum motivo que me foge ao conhecimento o SVG precisa estar em uma única linha, caso contrário não renderiza corretamente.
Agora precisamos chamar uma função ao clicar no café: onclick="pixme()"
.
Pronto, precisamos agora declarar o script para fazer isso usando a função pixme
. O
iframe precisa ser identificado, portanto podemos por uma id nele,
que vai ser chamado de pixmeacoffe
.
A ideia é tornar visível com estilo “inline”. Para fazer
isso, podemos invocar o atributo .style
do objeto visutal e adicionar o valor
arbitrariamente como chave valor, como pixmeacoffe.style.display = "block";
:
function pixme() {
const pixmeacoffe = document.getElementById("pixmeacoffe");
pixmeacoffe.style.display = "block";
}
Mas poderia ser melhor, né? Poderia ativar e desativar:
function pixme() {
const pixmeacoffe = document.getElementById("pixmeacoffe");
const displayToggledValue = pixmeacoffe.style.display === "" ? "block" : "";
pixmeacoffe.style.display = displayToggledValue;
}
Agora, isso faz sentido sempre? Não, só quando está com a @media
query
apontando que é pequeno. Para isso, posso validar que o atributo CSS
efetivo de pixmeacoffe
para .float
é "right"
.
Mas para isso primeiro precisa resgatar o estilo computado,
getComputedStyle(element)
:
function pixme() {
const pixmeacoffe = document.getElementById("pixmeacoffe");
const csspmc = getComputedStyle(pixmeacoffe)
if (csspmc.float !== "right") {
const displayToggledValue = pixmeacoffe.style.display === "" ? "block" : "";
pixmeacoffe.style.display = displayToggledValue;
}
}
Ok, bacana. Mas e se eu detectasse que ele aumentou a página?
E usar isso para remover a propriedade do style
inline?
De modo geral, seria fazer a seguinte computação:
const pixmeacoffe = document.getElementById("pixmeacoffe");
const csspmc = getComputedStyle(pixmeacoffe)
if (csspmc.float === "right") {
if (pixmeacoffe.style.display === "block") {
pixmeacoffe.style.display = "";
}
}
Mas se eu pegar esses valores a cada alteração de tamanho não ia ser legal, iria gastar bastante processamento desnecessariamente. Bem, então eu posso criar uma função que resgata isso e memoiza. A ideia é ter uma clausura local de modo que quem requisita só sabe que receberá o valor, não que ele pode ser computado. Uma estratégia para isso é criar uma arrow function auto chamada que retorna uma função que faz a memoização, com os valores a se memoizar na corpo da primeira função:
() => {
let pixmeacoffe
let csspmc
return () => {
if (pixmeacoffe) {
return [pixmeacoffe, csspmc];
}
pixmeacoffe = document.getElementById("pixmeacoffe");
csspmc = getComputedStyle(pixmeacoffe)
return [pixmeacoffe, csspmc];
}
}
Essa função acima retorna uma função que fará a computação
memoizada. Note que pixmeacoffe
e csspmc
estão dentro da
clausura do retorno, mas inacessíveis externamente.
Essa função que cria a clausura não é interessante manter,
o ideal seria que essa função já retornasse imediatamente,
para guardar o retorno. Como fazer isso? Chamando ela imediatamente
ao criar. Envolve com (
parênteses )
a arrow-function
e invoca ela:
(() => {
let pixmeacoffe
let csspmc
return () => {
if (pixmeacoffe) {
return [pixmeacoffe, csspmc];
}
pixmeacoffe = document.getElementById("pixmeacoffe");
csspmc = getComputedStyle(pixmeacoffe)
return [pixmeacoffe, csspmc];
}
})();
Pronto, agora só falta guardar o valor. Podemos guardar na
variável recoverPixmeaCoffe
:
const recoverPixmeaCoffe = (() => {
let pixmeacoffe
let csspmc
return () => {
if (pixmeacoffe) {
return [pixmeacoffe, csspmc];
}
pixmeacoffe = document.getElementById("pixmeacoffe");
csspmc = getComputedStyle(pixmeacoffe)
return [pixmeacoffe, csspmc];
}
})();
Só que por isso no script top-level vai fazer “vazar” essa constante.
Posso colocar dentro de <script>
em um bloco. const
e let
não
vazam para fora do bloco, mas function
vaza. Então posso criar
tudo o que eu quiser escondido e se expor a API da função desejada:
<script>
{
const recoverPixmeaCoffe = (() => {
let pixmeacoffe
let csspmc
return () => {
if (pixmeacoffe) {
return [pixmeacoffe, csspmc];
}
pixmeacoffe = document.getElementById("pixmeacoffe");
csspmc = getComputedStyle(pixmeacoffe)
return [pixmeacoffe, csspmc];
}
})();
function pixme() {
const [pixmeacoffe, csspmc] = recoverPixmeaCoffe();
if (csspmc.float !== "right") {
const displayToggledValue = pixmeacoffe.style.display === "" ? "block" : "";
pixmeacoffe.style.display = displayToggledValue;
}
}
}
</script>
Beleza, com isso consigo expor apenas a função pixme()
para ser
chamada pelo clique no HTML. Ela é ótima que só computa uma vez
e memoiza o resultado. Adequada para usar no
evento de resize:
onresize = event => {
const [pixmeacoffe, csspmc] = recoverPixmeaCoffe();
if (csspmc.float === "right") {
if (pixmeacoffe.style.display === "block") {
pixmeacoffe.style.display = "";
}
}
}
Note que a escolha é sempre analisando o estilo efetivo, pois se for preciso
alterar o breakpoint das @media
queries o que importa está garantido, que é
mudar se exibe condicionado ao tamanho do elemento vs da tela.
SVG
Os demais SVGs que eu tenho para os ícones não são limitados em tamanho, então
eles abrem unbounded no browser. Por exemplo o ícone do GitLab:
icon-gitlab.svg
.
Mas o Daniel Limae me mandou com limitações nisso. Veja
icon-pixme-original.svg
.
São os atributos width
e height
da tag <svg>
.
Outra coisa que me chamou a atenção foi que a tag <path>
tem um atributo
chamado opacity
. Quanto mais opaco (até 1
), mas visível. Opacidade
0
significa perfeitamente transparente.
Esses foram meus primeiros ajustes, remover o width
e height
para que o ícone seja unbounded e deixar a opacidade
padrão, removendo o atributo.
Ok, agora a viewBox
não está batendo perfeitamente encaixada com as demais.
A viewBox
nada mais é do que aquilo que será projetado do SVG. Dentro do
SVG eu posso desenhar em todo o plano cartesiano, mas apenas aquilo que
está dentro do viewBox
de fato será exibido.
A viewBox
dos outros ícones é quadrada, e no icon-pixme-original
ela é
retangular: 0 0 23 27
. Isso significa que vai do ponto (0, 0)
até o ponto
(23, 27)
. Mas deixar ela quadrada não é simplesmente alterar o valor para
0 0 27 27
, pois isso significa que estarei alterando a composição do desenho.
Adicionar os 4 pixels no eixo horizontal pode ser encarado como adicionar 2 pixels a esquerda e 2 pixels a direita, o que mantém a imagem centralizada se ela estivesse centralizada antes (estava). Bem, agora preciso ver como é o desenho…
Abaixo um excerto de como é o SVG do Pix me a Coffe:
<svg viewBox="0 0 23 27" width="23" height="27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="1" d="M20.3747 4.1841L20.7902 4.19132 [...] L5.25937 24.4217L16.6876 24.6202L18.0103 9.73399L4.45443 9.49853Z" fill="#828282"/>
</svg>
Bem vamos lá. A única tag dentro de <svg>
é <path>
. Então é seguro afirmar que os
desenhos estão todos dentro dessa tag. Procurando mais informações a respeito do que
seriam esses detalhes, achei a seguinte
referência inicial.
Na referência aprendi que o atributo d
carrega toda a magia. E que na verdade ele
é uma lista de comandos para desenhar e que eu poderia (para questão de minha
leitura do SVG) separar os comandos em 1 por linha que continuaria um comando válido.
Cada comando é indicado por uma letra distinta dentro do d
. Cada comando recebe
uma quantidade pré-determinada de coordenadas (muitas vezes em múltiplos de x y
).
Por exemplo, pegando o excerto acima:
<svg viewBox="0 0 23 27" width="23" height="27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="1" d="
M20.3747 4.1841
L20.7902 4.19132
[...]
L5.25937 24.4217
L16.6876 24.6202
L18.0103 9.73399
L4.45443 9.49853
Z
" fill="#828282"/>
</svg>
Para “adicionar” os 2 pixels a direita não preciso fazer nada, pois o que está a direita da imagem é vazio.
Mas o para a esquerda da imagem eu preciso atualizar todas as coordenadas x
em +2. No exemplo acima:
onde M
é o comando moveto
que move a caneta para uma posição arbitrária sem tocar,
L
é um comando line
que move a caneta para uma posição arbitrária TOCANDO o papel,
e Z
é o comando de “fechamento” closepath
, como se fosse um L
para o ponto inicial (onde
ponto inicial é determinado como o último ponto para o qual se deu um moveto
).
Podem ler mais
No caso do Pix me a Coffe, ainda tem o comando C
: curva de Bézier. Ela recebe 3
pares de coordenadas. Fica assim após somar 2 pixels de ambos os lados.
<svg viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="1" d="
M22.3747 4.1841
L22.7902 4.19132
[...]
L7.25937 24.4217
L18.6876 24.6202
L20.0103 9.73399
L6.45443 9.49853
Z
" fill="#828282"/>
</svg>
E para uma curva de Bezier:
-C23.011 4.19481 23.2234 4.27664 23.3895 4.42221
+C25.011 4.19481 25.2234 4.27664 25.3895 4.42221
Agora, agora preciso normalizar para caber em 16 x 16 pixels. Isso significa agora que os pontos, a partir da origem, serão encolhidos em:
Pronto, agora eu preciso pegar todos os valores e, individualmente,
multiplicar por 16.0/27.0
. Vamos fazer isso com Ruby?
Inicialmente, vamos deixar os comandos bem formados. Cada comando
em sua própria linha. Só vamos começar a processar no momento
em que encontrar o <path
, não precisa fechar o comando agora,
o objeto de interesse não é o path
. Então, em algum momento teremos
o d="
, que iniciará o processo até encontrar uma linha que começa
com "
.
Essa solução não é boa o suficiente, mas funciona para o caso em minhas
mãos: com apenas um <path>
no SVG.
Para ler um arquivo em Ruby, uma alternativa é usar o IO.readlines(path)
,
que me retorna um array de linhas. Depois disso posso ir escrevendo no array
de output e, ao final, só escrever o output na stdout. Com o array
em mãos, consigo facilmente detectar em qual linha termina com d="
.
Irei processar as coisas de linhas
e jogar o resultado desejado em
novas_linhas
. Então, para imprimir, só fazer puts novas_linhas.join "\n"
.
Primeiro, a prova de conceito, jogar as coisas em novas_linhas
e imprimir:
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
for linha in linhas do
novas_linhas.append linha
end
puts novas_linhas.join "\n"
Bem, isso gerou uma saída que começou assim:
<svg viewBox="0 0 27 27" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M22.3747 4.1841
L22.7902 4.19132
Isso porque o IO.readlines
mantém o fim de linha, e mandei dar o .join "\n"
. Vamos
nos livrar das coisas desnecessárias do começo e final de linha? É só dar um .strip
que se resolve:
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
for linha in linhas do
linha = linha.strip
novas_linhas.append linha
end
puts novas_linhas.join "\n"
Saída:
<svg viewBox="0 0 27 27" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M22.3747 4.1841
L22.7902 4.19132
Pronto, agora sim, bacana. Bem, podemos aplicar uma tratativa especial para a primeira linha logo
e retornar o viewBox
que eu desejo, né? Seria o equivalente a fazer um s/0 0 27 27/0 0 16 16/
.
Para isso a string fornece o método .sub(pattern, replace)
. Tem o método .gsub(pattern, replace)
também que funciona de modo similar, mas fazendo geral na string, enquanto que sub
é só o
primeiro match.
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
for linha in linhas do
linha = linha.strip
novas_linhas.append linha
end
# tratando a primeira linha
novas_linhas[0] = novas_linhas[0].sub("0 0 27 27", "0 0 16 16")
puts novas_linhas.join "\n"
Saída:
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M22.3747 4.1841
L22.7902 4.19132
C23.011 4.19481 23.2234 4.27664 23.3895 4.42221
Ok, agora vamos lidar de modo diferente caso eu tenho encontrado um d="
?
Adicionemos a flag de transformação para indicar que estamos lendo
coisas de dentro da tag <path>
:
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
lendoPath = false
for linha in linhas do
linha = linha.strip
if lendoPath then
novas_linhas.append linha + "PATH DETECTED!" # só para debug memso
else
if linha.end_with? 'd="' then
lendoPath = true
end
novas_linhas.append linha
end
end
# tratando a primeira linha
novas_linhas[0] = novas_linhas[0].sub("0 0 27 27", "0 0 16 16")
puts novas_linhas.join "\n"
Saída:
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M22.3747 4.1841PATH DETECTED!
L22.7902 4.19132PATH DETECTED!
C23.011 4.19481 23.2234 4.27664 23.3895 4.42221PATH DETECTED!
Beleza, tá detectando corretamente que tá iniciando o tratamento do <path>
.
Mas não indica onde terminou, o que pod eser problemático. Pelo jeito que organizamos
aqui, ele termina em uma string que começa com "
:
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
lendoPath = false
for linha in linhas do
linha = linha.strip
if lendoPath then
if linha.start_with? '"' then
lendoPath = false
novas_linhas.append linha
else
novas_linhas.append linha + "PATH DETECTED!" # só para debug memso
end
else
if linha.end_with? 'd="' then
lendoPath = true
end
novas_linhas.append linha
end
end
# tratando a primeira linha
novas_linhas[0] = novas_linhas[0].sub("0 0 27 27", "0 0 16 16")
puts novas_linhas.join "\n"
Saída:
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M22.3747 4.1841PATH DETECTED!
L22.7902 4.19132PATH DETECTED!
C23.011 4.19481 23.2234 4.27664 23.3895 4.42221PATH DETECTED!
[ omitido ]
L18.6876 24.6202PATH DETECTED!
L20.0103 9.73399PATH DETECTED!
L6.45443 9.49853PATH DETECTED!
ZPATH DETECTED!
" fill="#828282"/>
</svg>
Perfeitinho, já detectou todas as linhas que precisam de alteração. Agora, qual vai ser a transformação delas?
Bem, vamos lá. A linha é composta de um caracter de comando seguido
de vários pares de coordenadas, que são indicados como pontos flutuantes
separados por espaços. Para pegar o primeiro caracter, podemos fazer
linha[0]
. Para pegar os outros caracteres, linha[1..]
.
O resto da linha, restoLinha = linha[1..]
é composto de vários
números separados por espaço. O Ruby me fornece o método de
string.split(pattern)
, que permite transformar a string em um
array dado aquele pattern
. Experimentalmente verifiquei que
a quantidade de espaços entre os números é desconsiderado
ao pedir restoLinha.split " "
:
"123 abc def".split " "
# => ["123", "abc", "def"]
"123 abc def".split " "
# => ["123", "abc", "def"]
Pelo jeito como o arquivo foi separado, podemos pegar cada elemento
desse do vetor e transformar em ponto flutuante (método to_f
),
assim teremos eles na versão numérica para calcular:
restoLinha.split(" ").map do |n|
n.to_f
end
Como tenho o número, basta fazer a transformação adequada para sair
de 27 para 16 pixels: multiplicar por 16/27
:
restoLinha.split(" ").map do |n|
n.to_f * (16.0/27.0)
end
E para gerar a linha depois, só juntar com o comando a esquerda e pedir uma junção do array. Algo assim:
linha_transformada = linha[0] + restoLinha.split(" ").map do |n|
n.to_f * (16.0/27.0)
end.join(" ")
novas_linhas.append(linha_transformada)
Agora, na real? Essa variável restoLinha
parece que meio que perdeu
o sentido, posso trocar ela pelo seu dado de origem:
linha_transformada = linha[0] + linha[1..].split(" ").map do |n|
n.to_f * (16.0/27.0)
end.join(" ")
novas_linhas.append(linha_transformada)
Juntando tudo?
linhas = IO.readlines "./icon-pixme-linha-separada.svg"
novas_linhas = []
lendoPath = false
for linha in linhas do
linha = linha.strip
if lendoPath then
if linha.start_with? '"' then
lendoPath = false
novas_linhas.append linha
else
linha_transformada = linha[0] + linha[1..].split(" ").map do |n|
n.to_f * (16.0/27.0)
end.join(" ")
novas_linhas.append(linha_transformada)
end
else
if linha.end_with? 'd="' then
lendoPath = true
end
novas_linhas.append linha
end
end
# tratando a primeira linha
novas_linhas[0] = novas_linhas[0].sub("0 0 27 27", "0 0 16 16")
puts novas_linhas.join "\n"
Saída:
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414">
<path opacity="1" d="
M13.25908148148148 2.4794666666666667
L13.505303703703703 2.4837451851851853
E pronto, transformamos. Bem, na real quase, porque para importar o SVG precisa ser em uma única linha. Basicamente a resposta foi fazer tudo do jeito que estava porém fazer join sem parâmetro:
puts novas_linhas.join
Pronto, tudo sanado e logo transformada. Você pode ver o script ruby completo aqui no
transform.rb
.
Adicionando comportamento de alt-text
Senti uma falta de alt-text ao colocar o cafezinho. Como resolver?
Bem, achei uma fica no CSS Tricks:
tag <title>
dentro do SVG. Ela funciona na prática como alt-text para o
meu fim.
Então, dentro do SVG, adicionei:
<svg ...>
<title>Me compra um café 🥺</title>
<path .../>
</svg>
O ícone ficou assim: .
Para usar aqui para indicar o café achei que seria mais adequado não colocar o alt-text.
O botão do café
Bem, inicialmente o café no meio do texto era só um ícone. Depois
eu senti a necessidade de transformar ele em algo mais interativo
e com isso surgiu a necessidade de tornar ele clicável. Mas
eu fiz isso a nível apenas de colocar um onclick
na tag HTML,
não era algo semântico. Então, que tal, colocar dois elementos
internos idêncos cujo invólucro apareça de acordo com o
@media
query adequada?
Daí surgiu a ideia de colocar tanto um <span>
(para visão completa)
e um <button>
(para indicar ação). No <span>
temos um <a>
envolvendo o <span>
com um href
para #pixmeacoffe
, para focar no
iframe
.
No caso do <button>
focar no <iframe>
foi um pouco mais complicado.
Só faz sentido focar no <iframe>
se eu estiver colocando o <iframe>
para ser visível, e só faz sentido focar depois de mandar ser visível.
Não fiz a transação mais suave possível, mas consegui fazer.
No caso, para simular o “ir para o elemento específico” precisa
manipular o window.location
(ou window.location.href
, são APIs
compatíveis nesse sentido).
Pode ler mais na
documentação da Window.location API
.
Em um primeira momento procurei pelo fragmento da URL e não achei
algo semelhante em window.location
. Não havendo imediatamente, fui atrás
de controlar o fragmento através do href
. Quando havia um elemento
previamente selecionado, bastaria substituir tudo que tivesse depois do
#
por #${id}
. Uma solução que permite essa substituição é usar
uma regex que casa com #
e vai até o fim da string: /#.*$
.
Então trocaria por #pixmeacoffe
. Para manter a generalidade
no JS, posso usar a interpolação de string:
const focusElementByIdIfAlreadyFragment = (id) => {
window.location.href = window.location.href.replace(/#.*$/, `#${id}`);
}
Mas essa substituição só funciona se tiver um #
. E na ausência?
Bem, na ausência basta concatenar o #${id}
:
const focusElementByIdIfNoFragment = (id) => {
window.location.href += `#${id}`;
}
Juntando as duas, basta detectar a presença de um #
na representação
em string do window.location
:
const focusElementById = (id) => {
if (window.location.toString().includes("#")) {
window.location.href = window.location.href.replace(/#.*$/, `#${id}`);
} else {
window.location.href += `#${id}`;
}
}
Porém descobri posteriormente que esse trabalho todo já havia sido
feito, mas em uma API com um nome que eu não esperava:
.hash
.
O que simplifica o código:
const focusElementById = (id) => {
window.location.hash = `#${id}`;
}
Agora, para garantir o sumiço das coisas? Preciso manipular os pontos
de quebra da @media
query:
@media (max-width: 800px) {
.hide-on-mobile {
display: none;
}
}
@media (min-width: 801px) {
.hide-on-full {
display: none;
}
}
Desse jeito, ao colocar a classe hide-on-full
, o elemento não é mostrado
quando se tem uma largura mínima de 801px. E de modo semelhante o
hide-on-mobile
só se esconde até a largura máxima de 800px. Para
consultar a largura da tela você pode usar
window.innerWidth
.
Usei isso para debugar o ponto de quebra padrão do Computaria.