diff --git a/package-lock.json b/package-lock.json index 9afa0d162..39df72a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@headlessui/react": "^2.2.4", "@mdx-js/loader": "^3.1.0", "@rescript/react": "^0.14.0-rc.1", + "@rescript/webapi": "^0.1.0-experimental-03eae8b", "codemirror": "^5.54.0", "docson": "^2.1.0", "escodegen": "^2.1.0", @@ -2089,6 +2090,15 @@ "react-dom": ">=19.0.0" } }, + "node_modules/@rescript/webapi": { + "version": "0.1.0-experimental-03eae8b", + "resolved": "https://registry.npmjs.org/@rescript/webapi/-/webapi-0.1.0-experimental-03eae8b.tgz", + "integrity": "sha512-0McQ9XQlbF+/BWs70P2XJZ3wP6us7/HnNWAFnDYSnA9+Rvp6IQAuKCXfhbqJgTIge4YfiY5j+SqDt+OLfsSUTA==", + "license": "MIT", + "dependencies": { + "rescript": "^12.0.0-alpha.13" + } + }, "node_modules/@rescript/win32-x64": { "version": "12.0.0-alpha.14", "resolved": "https://registry.npmjs.org/@rescript/win32-x64/-/win32-x64-12.0.0-alpha.14.tgz", diff --git a/package.json b/package.json index dd63efb01..58869e8ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@headlessui/react": "^2.2.4", "@mdx-js/loader": "^3.1.0", "@rescript/react": "^0.14.0-rc.1", + "@rescript/webapi": "^0.1.0-experimental-03eae8b", "codemirror": "^5.54.0", "docson": "^2.1.0", "escodegen": "^2.1.0", diff --git a/rescript.json b/rescript.json index fc09510b1..87790843e 100644 --- a/rescript.json +++ b/rescript.json @@ -5,7 +5,11 @@ "version": 4 }, "bs-dependencies": [ - "@rescript/react" + "@rescript/react", + "@rescript/webapi" + ], + "bsc-flags": [ + "-open WebAPI.Global" ], "sources": [ { diff --git a/src/ApiDocs.res b/src/ApiDocs.res index 36f02582c..fba3abf17 100644 --- a/src/ApiDocs.res +++ b/src/ApiDocs.res @@ -95,7 +95,7 @@ module SidebarTree = { let version = url->Url.getVersionString let moduleRoute = - Webapi.URL.make("file://" ++ router.asPath).pathname + WebAPI.URL.make(~url="file://" ++ router.asPath).pathname ->String.replace(`/docs/manual/${version}/api/`, "") ->String.split("/") diff --git a/src/ConsolePanel.res b/src/ConsolePanel.res index 6c72f86f0..8018483db 100644 --- a/src/ConsolePanel.res +++ b/src/ConsolePanel.res @@ -17,8 +17,8 @@ let make = (~logs, ~appendLog) => { | _ => () } } - Webapi.Window.addEventListener("message", cb) - Some(() => Webapi.Window.removeEventListener("message", cb)) + WebAPI.Window.addEventListener(window, Custom("message"), cb) + Some(() => WebAPI.Window.removeEventListener(window, Custom("message"), cb)) }, [appendLog])
diff --git a/src/Packages.res b/src/Packages.res index 7460b704d..7cec69cdc 100644 --- a/src/Packages.res +++ b/src/Packages.res @@ -73,7 +73,7 @@ module Resource = { }) } - let uniqueKeywords: array => array = %raw(`(keywords) => [...new Set(keywords)]`) + let uniqueKeywords = arr => arr->Set.fromArray->Set.toArray let isOfficial = (res: t) => { switch res { @@ -340,24 +340,15 @@ module InfoSidebar = { } type props = { - "packages": array, - "urlResources": array, - "unmaintained": array, + packages: array, + urlResources: array, + unmaintained: array, } type state = | All | Filtered(string) // search term -let scrollToTop: unit => unit = %raw(`function() { - window.scroll({ - top: 0, - left: 0, - behavior: 'smooth' - }); -} -`) - let default = (props: props) => { open Markdown @@ -373,9 +364,9 @@ let default = (props: props) => { }) let allResources = { - let npms = props["packages"]->Array.map(pkg => Resource.Npm(pkg)) - let urls = props["urlResources"]->Array.map(res => Resource.Url(res)) - let outdated = props["unmaintained"]->Array.map(pkg => Resource.Outdated(pkg)) + let npms = props.packages->Array.map(pkg => Resource.Npm(pkg)) + let urls = props.urlResources->Array.map(res => Resource.Url(res)) + let outdated = props.unmaintained->Array.map(pkg => Resource.Outdated(pkg)) Belt.Array.concatMany([npms, urls, outdated]) } @@ -420,7 +411,7 @@ let default = (props: props) => { }) let onKeywordSelect = keyword => { - scrollToTop() + WebAPI.Window.scrollTo(window, ~options={left: 0.0, top: 0.0, behavior: Smooth}) setState(_ => { Filtered(keyword) }) @@ -524,73 +515,99 @@ let default = (props: props) => { } -type npmData = { - "objects": array<{ - "searchScore": float, - "score": { - "final": float, - "detail": {"quality": float, "popularity": float, "maintenance": float}, - }, - "package": { - "name": string, - "keywords": array, - "description": option, - "version": string, - "links": {"npm": string, "repository": option}, - }, - }>, -} - -module Response = { - type t - @send external json: t => promise = "json" +let parsePkgs = data => { + open JSON + + switch data { + | Object(dict{"objects": Array(arr)}) => + arr->Array.filterMap(pkg => { + switch pkg { + | Object(dict{ + "searchScore": Number(searchScore), + "score": Object(dict{"detail": Object(dict{"maintenance": Number(maintenanceScore)})}), + "package": Object(dict{ + "name": String(name), + "keywords": Array(keywords), + "version": String(version), + "description": ?Some(String(description)), + "links": Object(dict{ + "npm": String(npmHref), + "repository": ?Some(String(repositoryHref)), + }), + }), + }) => + let keywords = + keywords + ->Array.filterMap(k => { + switch k { + | String(k) => Some(k) + | _ => None + } + }) + ->Resource.filterKeywords + ->Resource.uniqueKeywords + + Some({ + name, + version, + keywords, + description, + repositoryHref: repositoryHref->Null.make, + npmHref, + searchScore, + maintenanceScore, + }) + | _ => None + } + }) + | _ => [] + } } -@val external fetchNpmPackages: string => promise = "fetch" - -let parsePkgs = data => - Array.map(data["objects"], item => { - let pkg = item["package"] - { - name: pkg["name"], - version: pkg["version"], - keywords: Resource.filterKeywords(pkg["keywords"])->Resource.uniqueKeywords, - description: Option.getOr(pkg["description"], ""), - repositoryHref: Null.fromOption(pkg["links"]["repository"]), - npmHref: pkg["links"]["npm"], - searchScore: item["searchScore"], - maintenanceScore: item["score"]["detail"]["maintenance"], - } - }) - let getStaticProps: Next.GetStaticProps.t = async _ctx => { let baseUrl = "https://registry.npmjs.org/-/v1/search?text=keywords:rescript&size=250&maintenance=1.0&popularity=0.5&quality=0.9" let (one, two, three) = await Promise.all3(( - fetchNpmPackages(baseUrl), - fetchNpmPackages(baseUrl ++ "&from=250"), - fetchNpmPackages(baseUrl ++ "&from=500"), + fetch(baseUrl), + fetch(baseUrl ++ "&from=250"), + fetch(baseUrl ++ "&from=500"), )) + let responseToOption = async response => { + try { + let json = await response->WebAPI.Response.json + Some(json) + } catch { + | _ => + Console.error2("Failed to parse response", response) + None + } + } + let (data1, data2, data3) = await Promise.all3(( - one->Response.json, - two->Response.json, - three->Response.json, + one->responseToOption, + two->responseToOption, + three->responseToOption, )) let unmaintained = [] let pkges = - parsePkgs(data1) - ->Array.concat(parsePkgs(data2)) - ->Array.concat(parsePkgs(data3)) + [data1, data2, data3] + ->Array.filterMap(d => + switch d { + | Some(d) => Some(parsePkgs(d)) + | None => None + } + ) + ->Array.flat ->Array.filter(pkg => { if packageAllowList->Array.includes(pkg.name) { true } else if pkg.name->String.includes("reason") { false } else if pkg.maintenanceScore < 0.3 { - let _ = unmaintained->Array.push(pkg) + unmaintained->Array.push(pkg) false } else { true @@ -606,9 +623,9 @@ let getStaticProps: Next.GetStaticProps.t = async _ctx => { { "props": { - "packages": pkges, - "unmaintained": unmaintained, - "urlResources": urlResources, + packages: pkges, + unmaintained, + urlResources, }, } } diff --git a/src/Packages.resi b/src/Packages.resi index 1c976f951..6f847a819 100644 --- a/src/Packages.resi +++ b/src/Packages.resi @@ -18,9 +18,9 @@ type npmPackage = { } type props = { - "packages": array, - "urlResources": array, - "unmaintained": array, + packages: array, + urlResources: array, + unmaintained: array, } let default: props => React.element diff --git a/src/Playground.res b/src/Playground.res index cb640325f..bd20c5939 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -352,30 +352,20 @@ module ResultPane = { } module WarningFlagsWidget = { - @set external _scrollTop: (Dom.element, int) => unit = "scrollTop" - @send external focus: Dom.element => unit = "focus" - - @send external blur: Dom.element => unit = "blur" - - @get external scrollHeight: Dom.element => int = "scrollHeight" - @get external clientHeight: Dom.element => int = "clientHeight" - @get external scrollTop: Dom.element => int = "scrollTop" - @get external offsetTop: Dom.element => int = "offsetTop" - @get external offsetHeight: Dom.element => int = "offsetHeight" - - @set external setScrollTop: (Dom.element, int) => unit = "scrollTop" - // Inspired by MUI (who got inspired by WAI best practise examples) // https://github.com/mui-org/material-ui/blob/next/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js#L327 - let scrollToElement = (~parent: Dom.element, element: Dom.element): unit => - if parent->scrollHeight > parent->clientHeight { - let scrollBottom = parent->clientHeight + parent->scrollTop - let elementBottom = element->offsetTop + element->offsetHeight + let scrollToElement = ( + ~parent: WebAPI.DOMAPI.htmlElement, + element: WebAPI.DOMAPI.htmlElement, + ): unit => + if parent.scrollHeight > parent.clientHeight { + let scrollBottom = parent.clientHeight + Float.toInt(parent.scrollTop) + let elementBottom = element.offsetTop + element.offsetHeight if elementBottom > scrollBottom { - parent->setScrollTop(elementBottom - parent->clientHeight) - } else if element->offsetTop - element->offsetHeight < parent->scrollTop { - parent->setScrollTop(element->offsetTop - element->offsetHeight) + parent.scrollTop = Float.fromInt(elementBottom - parent.clientHeight) + } else if element.offsetTop - element.offsetHeight < Float.toInt(parent.scrollTop) { + parent.scrollTop = Float.fromInt(element.offsetTop - element.offsetHeight) } } @@ -527,9 +517,11 @@ module WarningFlagsWidget = { // Used for the text input let inputRef = React.useRef(Nullable.null) - let focusInput = () => inputRef.current->Nullable.forEach(el => el->focus) + let focusInput = () => + inputRef.current->Nullable.forEach(el => WebAPI.HTMLInputElement.focus(el)) - let blurInput = () => inputRef.current->Nullable.forEach(el => el->blur) + let blurInput = () => + inputRef.current->Nullable.forEach(el => WebAPI.HTMLInputElement.focus(el)) let chips = Array.mapWithIndex(flags, (token, i) => { let {WarningFlagDescription.Parser.flag: flag, enabled} = token @@ -696,7 +688,8 @@ module WarningFlagsWidget = { let parent = listboxRef.current->Nullable.toOption switch (parent, el) { - | (Some(parent), Some(el)) => Some(() => scrollToElement(~parent, el)) + | (Some(parent), Some(el)) => + Some(() => scrollToElement(~parent, (Obj.magic(el): WebAPI.DOMAPI.htmlElement))) | _ => None } })->Some @@ -745,7 +738,7 @@ module WarningFlagsWidget = { let suggestionBox = Option.map(suggestions, elements =>
>))} className="p-2 absolute overflow-auto z-50 border-b rounded border-l border-r block w-full bg-gray-100 max-h-[15rem]"> elements
@@ -816,7 +809,7 @@ module WarningFlagsWidget = { chips
>))} className="inline-block p-1 max-w-20 outline-none bg-gray-90 placeholder-gray-20 placeholder-opacity-50" placeholder="Flags" type_="text" @@ -1064,7 +1057,7 @@ module ControlPanel = { let onClick = evt => { ReactEvent.Mouse.preventDefault(evt) - let ret = copyToClipboard(Webapi.Window.Location.href) + let ret = copyToClipboard(window.location.href) if ret { setState(_ => CopySuccess) } @@ -1129,12 +1122,12 @@ module ControlPanel = { } React.useEffect(() => { - Webapi.Window.addEventListener("keydown", onKeyDown) - Some(() => Webapi.Window.removeEventListener("keydown", onKeyDown)) + WebAPI.Window.addEventListener(window, Keydown, onKeyDown) + Some(() => WebAPI.Window.removeEventListener(window, Keydown, onKeyDown)) }, []) let runButtonText = { - let userAgent = Webapi.Window.Navigator.userAgent + let userAgent = window.navigator.userAgent let run = "Run" if userAgent->String.includes("iPhone") || userAgent->String.includes("Android") { run @@ -1496,9 +1489,7 @@ let make = (~versions: array) => { None }, (compilerState, compilerDispatch)) - let (layout, setLayout) = React.useState(_ => - Webapi.Window.innerWidth < breakingPoint ? Column : Row - ) + let (layout, setLayout) = React.useState(_ => window.innerWidth < breakingPoint ? Column : Row) let isDragging = React.useRef(false) @@ -1510,26 +1501,34 @@ let make = (~versions: array) => { let subPanelRef = React.useRef(Nullable.null) let onResize = () => { - let newLayout = Webapi.Window.innerWidth < breakingPoint ? Column : Row + let newLayout = window.innerWidth < breakingPoint ? Column : Row setLayout(_ => newLayout) switch panelRef.current->Nullable.toOption { | Some(element) => - let offsetTop = Webapi.Element.getBoundingClientRect(element)["top"] - Webapi.Element.Style.height(element, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(element).top + WebAPI.Element.setAttribute( + element, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) | None => () } switch subPanelRef.current->Nullable.toOption { | Some(element) => - let offsetTop = Webapi.Element.getBoundingClientRect(element)["top"] - Webapi.Element.Style.height(element, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(element).top + WebAPI.Element.setAttribute( + element, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) | None => () } } React.useEffect(() => { - Webapi.Window.addEventListener("resize", onResize) - Some(() => Webapi.Window.removeEventListener("resize", onResize)) + WebAPI.Window.addEventListener(window, Resize, onResize) + Some(() => WebAPI.Window.removeEventListener(window, Resize, onResize)) }, []) // To force CodeMirror render scrollbar on first render @@ -1552,30 +1551,50 @@ let make = (~versions: array) => { subPanelRef.current->Nullable.toOption, ) { | (Some(panelElement), Some(leftElement), Some(rightElement), Some(subElement)) => - let rectPanel = Webapi.Element.getBoundingClientRect(panelElement) + let rectPanel = WebAPI.Element.getBoundingClientRect(panelElement) // Update OutputPanel height - let offsetTop = Webapi.Element.getBoundingClientRect(subElement)["top"] - Webapi.Element.Style.height(subElement, `calc(100vh - ${offsetTop->Float.toString}px)`) + let offsetTop = WebAPI.Element.getBoundingClientRect(subElement).top + WebAPI.Element.setAttribute( + subElement, + ~qualifiedName="style", + ~value=`height: calc(100vh - ${offsetTop->Float.toString}px)`, + ) switch layout { | Row => - let delta = Int.toFloat(position) -. rectPanel["left"] + let delta = Int.toFloat(position) -. rectPanel.left - let leftWidth = delta /. rectPanel["width"] *. 100.0 - let rightWidth = (rectPanel["width"] -. delta) /. rectPanel["width"] *. 100.0 + let leftWidth = delta /. rectPanel.width *. 100.0 + let rightWidth = (rectPanel.width -. delta) /. rectPanel.width *. 100.0 - Webapi.Element.Style.width(leftElement, `${leftWidth->Float.toString}%`) - Webapi.Element.Style.width(rightElement, `${rightWidth->Float.toString}%`) + WebAPI.Element.setAttribute( + leftElement, + ~qualifiedName="style", + ~value=`width: ${leftWidth->Float.toString}%`, + ) + WebAPI.Element.setAttribute( + rightElement, + ~qualifiedName="style", + ~value=`width: ${rightWidth->Float.toString}%`, + ) | Column => - let delta = Int.toFloat(position) -. rectPanel["top"] + let delta = Int.toFloat(position) -. rectPanel.top - let topHeight = delta /. rectPanel["height"] *. 100. - let bottomHeight = (rectPanel["height"] -. delta) /. rectPanel["height"] *. 100. + let topHeight = delta /. rectPanel.height *. 100. + let bottomHeight = (rectPanel.height -. delta) /. rectPanel.height *. 100. - Webapi.Element.Style.height(leftElement, `${topHeight->Float.toString}%`) - Webapi.Element.Style.height(rightElement, `${bottomHeight->Float.toString}%`) + WebAPI.Element.setAttribute( + leftElement, + ~qualifiedName="style", + ~value=`height: ${topHeight->Float.toString}%`, + ) + WebAPI.Element.setAttribute( + rightElement, + ~qualifiedName="style", + ~value=`height: ${bottomHeight->Float.toString}%`, + ) } | _ => () } @@ -1594,15 +1613,15 @@ let make = (~versions: array) => { onMove(position) } - Webapi.Window.addEventListener("mousemove", onMouseMove) - Webapi.Window.addEventListener("touchmove", onTouchMove) - Webapi.Window.addEventListener("mouseup", onMouseUp) + WebAPI.Window.addEventListener(window, Mousemove, onMouseMove) + WebAPI.Window.addEventListener(window, Touchmove, onTouchMove) + WebAPI.Window.addEventListener(window, Mouseup, onMouseUp) Some( () => { - Webapi.Window.removeEventListener("mousemove", onMouseMove) - Webapi.Window.removeEventListener("touchmove", onTouchMove) - Webapi.Window.removeEventListener("mouseup", onMouseUp) + WebAPI.Window.removeEventListener(window, Mousemove, onMouseMove) + WebAPI.Window.removeEventListener(window, Touchmove, onTouchMove) + WebAPI.Window.removeEventListener(window, Mouseup, onMouseUp) }, ) }, [layout]) @@ -1765,10 +1784,10 @@ let make = (~versions: array) => { />
+ ref={ReactDOM.Ref.domRef((Obj.magic(panelRef): React.ref>))}> // Left Panel
>))} className={`${layout == Column ? "h-2/4" : "!h-full"} ${layout == Column ? "w-full" : "w-[50%]"}`}> @@ -1785,10 +1804,10 @@ let make = (~versions: array) => { | None => () | Some(timer) => clearTimeout(timer) } - let timer = setTimeout(() => { + let timer = setTimeout(~handler=() => { timeoutCompile.current() typingTimer.current = None - }, 100) + }, ~timeout=100) typingTimer.current = Some(timer) }} onMarkerFocus={rowCol => setFocusedRowCol(_prev => Some(rowCol))} @@ -1797,7 +1816,7 @@ let make = (~versions: array) => {
// Separator
>))} // TODO: touch-none not applied className={`flex items-center justify-center touch-none select-none bg-gray-70 opacity-30 hover:opacity-50 rounded-lg ${layout == Column @@ -1812,14 +1831,16 @@ let make = (~versions: array) => {
// Right Panel
>))} className={`${layout == Column ? "h-6/15" : "!h-inherit"} ${layout == Column ? "w-full" : "w-[50%]"}`}>
{React.array(headers)}
-
+
>))} + className="overflow-auto">
diff --git a/src/SyntaxLookup.res b/src/SyntaxLookup.res index 843cb87af..a9aa4d147 100644 --- a/src/SyntaxLookup.res +++ b/src/SyntaxLookup.res @@ -141,10 +141,7 @@ type state = | ShowFiltered(string, array) // (search, filteredItems) | ShowDetails(Item.t) -@val @scope("window") -external scrollTo: (int, int) => unit = "scrollTo" - -let scrollToTop = () => scrollTo(0, 0) +let scrollToTop = () => WebAPI.Window.scrollTo(window, ~options={left: 0.0, top: 0.0}) type props = {mdxSources: array} type params = {slug: string} diff --git a/src/Try.res b/src/Try.res index 15fa8f46c..feef200c7 100644 --- a/src/Try.res +++ b/src/Try.res @@ -33,10 +33,8 @@ let default = props => { let getStaticProps: Next.GetStaticProps.t = async _ => { let versions = { - let response = await Webapi.Fetch.fetch( - "https://cdn.rescript-lang.org/playground-bundles/versions.json", - ) - let json = await Webapi.Fetch.Response.json(response) + let response = await fetch("https://cdn.rescript-lang.org/playground-bundles/versions.json") + let json = await WebAPI.Response.json(response) json ->JSON.Decode.array ->Option.getOrThrow diff --git a/src/bindings/Jsdom.res b/src/bindings/Jsdom.res index 5021bab42..43f12c740 100644 --- a/src/bindings/Jsdom.res +++ b/src/bindings/Jsdom.res @@ -1,5 +1,5 @@ -type window = {document: Dom.document} -type t = {window: window} +type window = {document: WebAPI.DOMAPI.document} +type t = {window: WebAPI.DOMAPI.window} @module("jsdom") @new external make: string => t = "JSDOM" diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res deleted file mode 100644 index ab53ceb41..000000000 --- a/src/bindings/Webapi.res +++ /dev/null @@ -1,96 +0,0 @@ -module Document = { - @val external document: Dom.element = "document" - @scope("document") @val external createElement: string => Dom.element = "createElement" - @scope("document") @val external createTextNode: string => Dom.element = "createTextNode" - @send - external querySelector: (Dom.document, string) => Nullable.t = "querySelector" - @send - external querySelectorAll: (Dom.document, string) => Js.Array2.array_like = - "querySelectorAll" -} - -module ClassList = { - type t - @send external toggle: (t, string) => unit = "toggle" - @send external remove: (t, string) => unit = "remove" -} - -module Element = { - @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild" - @send external removeChild: (Dom.element, Dom.element) => unit = "removeChild" - @set external setClassName: (Dom.element, string) => unit = "className" - @get external classList: Dom.element => ClassList.t = "classList" - @send external getBoundingClientRect: Dom.element => {..} = "getBoundingClientRect" - @send external addEventListener: (Dom.element, string, unit => unit) => unit = "addEventListener" - @send external getAttribute: (Dom.element, string) => Nullable.t = "getAttribute" - - @send - external getElementById: (Dom.element, string) => Nullable.t = "getElementById" - - type contentWindow - @get external contentWindow: Dom.element => option = "contentWindow" - - @send - external postMessage: (contentWindow, string, ~targetOrigin: string=?) => unit = "postMessage" - - @send - external postMessageAny: (contentWindow, 'a, ~targetOrigin: string=?) => unit = "postMessage" - - module Style = { - @scope("style") @set external width: (Dom.element, string) => unit = "width" - @scope("style") @set external height: (Dom.element, string) => unit = "height" - } -} - -type animationFrameId - -@scope("window") @val -external requestAnimationFrame: (unit => unit) => animationFrameId = "requestAnimationFrame" - -@scope("window") @val -external cancelAnimationFrame: animationFrameId => unit = "cancelAnimationFrame" - -module Window = { - @scope("window") @val external addEventListener: (string, 'a => unit) => unit = "addEventListener" - @scope("window") @val - external removeEventListener: (string, 'a => unit) => unit = "removeEventListener" - @scope("window") @val external innerWidth: int = "innerWidth" - @scope("window") @val external innerHeight: int = "innerHeight" - @scope("window") @val external scrollY: int = "scrollY" - - module History = { - @scope(("window", "history")) @val - external pushState: (nullable<'a>, @as(json`""`) _, ~url: string=?) => unit = "pushState" - @scope(("window", "history")) @val - external replaceState: (nullable<'a>, @as(json`""`) _, ~url: string=?) => unit = "replaceState" - } - - module Location = { - @scope(("window", "location")) @val external href: string = "href" - } - - module Navigator = { - @scope(("window", "navigator")) @val external userAgent: string = "userAgent" - } -} - -module Fetch = { - module Response = { - type t - @send external text: t => promise = "text" - @send external json: t => promise = "json" - } - - @val external fetch: string => promise = "fetch" -} - -module URL = { - type t = { - hash: string, - host: string, - hostname: string, - href: string, - pathname: string, - } - @new external make: string => t = "URL" -} diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index 2d22a43e9..e17b92bac 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -583,7 +583,7 @@ let useCompilerManager = ( | SetupFailed(_) => () | Ready(ready) => let url = createUrl(router.route, ready) - Webapi.Window.History.replaceState(null, ~url) + WebAPI.History.replaceState(history, ~data=JSON.Null, ~unused="", ~url) } } diff --git a/src/common/EvalIFrame.res b/src/common/EvalIFrame.res index 507268f66..3531edcf1 100644 --- a/src/common/EvalIFrame.res +++ b/src/common/EvalIFrame.res @@ -69,23 +69,24 @@ let srcDoc = ` ` -type message = { - code: string, - imports: Dict.t, -} - let sendOutput = (code, imports) => { - open Webapi - - let frame = Document.document->Element.getElementById("iframe-eval") + let frame = document->WebAPI.Document.querySelector("#iframe-eval") switch frame { | Value(element) => - switch element->Element.contentWindow { - | Some(win) => win->Element.postMessageAny({code, imports}, ~targetOrigin="*") - | None => Console.error("contentWindow not found") + let element: WebAPI.DOMAPI.htmliFrameElement = element->Obj.magic + switch element.contentWindow { + | Value({window}) => + let message = JSON.Object( + dict{ + "code": JSON.String(code), + "imports": JSON.Object(imports->Dict.mapValues(v => JSON.String(v))), + }, + ) + window->WebAPI.Window.postMessage(~message, ~targetOrigin="*") + | Null => Console.error("contentWindow not found") } - | Null | Undefined => Console.error("iframe not found") + | Null => Console.error("iframe not found") } } diff --git a/src/common/Hooks.res b/src/common/Hooks.res index e20fda83b..e4382b816 100644 --- a/src/common/Hooks.res +++ b/src/common/Hooks.res @@ -40,7 +40,7 @@ let useScrollDirection = (~topMargin=80, ~threshold=20) => { React.useEffect(() => { let onScroll = _e => { setScrollDir(prev => { - let scrollY = Webapi.Window.scrollY + let scrollY = scrollY->Float.toInt let enterTopMargin = scrollY <= topMargin let action = switch prev { @@ -60,8 +60,8 @@ let useScrollDirection = (~topMargin=80, ~threshold=20) => { } }) } - Webapi.Window.addEventListener("scroll", onScroll) - Some(() => Webapi.Window.removeEventListener("scroll", onScroll)) + WebAPI.Window.addEventListener(window, Scroll, onScroll) + Some(() => WebAPI.Window.removeEventListener(window, Scroll, onScroll)) }, [topMargin, threshold]) scrollDir diff --git a/src/common/MetaTagsApi.res b/src/common/MetaTagsApi.res index 01a3dbc90..dc0fb3b67 100644 --- a/src/common/MetaTagsApi.res +++ b/src/common/MetaTagsApi.res @@ -5,43 +5,61 @@ type t = { } /** - This function uses JSDOM to fetch a webpage and extract the meta tags from it. + This function uses JSDOM to fetch a webpage and extract the meta tags from it. JSDOM is required since this runs on Node. */ let extractMetaTags = async (url: string) => { - open Webapi try { - let response = await Fetch.fetch(url) + let response = await fetch(url) - let html = await response->Fetch.Response.text + let html = await response->WebAPI.Response.text let dom = Jsdom.make(html) let document = dom.window.document - let metaTags = - document - ->Document.querySelectorAll("meta") - ->Array.fromArrayLike - ->Array.reduce(Dict.fromArray([]), (tags, meta) => { - let name = meta->Element.getAttribute("name")->Nullable.toOption - let property = meta->Element.getAttribute("property")->Nullable.toOption - let itemprop = meta->Element.getAttribute("itemprop")->Nullable.toOption + let nodeList = document->WebAPI.Document.querySelectorAll("meta") - let name = switch (name, property, itemprop) { - | (Some(name), _, _) => Some(name) - | (_, Some(property), _) => Some(property) - | (_, _, Some(itemprop)) => Some(itemprop) - | _ => None - } + let elements = [] - let content = meta->Element.getAttribute("content")->Nullable.toOption + for i in 0 to nodeList.length { + let node = WebAPI.NodeList.item(nodeList, i) + // cast Node elements to Element + elements->Array.push((Obj.magic(node): WebAPI.DOMAPI.element)) + } - switch (name, content) { - | (Some(name), Some(content)) => tags->Dict.set(name, content) - | _ => () - } + let metaTags = elements->Array.reduce(Dict.fromArray([]), (tags, meta) => { + let name = + meta + ->WebAPI.Element.getAttribute("name") + ->Null.make + let property = + meta + ->WebAPI.Element.getAttribute("property") + ->Null.make + let itemprop = + meta + ->WebAPI.Element.getAttribute("itemprop") + ->Null.make + + let name = switch (name, property, itemprop) { + | (Value(name), _, _) => Some(name) + | (_, Value(property), _) => Some(property) + | (_, _, Value(itemprop)) => Some(itemprop) + | _ => None + } - tags - }) + let content = + meta + ->WebAPI.Element.getAttribute("content") + ->Nullable.make + ->Nullable.toOption + + switch (name, content) { + | (Some(name), Some(content)) => tags->Dict.set(name, content) + | _ => () + } + + tags + }) let title = metaTags->Dict.get("og:title") let description = metaTags->Dict.get("og:description") diff --git a/src/components/CodeExample.res b/src/components/CodeExample.res index 0c84e3a9c..1418e498c 100644 --- a/src/components/CodeExample.res +++ b/src/components/CodeExample.res @@ -8,26 +8,6 @@ let langShortname = (lang: string) => | rest => rest } -module DomUtil = { - @scope("document") @val external createElement: string => Dom.element = "createElement" - @scope("document") @val external createTextNode: string => Dom.element = "createTextNode" - @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild" - @send external removeChild: (Dom.element, Dom.element) => unit = "removeChild" - - @set external setClassName: (Dom.element, string) => unit = "className" - - type classList - @get external classList: Dom.element => classList = "classList" - @send external toggle: (classList, string) => unit = "toggle" - - type animationFrameId - @scope("window") @val - external requestAnimationFrame: (unit => unit) => animationFrameId = "requestAnimationFrame" - - @scope("window") @val - external cancelAnimationFrame: animationFrameId => unit = "cancelAnimationFrame" -} - module CopyButton = { let copyToClipboard: string => bool = %raw(` function(str) { @@ -77,31 +57,29 @@ module CopyButton = { React.useEffect(() => { switch state { | Copied => - open DomUtil let buttonEl = Nullable.toOption(buttonRef.current)->Option.getOrThrow // Note on this imperative DOM nonsense: // For Tailwind transitions to behave correctly, we need to first paint the DOM element in the tree, // and in the next tick, add the opacity-100 class, so the transition animation actually takes place. // If we don't do that, the banner will essentially pop up without any animation - let bannerEl = createElement("div") - bannerEl->setClassName( - "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out ", - ) - let textNode = createTextNode("Copied to clipboard") + let bannerEl = WebAPI.Document.createElement(document, "div") + bannerEl.className = "opacity-0 absolute -top-6 right-0 -mt-5 -mr-4 px-4 py-2 w-40 rounded-lg captions text-white bg-gray-100 text-gray-80-tr transition-all duration-1000 ease-in-out " + + let textNode = WebAPI.Document.createTextNode(document, "Copied to clipboard") - bannerEl->appendChild(textNode) - buttonEl->appendChild(bannerEl) + WebAPI.Element.appendChild(bannerEl, textNode)->ignore + WebAPI.Element.appendChild(buttonEl, bannerEl)->ignore - let nextFrameId = requestAnimationFrame(() => { - bannerEl->classList->toggle("opacity-0") - bannerEl->classList->toggle("opacity-100") + let nextFrameId = WebAPI.Window.requestAnimationFrame(window, _ => { + WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-0")->ignore + WebAPI.DOMTokenList.toggle(bannerEl.classList, ~token="opacity-100")->ignore }) - let timeoutId = setTimeout(() => { - buttonEl->removeChild(bannerEl) + let timeoutId = setTimeout(~handler=() => { + buttonEl->WebAPI.Element.removeChild(bannerEl)->ignore setState(_ => Init) - }, 3000) + }, ~timeout=3000) Some( () => { @@ -114,7 +92,10 @@ module CopyButton = { }, [state]) //Copy-Button