refactor: collapsible drawing form with add/discard UX

This commit is contained in:
Lewis Wynne 2026-04-10 02:24:55 +01:00
parent 68080a1455
commit ab6dd05b6e

View file

@ -41,46 +41,67 @@ pub fn render_form(config: &Config) -> String {
let drawing_section = if config.enable_drawings { let drawing_section = if config.enable_drawings {
format!( format!(
r##"<label class="guestbook-label">{label}</label> r##"<span class="guestbook-drawing-wrap"><span class="guestbook-drawing-inline"><a href="#" class="guestbook-drawing-toggle">add a drawing</a></span>
<canvas class="guestbook-canvas" width="{w}" height="{h}"></canvas> <span class="guestbook-drawing-content"></span></span><input type="hidden" name="drawing"><script>(function(){{
<span class="guestbook-canvas-tools"><span class="guestbook-swatch active" data-c="#000" style="background:#000"></span><span class="guestbook-swatch" data-c="#e03131" style="background:#e03131"></span><span class="guestbook-swatch" data-c="#2f9e44" style="background:#2f9e44"></span><span class="guestbook-swatch" data-c="#1971c2" style="background:#1971c2"></span> <input type="range" class="guestbook-size-slider" min="1" max="20" value="5"> | <a href="#" class="guestbook-undo">undo</a> | <a href="#" class="guestbook-canvas-reset">reset</a></span><input type="hidden" name="drawing"><script>(function(){{ var inl=document.querySelector('.guestbook-drawing-inline'),
var c=document.querySelector('.guestbook-canvas'), cnt=document.querySelector('.guestbook-drawing-content'),
x=c.getContext('2d'), hid=document.querySelector('[name=drawing]'),
d=false,lx,ly,h=[],col='#000',sz=5; c,x,d=false,lx,ly,h=[],col='#000',sz=5;
x.strokeStyle=col;x.lineWidth=sz;x.lineCap='round';x.lineJoin='round'; function pos(e){{var r=c.getBoundingClientRect();return[e.clientX-r.left,e.clientY-r.top]}}
function pos(e){{var r=c.getBoundingClientRect(); function tpos(e){{var r=c.getBoundingClientRect(),t=e.touches[0];return[t.clientX-r.left,t.clientY-r.top]}}
return[e.clientX-r.left,e.clientY-r.top]}} function save(){{if(h.length>=20)h.shift();h.push(x.getImageData(0,0,c.width,c.height))}}
function tpos(e){{var r=c.getBoundingClientRect(),t=e.touches[0];
return[t.clientX-r.left,t.clientY-r.top]}}
function save(){{if(h.length>=20)h.shift();
h.push(x.getImageData(0,0,c.width,c.height))}}
function dot(px,py){{x.beginPath();x.arc(px,py,sz/2,0,Math.PI*2);x.fillStyle=col;x.fill()}} function dot(px,py){{x.beginPath();x.arc(px,py,sz/2,0,Math.PI*2);x.fillStyle=col;x.fill()}}
function bindCanvas(){{
x=c.getContext('2d');x.strokeStyle=col;x.lineWidth=sz;x.lineCap='round';x.lineJoin='round';
c.addEventListener('mousedown',function(e){{save();d=true;var p=pos(e);lx=p[0];ly=p[1];dot(lx,ly)}}); c.addEventListener('mousedown',function(e){{save();d=true;var p=pos(e);lx=p[0];ly=p[1];dot(lx,ly)}});
c.addEventListener('mousemove',function(e){{if(!d)return;var p=pos(e); c.addEventListener('mousemove',function(e){{if(!d)return;var p=pos(e);x.beginPath();x.moveTo(lx,ly);x.lineTo(p[0],p[1]);x.stroke();lx=p[0];ly=p[1]}});
x.beginPath();x.moveTo(lx,ly);x.lineTo(p[0],p[1]);x.stroke();lx=p[0];ly=p[1]}});
c.addEventListener('mouseup',function(){{d=false}}); c.addEventListener('mouseup',function(){{d=false}});
c.addEventListener('mouseleave',function(){{d=false}}); c.addEventListener('mouseleave',function(){{d=false}});
c.addEventListener('touchstart',function(e){{e.preventDefault();save();var p=tpos(e);lx=p[0];ly=p[1];dot(lx,ly)}}); c.addEventListener('touchstart',function(e){{e.preventDefault();save();var p=tpos(e);lx=p[0];ly=p[1];dot(lx,ly)}});
c.addEventListener('touchmove',function(e){{e.preventDefault();var p=tpos(e); c.addEventListener('touchmove',function(e){{e.preventDefault();var p=tpos(e);x.beginPath();x.moveTo(lx,ly);x.lineTo(p[0],p[1]);x.stroke();lx=p[0];ly=p[1]}});
x.beginPath();x.moveTo(lx,ly);x.lineTo(p[0],p[1]);x.stroke();lx=p[0];ly=p[1]}}); }}
var sw=document.querySelectorAll('.guestbook-swatch'); function showCanvas(){{
sw.forEach(function(s){{s.addEventListener('click',function(){{ inl.innerHTML='';
sw.forEach(function(el){{el.classList.remove('active')}}); var sw=[{{c:'#000'}},{{c:'#e03131'}},{{c:'#2f9e44'}},{{c:'#1971c2'}}];
s.classList.add('active');col=s.getAttribute('data-c');x.strokeStyle=col}})}}); sw.forEach(function(s,i){{
document.querySelector('.guestbook-size-slider').addEventListener('input',function(e){{ var sp=document.createElement('span');
sz=parseInt(e.target.value);x.lineWidth=sz}}); sp.className='guestbook-swatch'+(i===0?' active':'');
document.querySelector('.guestbook-undo').addEventListener('click',function(e){{ sp.setAttribute('data-c',s.c);sp.style.background=s.c;
e.preventDefault();if(h.length)x.putImageData(h.pop(),0,0)}}); sp.addEventListener('click',function(){{
document.querySelector('.guestbook-canvas-reset').addEventListener('click',function(e){{ inl.querySelectorAll('.guestbook-swatch').forEach(function(el){{el.classList.remove('active')}});
e.preventDefault();h=[];x.clearRect(0,0,c.width,c.height)}}); sp.classList.add('active');col=s.c;x.strokeStyle=col;
}});
inl.appendChild(sp);
}});
var sl=document.createElement('input');
sl.type='range';sl.className='guestbook-size-slider';sl.min='1';sl.max='20';sl.value='5';
sl.addEventListener('input',function(){{sz=parseInt(sl.value);x.lineWidth=sz}});
inl.appendChild(document.createTextNode(' '));inl.appendChild(sl);
inl.appendChild(document.createTextNode(' | '));
var undo=document.createElement('a');undo.href='#';undo.textContent='undo';
undo.addEventListener('click',function(e){{e.preventDefault();if(h.length)x.putImageData(h.pop(),0,0)}});
inl.appendChild(undo);
inl.appendChild(document.createTextNode(' | '));
var disc=document.createElement('a');disc.href='#';disc.textContent='discard';
disc.addEventListener('click',function(e){{
e.preventDefault();h=[];col='#000';sz=5;d=false;cnt.innerHTML='';hid.value='';setInit();
}});
inl.appendChild(disc);
c=document.createElement('canvas');c.className='guestbook-canvas';c.width={w};c.height={h};
cnt.innerHTML='';cnt.appendChild(c);bindCanvas();
c.closest('form').addEventListener('submit',function(){{ c.closest('form').addEventListener('submit',function(){{
var px=new Uint32Array(x.getImageData(0,0,c.width,c.height).data.buffer); var px=new Uint32Array(x.getImageData(0,0,c.width,c.height).data.buffer);
if(px.some(function(v){{return v!==0}})){{ if(px.some(function(v){{return v!==0}})){{hid.value=c.toDataURL('image/png')}}
c.closest('form').querySelector('[name=drawing]').value=c.toDataURL('image/png');
}}
}}); }});
}}
function setInit(){{
inl.innerHTML='';
var a=document.createElement('a');a.href='#';a.textContent='add a drawing';
a.addEventListener('click',function(e){{e.preventDefault();showCanvas()}});
inl.appendChild(a);
}}
inl.querySelector('a').addEventListener('click',function(e){{e.preventDefault();showCanvas()}});
}})();</script>"##, }})();</script>"##,
label = config.label_drawing,
w = config.canvas_width, w = config.canvas_width,
h = config.canvas_height, h = config.canvas_height,
) )
@ -97,8 +118,7 @@ pub fn render_form(config: &Config) -> String {
<label class="guestbook-label">{label_message}</label> <label class="guestbook-label">{label_message}</label>
<textarea class="guestbook-textarea" name="message" style="width:{tw}px;height:{th}px" required></textarea> <textarea class="guestbook-textarea" name="message" style="width:{tw}px;height:{th}px" required></textarea>
{captcha_section} {captcha_section}
{drawing_section}<input name="url" style="display:none" tabindex="-1" autocomplete="off"> {drawing_section}<input name="url" style="display:none" tabindex="-1" autocomplete="off"><button class="guestbook-button" type="submit">{button}</button>
<button class="guestbook-button" type="submit">{button}</button>
</form>"#, </form>"#,
prompt = config.form_prompt, prompt = config.form_prompt,
label_name = config.label_name, label_name = config.label_name,
@ -430,21 +450,20 @@ mod tests {
} }
#[test] #[test]
fn test_render_form_shows_canvas_when_drawings_enabled() { fn test_render_form_shows_drawing_toggle_when_enabled() {
let mut config = test_config(); let mut config = test_config();
config.enable_drawings = true; config.enable_drawings = true;
let form = render_form(&config); let form = render_form(&config);
assert!(form.contains("<canvas")); assert!(form.contains("add a drawing"));
assert!(form.contains("class=\"guestbook-canvas\"")); assert!(form.contains("guestbook-drawing-toggle"));
assert!(form.contains("name=\"drawing\"")); assert!(form.contains("name=\"drawing\""));
assert!(form.contains("reset"));
} }
#[test] #[test]
fn test_render_form_hides_canvas_when_drawings_disabled() { fn test_render_form_hides_drawing_when_disabled() {
let config = test_config(); let config = test_config();
let form = render_form(&config); let form = render_form(&config);
assert!(!form.contains("<canvas")); assert!(!form.contains("add a drawing"));
assert!(!form.contains("name=\"drawing\"")); assert!(!form.contains("name=\"drawing\""));
} }