let langShortname = (lang: string) => switch lang { | "ocaml" => "ml" | "reasonml" | "reason" => "re" | "bash" => "sh" | "text" => "" | 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) { try { const el = document.createElement('textarea'); el.value = str; el.setAttribute('readonly', ''); el.style.position = 'absolute'; el.style.left = '-9999px'; document.body.appendChild(el); const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; el.select(); document.execCommand('copy'); document.body.removeChild(el); if (selected) { document.getSelection().removeAllRanges(); document.getSelection().addRange(selected); } return true; } catch(e) { return false; } } `) type state = | Init | Copied | Failed @react.component let make = (~code) => { let (state, setState) = React.useState(_ => Init) let buttonRef = React.useRef(Nullable.null) let onClick = evt => { ReactEvent.Mouse.preventDefault(evt) if copyToClipboard(code) { setState(_ => Copied) } else { setState(_ => Failed) } } React.useEffect(() => { switch state { | Copied => open DomUtil let buttonEl = Nullable.toOption(buttonRef.current)->Option.getExn // 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") bannerEl->appendChild(textNode) buttonEl->appendChild(bannerEl) let nextFrameId = requestAnimationFrame(() => { bannerEl->classList->toggle("opacity-0") bannerEl->classList->toggle("opacity-100") }) let timeoutId = setTimeout(() => { buttonEl->removeChild(bannerEl) setState(_ => Init) }, 3000) Some( () => { cancelAnimationFrame(nextFrameId) clearTimeout(timeoutId) }, ) | _ => None } }, [state]) //Copy-Button } } @react.component let make = (~highlightedLines=[], ~code: string, ~showLabel=true, ~lang="text") => { let children = HighlightJs.renderHLJS(~highlightedLines, ~code, ~lang, ()) let label = if showLabel { let label = langShortname(lang)
{//RES or JS Label String.toUpperCase(label)->React.string}
} else { React.null }
label
children
} module Toggle = { type tab = { highlightedLines: option>, label: option, lang: option, code: string, } @react.component let make = (~tabs: array) => { let (selected, setSelected) = React.useState(_ => 0) switch tabs { | [tab] => make({ highlightedLines: ?tab.highlightedLines, code: tab.code, lang: ?tab.lang, showLabel: true, }) | multiple => let numberOfItems = Array.length(multiple) let tabElements = Array.mapWithIndex(multiple, (tab, i) => { // if there's no label, infer the label from the language let label = switch tab.label { | Some(label) => label | None => switch tab.lang { | Some(lang) => langShortname(lang)->String.toUpperCase | None => Int.toString(i) } } let activeClass = if selected === i { "font-medium text-12 text-gray-40 bg-gray-5 border-t-2 first:border-l" } else { "font-medium text-12 hover:text-gray-60 border-t-2 bg-gray-20 hover:cursor-pointer" } let onClick = evt => { ReactEvent.Mouse.preventDefault(evt) setSelected(_ => i) } let key = label ++ ("-" ++ Int.toString(i)) let paddingX = switch numberOfItems { | 1 | 2 => "sm:px-4" | 3 => "lg:px-8" | _ => "" } let borderColor = if selected === i { "#696B7D #EDF0F2" } else { "transparent" } {React.string(label)} }) let children = multiple[selected] ->Option.map(tab => { let lang = Option.getOr(tab.lang, "text") HighlightJs.renderHLJS(~highlightedLines=?tab.highlightedLines, ~code=tab.code, ~lang, ()) }) ->Option.getOr(React.null) // On a ReScript tab, always copy or open the ReScript code. Otherwise, copy the current selected code. let isReScript = tab => switch tab.lang { | Some("res") | Some("rescript") => true | _ => false } let buttonDiv = switch Array.findWithIndex(multiple, (tab, index) => tab->isReScript || index === selected ) { | Some({code: ""}) => React.null | Some(tab) => let playgroundLinkButton = tab->isReScript ? // ICON Link to PLAYGROUND : React.null let copyButton =
playgroundLinkButton copyButton
| None => React.null }
//text within code-box
{React.array(tabElements)}
buttonDiv
 children 
} } }