aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/web/split-input.mjs
blob: 294e9a36c9dc811bb0e7228cdf1bd5c28416fbd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Only gesture ownership and transient geometry live here. Layout belongs to CSS.
export function bindSplit({container, handle, readRatio, writeRatio, narrow, stats}) {
  const events=container.ownerDocument.defaultView, abort=new AbortController();
  const listen=(node,type,callback,options={})=>node.addEventListener(type,callback,{...options,signal:abort.signal});
  let drag=null, frame=0, dirty=true, rect=null, handleWidth=0;
  const invalidate=()=>{dirty=true;};
  const observer=new ResizeObserver(invalidate);observer.observe(container);observer.observe(handle);
  function measure() {
    rect=container.getBoundingClientRect();const h=handle.getBoundingClientRect();
    handleWidth=h.width;stats.geometryReads+=2;dirty=false;return h;
  }
  function flush() {
    if(frame){cancelAnimationFrame(frame);frame=0;}
    if(!drag || drag.x===null)return;
    if(dirty)measure();
    const width=rect.width-handleWidth;
    if(width>0)writeRatio(100*(drag.x-rect.left-handleWidth/2-drag.offset)/width,'pointer');
    drag.x=null;
  }
  function finish(cancel) {
    if(!drag)return;
    if(cancel){if(frame)cancelAnimationFrame(frame);frame=0;}else flush();
    const previous=drag;drag=null;
    if(handle.hasPointerCapture(previous.id))handle.releasePointerCapture(previous.id);
    handle.classList.remove('dragging');
    if(cancel)writeRatio(previous.before,'cancel');
  }
  listen(handle,'pointerdown',event=>{
    if(event.button!==0 || drag || narrow.matches)return;
    const h=measure();if(rect.width<=handleWidth)return;
    event.preventDefault();handle.focus();
    handle.setPointerCapture(event.pointerId);
    drag={id:event.pointerId,type:event.pointerType,before:readRatio(),offset:event.clientX-(h.left+h.width/2),x:null};
    handle.classList.add('dragging');
  });
  listen(handle,'pointermove',event=>{
    if(!drag || drag.id!==event.pointerId)return;
    event.preventDefault();drag.x=event.clientX;
    if(!frame)frame=requestAnimationFrame(()=>{frame=0;flush();});
  });
  listen(events,'pointerup',event=>{if(drag?.id===event.pointerId)finish(false);},{capture:true});
  listen(events,'pointercancel',event=>{if(drag?.id===event.pointerId)finish(true);},{capture:true});
  listen(events,'mouseup',event=>{if(event.button===0&&drag?.type==='mouse')finish(false);},{capture:true});
  listen(handle,'lostpointercapture',event=>{if(event.target===handle&&drag?.id===event.pointerId)finish(true);});
  listen(events,'blur',()=>finish(true));
  listen(container.ownerDocument,'visibilitychange',()=>{if(container.ownerDocument.hidden)finish(true);});
  listen(events,'resize',invalidate);listen(events,'scroll',invalidate,{capture:true,passive:true});
  listen(narrow,'change',()=>{invalidate();if(narrow.matches)finish(true);});
  listen(handle,'keydown',event=>{
    if(event.key==='Escape'){event.preventDefault();finish(true);return;}
    if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;
    event.preventDefault();finish(true);
    writeRatio(event.key==='Home'?20:event.key==='End'?80:readRatio()+(event.key==='ArrowLeft'?-1:1)*(event.shiftKey?10:1),'keyboard');
  });
  return Object.freeze({cancel:()=>finish(true),isDragging:()=>drag!==null,
    dispose(){finish(true);observer.disconnect();abort.abort();}});
}