181 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			181 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import React from "react";
 | 
						|
 | 
						|
type StateArgs = {done?:boolean, open?:boolean};
 | 
						|
type StateObj = {done:boolean, open:boolean};
 | 
						|
type StateBinding = [state:StateObj, update:(args:StateArgs)=>void];
 | 
						|
 | 
						|
const CTX = React.createContext([{done:true, open:false}, (args)=>{}] as StateBinding);
 | 
						|
 | 
						|
export const Group =(props:{children:React.JSX.Element|React.JSX.Element[]})=>
 | 
						|
{
 | 
						|
    const [stateGet, stateSet] = React.useState({done:true, open:false} as StateObj);
 | 
						|
    return <CTX.Provider value={[stateGet, (args:StateArgs)=> stateSet({...stateGet, ...args})]}>{props.children}</CTX.Provider>;
 | 
						|
};
 | 
						|
 | 
						|
export const Menu =(props:{children:React.JSX.Element|React.JSX.Element[]})=>
 | 
						|
{
 | 
						|
    const [stateGet, stateSet] = React.useContext(CTX);
 | 
						|
    const refElement:React.MutableRefObject<HTMLElement|null> = React.useRef( null );
 | 
						|
    const refControl:React.MutableRefObject<CollapseControls|null> = React.useRef( null );
 | 
						|
 | 
						|
    type MenuClassStates = {Keep:string, Open:string, Shut:string, Move:string, Exit:string};
 | 
						|
    const base = `relative transition-all border(8 black) overflow-hidden`;
 | 
						|
    const Classes:MenuClassStates =
 | 
						|
    {
 | 
						|
      Shut: `${base} h-0    top-0  w-1/2  duration-300`,
 | 
						|
      Open: `${base} h-auto top-8  w-full  duration-1000`,
 | 
						|
      lol: `${base} h-auto top-36 bg-yellow-500  w-2/3  duration-700`,
 | 
						|
    };
 | 
						|
    const Window = window as {TwindInst?:(c:string)=>string};
 | 
						|
    if(Window.TwindInst)
 | 
						|
    {
 | 
						|
      for(let stateName in Classes)
 | 
						|
      {
 | 
						|
        Classes[stateName as keyof MenuClassStates] = Window.TwindInst(Classes[stateName as keyof MenuClassStates]);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const initialKey = stateGet.open ? "Open" : "Shut";
 | 
						|
 | 
						|
    React.useEffect(()=> {
 | 
						|
      refElement.current?.setAttribute("style", "");
 | 
						|
      refControl.current = refElement.current && Collapser(refElement.current, initialKey, Classes);
 | 
						|
    }, []);
 | 
						|
    React.useEffect(()=> {refControl.current && refControl.current(initialKey, ()=>stateSet({done:true}))}, [stateGet.open])
 | 
						|
 | 
						|
    return <div ref={refElement as React.Ref<HTMLDivElement>} class={Classes.Shut} style="transition:none;">
 | 
						|
      { (!stateGet.open && stateGet.done) ? null : props.children}
 | 
						|
    </div>;
 | 
						|
};
 | 
						|
 | 
						|
export const Button =()=>
 | 
						|
{
 | 
						|
    const [stateGet, stateSet] = React.useContext(CTX);
 | 
						|
    return <>
 | 
						|
        <p>{JSON.stringify(stateGet)}</p>
 | 
						|
        <button class="px-10 py-2 bg-red-500 text-white" onClick={e=>stateSet({open:true,  done:false})}>Open</button>
 | 
						|
        <button class="px-10 py-2 bg-red-500 text-white" onClick={e=>stateSet({open:false, done:false})}>Close</button>
 | 
						|
    </>;
 | 
						|
};
 | 
						|
 | 
						|
type StyleSize = [classes:string, width:number, height:number];
 | 
						|
type StylePack = Record<string, string>;
 | 
						|
type StyleCalc = Record<string, StyleSize>;
 | 
						|
const StyleCalc =(inElement:HTMLElement, inClasses:StylePack)=>
 | 
						|
{
 | 
						|
  const initialStyle = inElement.getAttribute("style")||"";
 | 
						|
  const initialClass = inElement.getAttribute("class")||"";
 | 
						|
  const output = {} as StyleCalc;
 | 
						|
 | 
						|
  inElement.setAttribute("style", `transition: none; overflow: hidden;`);
 | 
						|
  Object.entries(inClasses).forEach(([key, value])=>
 | 
						|
  {
 | 
						|
    inElement.setAttribute("class", value);
 | 
						|
    output[key] =  [value, inElement.offsetWidth, inElement.offsetHeight];
 | 
						|
  });
 | 
						|
  inElement.setAttribute("class", initialClass);
 | 
						|
  inElement.offsetHeight;
 | 
						|
  inElement.setAttribute("style", initialStyle);
 | 
						|
 | 
						|
  return output;
 | 
						|
};
 | 
						|
 | 
						|
type DoneCallback =(openState:boolean)=>void;
 | 
						|
export type CollapseControls =(inOpen?:string, inDone?:DoneCallback)=>void;
 | 
						|
export function Collapser(inElement:HTMLElement, initialState:string, library:Record<string, string>)
 | 
						|
{
 | 
						|
    let userDone:DoneCallback = (openState) => {};
 | 
						|
    let userMode = initialState;
 | 
						|
    let frameRequest = 0;
 | 
						|
    let inTransition = false;
 | 
						|
    let measurements:StyleCalc;
 | 
						|
    const transitions:Set<string> = new Set();
 | 
						|
 | 
						|
    const run = (inEvent:TransitionEvent)=>
 | 
						|
    {
 | 
						|
      console.log("+", inEvent.propertyName);
 | 
						|
      (inEvent.target == inElement) && transitions.add(inEvent.propertyName);
 | 
						|
    };
 | 
						|
    const end = (inEvent:TransitionEvent)=>
 | 
						|
    {
 | 
						|
      console.log("-", inEvent.propertyName);
 | 
						|
      if (inEvent.target == inElement)
 | 
						|
      {
 | 
						|
        transitions.delete(inEvent.propertyName);
 | 
						|
        if(transitions.size === 0)
 | 
						|
        {
 | 
						|
          console.log("--done--", userMode);
 | 
						|
          inElement.setAttribute("style", "transition:none;");
 | 
						|
          inElement.clientHeight;
 | 
						|
          frameRequest = requestAnimationFrame(()=>
 | 
						|
          {
 | 
						|
            inElement.setAttribute("style", "");
 | 
						|
            inTransition = false;
 | 
						|
            userDone(userMode);   
 | 
						|
          });       
 | 
						|
        }
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    const child =(e:CustomEvent)=>
 | 
						|
    {
 | 
						|
      if(e.target == inElement || !inTransition){ return; }
 | 
						|
      console.log("resize", e.detail);
 | 
						|
      const oldWidth = inElement.offsetWidth;
 | 
						|
      const oldHeight = inElement.offsetHeight;
 | 
						|
      //inElement.style.overflow = "hidden";
 | 
						|
      inElement.style.width  = oldWidth  + e.detail[0] + "px";
 | 
						|
      inElement.style.height = oldHeight + e.detail[1] + "px";
 | 
						|
    }
 | 
						|
 | 
						|
    inElement.addEventListener("transitionend", end);
 | 
						|
    inElement.addEventListener("transitionrun", run);
 | 
						|
    inElement.addEventListener("childresize", child);
 | 
						|
 | 
						|
    return function(inState:string, inDone)
 | 
						|
    {
 | 
						|
      cancelAnimationFrame(frameRequest);
 | 
						|
 | 
						|
      if(arguments.length)
 | 
						|
      {
 | 
						|
        if(!library[inState]){ return; }
 | 
						|
 | 
						|
        userDone = inDone|| ((m)=>{}) as DoneCallback;
 | 
						|
        userMode = inState;
 | 
						|
 | 
						|
        if(!inTransition)
 | 
						|
        {
 | 
						|
          measurements = StyleCalc(inElement, library);
 | 
						|
          console.log("measurements taken", measurements)
 | 
						|
        }
 | 
						|
 | 
						|
        if(measurements)
 | 
						|
        {
 | 
						|
            const [classes, width, height] = measurements[inState] as StyleSize;
 | 
						|
            const oldWidth = inElement.offsetWidth;
 | 
						|
            const oldHeight = inElement.offsetHeight;
 | 
						|
            //inElement.style.overflow = "hidden";
 | 
						|
            inElement.style.width  = oldWidth  + "px";
 | 
						|
            inElement.style.height = oldHeight + "px";
 | 
						|
            inTransition = true;
 | 
						|
            console.log(`from: {${inElement.offsetWidth} ${inElement.offsetHeight}}`);
 | 
						|
 | 
						|
            inElement.dispatchEvent(new CustomEvent("childresize", {bubbles:true, detail:[width-oldWidth, height-oldHeight]}))
 | 
						|
 | 
						|
            frameRequest = requestAnimationFrame(()=>
 | 
						|
            {
 | 
						|
              inElement.style.height = height + "px";
 | 
						|
              inElement.style.width = width + "px";
 | 
						|
              inElement.className = classes;
 | 
						|
              console.log(`  to: {${width} ${height}}`)
 | 
						|
            });
 | 
						|
        }
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
        inElement.removeEventListener("transitionrun", run);
 | 
						|
        inElement.removeEventListener("transitionend", end);
 | 
						|
      }
 | 
						|
    } as CollapseControls;
 | 
						|
 | 
						|
} |