|
1 | 1 | import {locate,resolve,modularise,agent,virtual,window,jsdom,fetch,digest} from "./Blik_2023_interface.js";
|
2 |
| - import {note,wait,observe,provide,collect,slip,infer,either,each,pass,tether,buffer,differ,compose,revert,record,combine,wether,swap,compound,something,string,basic,functor,defined,simple,exit,route,drop,crop,same,major,binary,is,has,not,numeric,array,pdflike,when,expect,generator,clock,fields} from "./Blik_2023_inference.js"; |
| 2 | + import {note,wait,observe,provide,collect,slip,infer,either,each,pass,tether,buffer,differ,compose,revert,record,combine,whether,swap,compound,something,string,basic,functor,defined,simple,exit,route,drop,crop,same,major,binary,is,has,not,numeric,array,pdflike,when,expect,generator,clock,heritage} from "./Blik_2023_inference.js"; |
3 | 3 | import {search,merge,prune,extract} from "./Blik_2023_search.js";
|
4 | 4 | import {serialize,proceduralize,mime,data} from "./Blik_2023_meta.js";
|
5 | 5 | import layout,{color} from "./Blik_2023_layout.js";
|
|
106 | 106 | {fields=[fields].flat().filter(Boolean);
|
107 | 107 | if(fields.length)
|
108 | 108 | return Object.fromEntries(fields.map(field=>
|
109 |
| - [field,compose(infer("getAttribute",field),wether(isNaN,crop(1),Number))(node)])); |
| 109 | + [field,compose(infer("getAttribute",field),whether(isNaN,crop(1),Number))(node)])); |
110 | 110 | node.normalize();
|
111 | 111 | if(node.documentElement)
|
112 | 112 | return demarkup(node.documentElement);
|
|
160 | 160 | :[entry,id];
|
161 | 161 | if(!defined(value))
|
162 | 162 | return;
|
163 |
| - let type=wether([is(Date),is(Set),either(array,compound),either(binary,infer("match",/^(true|false)$/))] |
| 163 | + let type=whether([is(Date),is(Set),either(array,compound),either(binary,infer("match",/^(true|false)$/))] |
164 | 164 | ,...["date","radio","select","checkbox","text"].map(type=>swap(type)))(value);
|
165 | 165 | return JSON.parse(JSON.stringify(
|
166 | 166 | {for:id,title:id
|
|
230 | 230 | :parse(resource,semiotics);
|
231 | 231 | };
|
232 | 232 |
|
233 |
| - export function defer(form) |
| 233 | + export function dispatch(form) |
234 | 234 | {return document(
|
235 | 235 | {"img":
|
236 | 236 | {onload:"!function expect(){setTimeout(tick=>(typeof dispatch=='undefined'?expect:dispatch).call(this,event),500)}.call(this)"
|
|
341 | 341 | let selectors=name?{id:"#",classList:"."}:{id:'#',class:'.',classed:'.'};
|
342 | 342 | let attributes=Array.from(node.attributes||[]).filter(({name})=>
|
343 | 343 | !["id","class","style"].includes(name.toLowerCase())&&!name.startsWith("on")).map(({name,value})=>
|
344 |
| - "["+[name,value].join("=")+"]").join(""); |
| 344 | + "["+[name,value].join("='")+"']").join(""); |
345 | 345 | return name+Object.entries(selectors).flatMap(([attribute,selector])=>
|
346 | 346 | [attribute==="classList"?Array.from(node[attribute]):["class","className"].includes(attribute)?node[attribute]?.split(' '):node[attribute]].flat().filter(value=>
|
347 | 347 | string(value)&&value?.length).map(value=>selector+value)).join('')+attributes;
|
|
526 | 526 | {let {width:limitx,height:limity}=this.parentNode.getBoundingClientRect();
|
527 | 527 | let {left,top,width,height}=this.getBoundingClientRect();
|
528 | 528 | let overflow=[-left,-top,left+width-limitx,top+height-limity];
|
529 |
| - overflow.forEach(wether(major(0),(overflow,index)=> |
| 529 | + overflow.forEach(whether(major(0),(overflow,index)=> |
530 | 530 | this.style[index%2?"top":"left"]=Number(this.style[index%2?"top":"left"].match(/\d+/)?.[0])+overflow*(index<2||-1)+"px",infer()));
|
531 | 531 | };
|
532 | 532 |
|
|
659 | 659 | }
|
660 | 660 | }
|
661 | 661 | });
|
662 |
| - buffer(capture)(author,"/feed"); |
| 662 | + capture.call(author,"/feed"); |
663 | 663 | return author;
|
664 | 664 | };
|
665 | 665 |
|
|
703 | 703 | ,style
|
704 | 704 | ,span:
|
705 | 705 | [{canvas:await buffer
|
706 |
| -(compose(icon&&fetch,digest,wether(is(Blob),compose(image,canvas),infer())) |
| 706 | +(compose(icon&&fetch,digest,whether(is(Blob),compose(image,canvas),infer())) |
707 | 707 | ,swap({role:"img"})
|
708 | 708 | )(icon)
|
709 | 709 | }
|
|
758 | 758 | )(syntax);
|
759 | 759 | };
|
760 | 760 |
|
761 |
| - export function activate(event) |
| 761 | + export var actions= |
| 762 | + {body: |
| 763 | + {load(event) |
762 | 764 | {if(this!==event.target.body)
|
763 | 765 | // ignore propagated load events.
|
764 | 766 | return;
|
765 | 767 | this.querySelectorAll("canvas[role=img]").forEach(canvas=>
|
766 | 768 | canvas.dispatchEvent(new canvas.ownerDocument.defaultView.Event("contextrestored",{bubbles:true})));
|
767 | 769 | this.querySelectorAll("[actions]").forEach(scope=>
|
768 |
| - capture(scope,scope.getAttribute("actions"))|| |
| 770 | + capture.call(scope,scope.getAttribute("actions"))|| |
769 | 771 | scope.dispatchEvent(new scope.ownerDocument.defaultView.Event("contextrestored")));
|
770 |
| -}; |
| 772 | + socket("/peer"); |
| 773 | +},popstate(event) |
| 774 | +{//note(event);this.document.forms[0]?.dispatchEvent(new Event("submit")); |
| 775 | +},async message(event) |
| 776 | +{let {data:message}=event; |
| 777 | + console.info("send",message); |
| 778 | + if(this.ownerDocument.defaultView.socket?.readyState===3) |
| 779 | + await socket("/peer"); |
| 780 | + this.ownerDocument.defaultView.socket?.send(JSON.stringify(message)); |
| 781 | +}} |
| 782 | + ,"canvas[role=img]": |
| 783 | + {contextrestored(event) |
| 784 | +{let [src,alt]=["data-source","aria-label"].map(this.getAttribute.bind(this)); |
| 785 | + compose(image,canvas,infer(insert,"over",this))(src,alt); |
| 786 | +}} |
| 787 | + ,"span[role=link]": |
| 788 | + {click(){this.ownerDocument.defaultView.open(this.getAttribute("source"),this.classList.contains("redirect")?undefined:"_blank")} |
| 789 | + } |
| 790 | + ,".defer": |
| 791 | + {load(event) |
| 792 | +{if(!this.dataset.subject) |
| 793 | + return; |
| 794 | + let subject=JSON.parse(this.dataset.subject); |
| 795 | + compose(combine(compose("source",fetch,digest),infer()),transform,"over",this,insert)(subject); |
| 796 | +}} |
| 797 | + ,".reference":{click(){expand.call(this,...arguments);}} |
| 798 | + ,"#extend": |
| 799 | + {keydown(event) |
| 800 | +{event.stopPropagation(); |
| 801 | + event.preventDefault(); |
| 802 | + let {target,keyCode}=event; |
| 803 | + let {enter}=keyboard(keyCode); |
| 804 | + if(!enter)return; |
| 805 | + let value=target.textContent; |
| 806 | + if(Array.from(value).every(is("."))) |
| 807 | + return this.dispatchEvent(new Event("submit",{bubbles:true})); |
| 808 | + let form=this.closest("form"); |
| 809 | + let method=form.getAttribute("method"); |
| 810 | + fragment.form.call(form,{[method]:annotate({[value]:""},labels)}); |
| 811 | + target.textContent="..."; |
| 812 | +}} |
| 813 | + ,".spell":{click(){spell(this);}} |
| 814 | + ,".field":{click({target}){form.call(target.closest("form"),{extend:{key:"",value:""}})}} |
| 815 | + ,".carousel": |
| 816 | + {scroll({target}) |
| 817 | +{let timeout=target.timeout=setTimeout(tick=> |
| 818 | +{if(target.timeout!==timeout)return; |
| 819 | + delete target.timeout; |
| 820 | + let to= |
| 821 | +[target.scrollLeft,target.getBoundingClientRect().width |
| 822 | +].reduce((offset,width)=>Math.round(offset/width)*width); |
| 823 | + let duration=500; |
| 824 | + !function animateScroll(start,change,time,increment) |
| 825 | +{time+=increment; |
| 826 | + time/=span/2; |
| 827 | + if(time<1) |
| 828 | + target.scrollLeft=delta/2*time*time+start; |
| 829 | + else |
| 830 | + target.scrollLeft=-delta/2*(--time*(time-2)-1)+start; |
| 831 | + if(currentTime<duration) |
| 832 | + setTimeout(tick=>animateScroll(start,change,currentTime,increment),increment); |
| 833 | +}(target.scrollLeft,to-target.scrollLeft,0,20); |
| 834 | +},100); |
| 835 | +}} |
| 836 | + }; |
771 | 837 |
|
772 |
| -// export function activate(fragment,actions,path=[]) |
773 |
| -// {// dispose actions on node/fragment to event listeners. |
774 |
| -// if(!actions) |
775 |
| -// return fragment; |
776 |
| -// if(string(actions)) |
777 |
| -// return import(actions).then(({default:actions})=>activate(fragment,actions)); |
778 |
| -// let node=fragment instanceof window.DocumentFragment |
779 |
| -// ?Array.from(fragment.childNodes) |
780 |
| -// :fragment.nodeName |
781 |
| -// ?[fragment] |
782 |
| -// :fragment; |
783 |
| -// let dispatch=proceduralize(function(){dispatch.call(this,event)}); |
784 |
| -// let nodes=Object.entries(node).flatMap(([nodename,node])=> |
785 |
| -// [node].flat().filter(compound).map(node=>compose |
786 |
| -// (tether(select) |
787 |
| -// ,Object.keys |
788 |
| -// ,infer("map",event=>["on"+event,dispatch]) |
789 |
| -// ,Object.fromEntries |
790 |
| -// ,node |
791 |
| -// ,(events,node)=>document.call(node,events) |
792 |
| -// ,node=>node.childNodes?Array.from(node.childNodes):node |
793 |
| -// )(node,actions) |
794 |
| -// )); |
795 |
| -// nodes.forEach(infer(activate,actions,path.concat(node?.nodeName))); |
796 |
| -// return fragment; |
797 |
| -// }; |
| 838 | + export async function socket(actions) |
| 839 | +{var {default:peer}=await import(actions); |
| 840 | + var {protocol,host}=window.location; |
| 841 | + return revert((resolve,reject,socket)=> |
| 842 | + Object.assign(window |
| 843 | +,{socket:Object.assign(new WebSocket(socket) |
| 844 | +,{async onopen({target}) |
| 845 | +{console.warn("Websocket open: ",target); |
| 846 | + let {author:name}=cookies(window.document.cookie); |
| 847 | + if(name) |
| 848 | + this.send(JSON.stringify({action:"sign",name})); |
| 849 | + resolve(this); |
| 850 | +},onmessage(event) |
| 851 | +{let message=JSON.parse(event.data); |
| 852 | + if(message.action!=="check") |
| 853 | + console.log("receive",message); |
| 854 | + peer[message.action]?.call(this,message,window); |
| 855 | +},onerror({target}){console.warn("Websocket not available at "+target.url);reject(target);} |
| 856 | + ,onclose({target}){console.warn("Websocket closed: ",target);} |
| 857 | + }) |
| 858 | + }))(["ws",/s/.test(protocol)?"s":"","://",host].join("")); |
| 859 | +}; |
798 | 860 |
|
799 | 861 | export function keyboard(code)
|
800 |
| -{let [key]=Object.entries({enter:13,escape:27,space:32,leftright:[37,39],updown:[38,40]}).find(({1:codes})=> |
| 862 | +{let [key]=Object.entries( |
| 863 | + {enter:13,escape:27,space:32,leftright:[37,39],updown:[38,40],backspace:8,tab:9 |
| 864 | + ,shift:16,ctrl:17,alt:18,caps:20,end:35,home:36,insert:45,delete:46 |
| 865 | + ,...Object.fromEntries( |
| 866 | +[Array.from({length:10},(number,index)=>index) |
| 867 | +,Array.from("abcdefghijklmnopqrstuvwxyz") |
| 868 | +].flat().map((key,index)=>[key,(index>9?55:48)+index])) |
| 869 | + }).find(({1:codes})=> |
801 | 870 | [codes].flat().includes(code))||[];
|
802 | 871 | return key?{[key]:true}:{};
|
803 | 872 | };
|
|
854 | 923 | Promise.all(["/Blik_2023_interface.js","/Blik_2023_inference.js","/Blik_2023_search.js","/Blik_2023_fragment.js"].map(module=>
|
855 | 924 | import(module))).then((
|
856 | 925 | [{resolve,cookies}
|
857 |
| -,{note,provide,collect,record,infer,tether,compose,combine,either,wether,drop,crop,swap,slip,match,is,not,has,simple} |
| 926 | +,{note,provide,collect,record,infer,tether,compose,combine,either,whether,drop,crop,swap,slip,match,is,not,has,simple} |
858 | 927 | ,{merge,prune,search}
|
859 | 928 | ,{css}
|
860 | 929 | ])=>Object.assign(window
|
861 | 930 | ,{resolve,cookies
|
862 |
| - ,note,provide,collect,record,infer,tether,compose,combine,either,wether,drop,crop,swap,slip,match,is,not,has,simple |
| 931 | + ,note,provide,collect,record,infer,tether,compose,combine,either,whether,drop,crop,swap,slip,match,is,not,has,simple |
863 | 932 | ,merge,prune,search
|
864 | 933 | ,css
|
865 | 934 | }));
|
866 |
| - var {peer}=import("/peer").then(({default:module})=>peer=module); |
867 |
| - var {protocol,host}=window.location; |
868 |
| - let socket=["ws",/s/.test(protocol)?"s":"","://",host].join(""); |
869 |
| - Object.assign(window |
870 |
| -,{socket:Object.assign(new WebSocket(socket) |
871 |
| -,{async onopen({target}) |
872 |
| -{console.warn("Websocket open: ",target); |
873 |
| - await new Promise(function defer(resolve){window.cookies?resolve():setTimeout(time=>defer(resolve),500);}); |
874 |
| - let {author:name}=cookies(window.document.cookie); |
875 |
| - if(name) |
876 |
| - this.send(JSON.stringify({action:"sign",name})); |
877 |
| -},onmessage(event) |
878 |
| -{let message=JSON.parse(event.data); |
879 |
| - if(message.action!=="check") |
880 |
| - console.log("receive",message); |
881 |
| - peer[message.action]?.call(this,message,window); |
882 |
| -},onerror({target}){console.warn("Websocket not available at "+target.url);} |
883 |
| - ,onclose({target}){console.warn("Websocket closed: ",target);} |
884 |
| - }) |
885 |
| - ,worker:import("/Blik_2023_interface.js").then(({delegate})=>delegate("/worker")).then(worker=> |
| 935 | + Object. assign(window |
| 936 | +,{worker:import("/Blik_2023_interface.js").then(({delegate})=>delegate("/worker")).then(worker=> |
886 | 937 | Object.assign(window,{worker})).catch(fail=>
|
887 | 938 | Object.assign(window,{worker:console.warn("Worker not available at /worker.")}))
|
888 | 939 | ,dispatch(event,buffering)
|
889 |
| -{// deprecated in favor of capture(fragment,actions). |
| 940 | +{// deprecated in favor of capture.call(fragment,actions). |
890 | 941 | // rarely needed for unpropagated server-rendered events like focus/blur.
|
891 | 942 | // explicit stopPropagation should be reproduced with event target conditions on scopes
|
892 | 943 | // (eg. {form:{input({target}){if(target)return;}}}).
|
|
902 | 953 | }});
|
903 | 954 | };
|
904 | 955 |
|
905 |
| - export function capture(fragment,actions) |
| 956 | + export function capture(module) |
906 | 957 | {// synchronously register events to be routed to actions scoped by selector (eg. {#form:{change(){}}}).
|
907 |
| - // common procedure in deferred scripts (with {fields} composed), hence no return statements. |
908 | 958 | if(!globalThis.window)
|
909 |
| - fragment.setAttribute("actions",actions) |
910 |
| -,exit(fragment.nodeName+" marked interactive: "+actions+". Re-invoke "+capture.name+" on client load to route events."); |
| 959 | + // Re-invoke on client to capture events. |
| 960 | + return this.setAttribute("actions",module),this; |
911 | 961 | let exclusion=
|
912 | 962 | [["motion","orientation","orientationabsolute"].map(sensor=>"device"+sensor)
|
913 | 963 | ,["start","run","end","cancel"].map(state=>"transition"+state)
|
914 | 964 | ].flat().map(event=>"on"+event);
|
915 | 965 | let inclusion=["focusout","focusin","message"].map(event=>"on"+event);
|
916 |
| - let buffered=new Set([fields(fragment).filter(event=> |
917 |
| - event.startsWith?.("on")&&!exclusion.includes(event)),inclusion].flat().map(event=> |
918 |
| - event.replace(/^on/,""))); |
919 |
| - buffered.forEach(event=>fragment.addEventListener(event,buffer,{passive:false})); |
920 |
| - let lead=["/","./"].find(lead=>actions.startsWith(lead)); |
921 |
| - let [module,...route]=actions.replace(lead,"").split("/").filter(Boolean); |
922 |
| - import(lead+module).then(module=> |
923 |
| - actions=Object.assign(["default"],route).reduce((module,field)=> |
924 |
| - module[field],module)).then(function(actions) |
925 |
| -{let events=Object.values(actions).flatMap(Object.keys); |
926 |
| - new Set(events).forEach(event=>fragment.addEventListener(event,dispatch,{passive:false})); |
927 |
| - buffered.forEach(event=>fragment.removeEventListener(event,buffer)); |
928 |
| - console.groupCollapsed("routing all propagated events to actions from scope: ",{fragment}); |
929 |
| - console.log(actions); |
930 |
| - console.log(events.sort().join(" ")); |
931 |
| - console.log(globalThis.window.location.origin+lead+module); |
| 966 | + let lead=["/","./"].find(lead=>module.startsWith(lead)); |
| 967 | + let route=module.replace(lead,"").split("/").filter(Boolean); |
| 968 | + let actions=import(lead+route.shift()).then(module=> |
| 969 | + Object.assign(["default"],route).reduce((module,field)=>module[field],module)); |
| 970 | + let refer=defer.bind(actions),deferred=new Set([heritage(this).filter(event=> |
| 971 | + event.startsWith?.("on")&&!exclusion.includes(event)),inclusion].flat()); |
| 972 | + deferred.forEach(event=>this.addEventListener(event.slice(2),refer,{passive:false})); |
| 973 | + actions.then(actions=> |
| 974 | + new Set(Object.values(actions).flatMap(Object.keys)).forEach(event=> |
| 975 | + this.addEventListener(event,delegate.bind(actions),{passive:false}))).then(ready=> |
| 976 | + deferred.forEach(event=>this.removeEventListener(event.slice(2),refer))); |
| 977 | + console.groupCollapsed("routing all propagated events to actions from scope: ",{fragment:this}); |
| 978 | + console.log({[globalThis.window.location.origin+lead+module]:actions}); |
932 | 979 | console.groupEnd();
|
933 |
| -}); |
934 |
| - function dispatch(event) |
| 980 | + return this; |
| 981 | +}; |
| 982 | + |
| 983 | + export function delegate(event) |
935 | 984 | {let target=event.target.document?.body||event.target.body||event.target;
|
936 | 985 | if(target.nodeName==="#text")target=target.parentNode;
|
937 |
| - let scopes=Object.entries(actions).filter(([selector,actions])=> |
| 986 | + let scopes=Object.entries(this).filter(([selector,actions])=> |
938 | 987 | actions[event.type]&&target.closest(selector));
|
939 | 988 | scopes.map(([selector,actions])=>
|
940 | 989 | [target.closest(selector),actions[event.type]]).forEach(([scope,action])=>
|
941 | 990 | console.log({[event.type]:target,scope})||
|
942 | 991 | action.call(scope,event));
|
943 |
| - //if(!scopes.length) |
944 |
| - //console.info("consumed:",{[event.type]:event}); |
945 | 992 | };
|
946 |
| - function buffer(event,buffering=0) |
| 993 | + |
| 994 | + export function defer(event) |
947 | 995 | {// asynchronizing event dispatch unblocks its synchronous default unless prevented.
|
948 |
| - if(typeof actions==="string") |
949 |
| - return setTimeout(buffer.bind(this,event,buffering+1),500)&& |
950 |
| - buffering||event.defaultPrevented||event.preventDefault()||console.warn( |
951 |
| - {["waiting for actions to dispatch "+event.type+" event from"]:event.target}); |
952 |
| - dispatch(event); |
953 |
| -}; |
| 996 | + this.then(actions=>delegate.call(actions,event)); |
| 997 | + event.preventDefault(); |
| 998 | + console.warn({["captured "+event.type+" event from"]:event.target}); |
954 | 999 | };
|
955 | 1000 |
|
956 | 1001 | export function dispose(){if(globalThis.window)Object.assign(window.actions,actions);}
|
|
0 commit comments