|
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