diff --git a/example/dyn-test.tsx b/example/dyn-test.tsx new file mode 100644 index 0000000..63ca671 --- /dev/null +++ b/example/dyn-test.tsx @@ -0,0 +1,15 @@ + +import * as Util from "@able/"; +import * as React from "react"; +import {createElement} from "react/client"; + +import('react').then((module) => { + console.log(module); +}); + + +function unimport(n:number) +{ + return n; +} +unimport(123) \ No newline at end of file diff --git a/iso-menu.tsx b/iso-menu.tsx new file mode 100644 index 0000000..bf2ebe3 --- /dev/null +++ b/iso-menu.tsx @@ -0,0 +1,224 @@ +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 stateSet({...stateGet, ...args})]}>{props.children}; +}; + +export const Menu =(props:{children:React.JSX.Element|React.JSX.Element[]})=> +{ + const [stateGet, stateSet] = React.useContext(CTX); + const refElement:React.MutableRefObject = React.useRef( null ); + const refControl:React.MutableRefObject = React.useRef( null ); + const refInitial:React.MutableRefObject = React.useRef(true); + + 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-700`, + 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]); + } + } + + React.useEffect(()=> + { + refControl.current = refElement.current && Collapser(refElement.current, stateGet.open ? "Open" : "Shut", Classes); + } + , []); + React.useEffect(()=> + { + (!refInitial.current && refControl.current) && refControl.current(stateGet.open ? "Open" : "Shut", ()=>stateSet({done:true})); + refInitial.current = false; + } + , [stateGet.open]); + + useAway(refElement, (e)=>stateSet({open:false, done:false}) ); + + return
} class={Classes.Shut}> + { (!stateGet.open && stateGet.done) ? null : props.children} +
; +}; + +export const Button =()=> +{ + const [stateGet, stateSet] = React.useContext(CTX); + return <> +

{JSON.stringify(stateGet)}

+ + + ; +}; + +type Handler = (e:MouseEvent)=>void +const Refs:Map> = new Map(); +function isHighest(inElement:HTMLElement, inSelection:HTMLElement[]) +{ + let currentNode = inElement; + while (currentNode != document.body) + { + currentNode = currentNode.parentNode as HTMLElement; + if(currentNode.hasAttribute("data-use-away") && inSelection.includes(currentNode)) + { + return false; + } + } + return true; +} +window.innerWidth && document.addEventListener("click", e=> +{ + const path = e.composedPath(); + const away:HTMLElement[] = []; + + Refs.forEach( (handlerRef, element)=> + { + if(!path.includes(element) && handlerRef.current) + { + away.push(element); + } + }); + + away.forEach((element)=> + { + if(isHighest(element, away)) + { + const handler = Refs.get(element); + handler?.current && handler.current(e); + } + }); + +} +, true); +const useAway =(inRef:React.Ref, handleAway:Handler)=> +{ + const refHandler:React.MutableRefObject = React.useRef(handleAway); + refHandler.current = handleAway; + + React.useEffect(()=> + { + if(inRef.current) + { + inRef.current.setAttribute("data-use-away", "0"); + Refs.set(inRef.current, refHandler); + } + return ()=> inRef.current && Refs.delete(inRef.current); + } + , []); +}; + +type StyleSize = [classes:string, width:number, height:number]; +type StylePack = Record; +type StyleCalc = Record; +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;`); + Object.entries(inClasses).forEach(([key, value])=> + { + inElement.setAttribute("class", value); + output[key] = [value, inElement.offsetWidth, inElement.offsetHeight]; + }); + inElement.setAttribute("class", initialClass); + inElement.offsetHeight; // this has be be exactly here + inElement.setAttribute("style", initialStyle); + + return output; +}; + +type DoneCallback =(inState:string)=>void; +export type CollapseControls =(inOpen?:string, inDone?:DoneCallback)=>void; +export function Collapser(inElement:HTMLElement, initialState:string, library:Record) +{ + let userDone:DoneCallback = (openState) => {}; + let userMode = initialState; + let frameRequest = 0; + let inTransition = false; + let measurements:StyleCalc; + const transitions:Set = new Set(); + + const run = (inEvent:TransitionEvent)=> (inEvent.target == inElement) && transitions.add(inEvent.propertyName); + const end = (inEvent:TransitionEvent)=> + { + if (inEvent.target == inElement) + { + transitions.delete(inEvent.propertyName); + if(transitions.size === 0) + { + measurements = StyleCalc(inElement, library); + const [, w, h] = measurements[userMode]; + if(inElement.offsetHeight != h || inElement.offsetWidth != w) + { + anim(userMode, userDone); + } + else + { + inElement.setAttribute("style", ""); + inTransition = false; + userDone(userMode); + } + } + } + }; + const anim = 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); + } + + if(measurements) + { + const [classes, width, height] = measurements[inState] as StyleSize; + const oldWidth = inElement.offsetWidth; + const oldHeight = inElement.offsetHeight; + inElement.style.width = oldWidth + "px"; + inElement.style.height = oldHeight + "px"; + inTransition = true; + + frameRequest = requestAnimationFrame(()=> + { + inElement.style.height = height + "px"; + inElement.style.width = width + "px"; + inElement.className = classes; + }); + } + } + else + { + inElement.removeEventListener("transitionrun", run); + inElement.removeEventListener("transitionend", end); + } + } as CollapseControls; + + inElement.addEventListener("transitionend", end); + inElement.addEventListener("transitionrun", run); + + return anim; +} diff --git a/run-serve.tsx b/run-serve.tsx index f67320a..2ca9f30 100644 --- a/run-serve.tsx +++ b/run-serve.tsx @@ -55,7 +55,15 @@ let Configuration:Configuration = Allow: "*", Reset: "/clear-cache", Spoof: "/@able", - Serve(inReq, inURL, inExt, inMap, inConfig){}, + async Serve(inReq, inURL, inExt, inMap, inConfig) + { + if( inReq.headers.get("user-agent")?.startsWith("Deno") || inURL.searchParams.get("deno") ) + { + const file = await fetch(inConfig.Proxy + inURL.pathname); + const text = await file.text(); + return new Response(Transpile.Patch(text, "deno-"+inURL.pathname, inMap), {headers:{"content-type":"application/javascript"}} ); + } + }, Remap: (inImports, inConfig)=> { const reactURL = inImports["react"]; @@ -89,6 +97,9 @@ let Configuration:Configuration = }, SWCOp: { + env:{ + dynamicImport:false + }, sourceMaps: false, minify: true, jsc: @@ -127,6 +138,77 @@ export const Transpile = ImportMapReload(); return size; }, + /** + * Converts dynamic module imports in to static, also can resolve paths with an import map + */ + Patch(inText:string, inKey:string|false = false, inMap?:DenoConfig) + { + + if(inKey) + { + const check = this.Cache.get(inKey); + if(check) + { + return check; + } + } + + const remap = inMap ? (inPath:string)=> + { + const match = inMap.imports[inPath]; + if(match) + { + return match; + } + else if(inPath.includes("/")) + { + let bestKey = ""; + let bestLength = 0; + Object.keys(inMap.imports).forEach((key, i, arr)=> + { + if(key.endsWith("/") && inPath.startsWith(key) && key.length > bestLength) + { + bestKey = key; + bestLength = bestLength; + } + }); + if(bestKey) + { + return inMap.imports[bestKey]+inPath.substring(bestKey.length); + } + } + return inPath; + } + : (inPath:string)=>inPath; + let match, regex; + let convertedBody = inText; + + // remap static imports + regex = /from\s+(['"`])(.*?)\1/g; + while ((match = regex.exec(inText))) + { + const importStatement = match[0]; + const importPath = match[2]; + convertedBody = convertedBody.replace(importStatement, `from "${remap(importPath)}"`); + } + + // convert dynamic imports into static (to work around deno deploy) + const staticImports = []; + regex = /(?