From 7a01b001ccc4a7bda3da92486903540e3f9754fd Mon Sep 17 00:00:00 2001 From: Joris Date: Thu, 13 Feb 2020 18:54:18 +0100 Subject: Set up bucklescript --- src/arrayUtils.ml | 8 +++++++ src/dom.ts | 54 ------------------------------------------ src/domUtils.ml | 37 +++++++++++++++++++++++++++++ src/main.ml | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 71 ------------------------------------------------------- src/number.ml | 57 ++++++++++++++++++++++++++++++++++++++++++++ src/number.ts | 66 --------------------------------------------------- 7 files changed, 165 insertions(+), 191 deletions(-) create mode 100644 src/arrayUtils.ml delete mode 100644 src/dom.ts create mode 100644 src/domUtils.ml create mode 100644 src/main.ml delete mode 100644 src/main.ts create mode 100644 src/number.ml delete mode 100644 src/number.ts (limited to 'src') diff --git a/src/arrayUtils.ml b/src/arrayUtils.ml new file mode 100644 index 0000000..75319d8 --- /dev/null +++ b/src/arrayUtils.ml @@ -0,0 +1,8 @@ +let flatMap (f : 'a -> 'b option) (xs : 'a Js.Array.t) : 'b Js.Array.t = + xs |> Js.Array.map f + |> Js.Array.filter (fun maybe -> + match maybe with Some _ -> true | None -> false) + |> Js.Array.map (fun maybe -> + match maybe with + | Some x -> x + | None -> Js.Exn.raiseError "Unexpected None") diff --git a/src/dom.ts b/src/dom.ts deleted file mode 100644 index 6b1c803..0000000 --- a/src/dom.ts +++ /dev/null @@ -1,54 +0,0 @@ -type Attribute = string | boolean | ((e: Event) => void) - -type Child = Element | string - -export function h(tag: string, attrs: {[key: string]: Attribute}, children: Array = []): Element { - let element = document.createElement(tag) - - for (let name in attrs) { - let value = attrs[name] - if (typeof value === 'boolean') { - if (value) { - element.setAttribute(name, name) - } - } else if (typeof value === 'function') { - (element as any)[name] = (e: Event) => { - (value as ((e: Event) => void))(e) - } - } else { - element.setAttribute(name, value) - } - } - - children.forEach(child => { - if (typeof child === 'string') { - element.appendChild(document.createTextNode(child)) - } else { - element.appendChild(child) - } - }) - - return element -} - -export function toggleClassName(node: Element, className: string) { - if (node.className === className) { - node.className = '' - } else { - node.className = className - } -} - -export function nodeListToArray(nodeList: NodeListOf): HTMLElement[] { - const xs: HTMLElement[] = []; - nodeList.forEach(node => xs.push(node)) - return xs -} - -export function replace(node: Node, replacement: Node) { - const parentNode = node.parentNode - - if (parentNode) { - parentNode.replaceChild(replacement, node) - } -} diff --git a/src/domUtils.ml b/src/domUtils.ml new file mode 100644 index 0000000..282ac12 --- /dev/null +++ b/src/domUtils.ml @@ -0,0 +1,37 @@ +open Webapi.Dom + +let toggleClassName (element : Dom.element) (className : string) : unit = + Element.setClassName element + (if Element.className element == className then "" else className) + +type child = TextChild of string | ElemChild of Dom.element + +let h (tag : string) (attributes : (string * string) Js.Array.t) + (children : child Js.Array.t) : Dom.element = + let element = Document.createElement tag document in + let () = + attributes + |> Js.Array.forEach (fun a -> Element.setAttribute (fst a) (snd a) element) + in + let () = + children + |> Js.Array.forEach (fun c -> + match c with + | TextChild t -> + Element.appendChild (Document.createTextNode t document) element + | ElemChild e -> Element.appendChild e element) + in + element + +external replace_child : Dom.node -> Dom.element -> Dom.element -> unit + = "replaceChild" + [@@bs.send] + +let replace (element : Dom.element) (replacement : Dom.element) : unit = + match Element.parentNode element with + | Some parent -> replace_child parent replacement element + | _ -> () + +external value : Dom.eventTarget -> string option = "value" [@@bs.get] + +external setValue : Dom.element -> string -> unit = "value" [@@bs.set] diff --git a/src/main.ml b/src/main.ml new file mode 100644 index 0000000..a2174f2 --- /dev/null +++ b/src/main.ml @@ -0,0 +1,63 @@ +open Webapi.Dom + +(* Set up inputs for the ingredients *) + +type ingredient = { quantity : float; element : Dom.element } + +let ingredients : ingredient Js.Array.t = + document + |> Document.querySelectorAll ".g-Recipe__Content ul > li" + |> NodeList.toArray + |> ArrayUtils.flatMap (fun node -> + Belt.Option.map (Element.ofNode node) (fun e -> ("li", e))) + |> Js.Array.concat + ( match Document.querySelector ".g-Recipe__Content h1" document with + | Some element -> [| ("h1", element) |] + | _ -> [||] ) + |> ArrayUtils.flatMap (fun (tag, element) -> + Belt.Option.map + (Number.parseInsideText (Element.innerHTML element)) + (fun parsed -> + let created = Number.createElement tag parsed in + let () = DomUtils.replace element created.element in + { quantity = parsed.number; element = created.numberInput })) + +(* Update ingredients amounts *) + +let () = + ingredients + |> Js.Array.forEach (fun ingredient -> + Element.addEventListener "input" + (fun e -> + Belt.Option.forEach + (DomUtils.value (Event.target e)) + (fun numberStr -> + Belt.Option.forEach (Number.parse numberStr) (fun parsed -> + let factor = parsed.number /. ingredient.quantity in + ingredients + |> Js.Array.forEach (fun otherIngredient -> + if ingredient.element != otherIngredient.element + then + DomUtils.setValue otherIngredient.element + (Number.prettyPrint + (factor *. otherIngredient.quantity)) + else ())))) + ingredient.element) + +(* Set up done marks for steps *) + +let () = + document + |> Document.querySelectorAll ".g-Recipe__Content ol > li" + |> NodeList.toArray + |> Js.Array.forEach (fun node -> + match Element.ofNode node with + | Some element -> + Element.addEventListener "click" + (fun e -> + let () = + DomUtils.toggleClassName element "g-Recipe__Completed" + in + Event.stopPropagation e) + element + | _ -> ()) diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 184d26d..0000000 --- a/src/main.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as number from './number' -import * as dom from './dom' - -/* Set up inputs for the ingredients */ - -const itemEntries = - dom.nodeListToArray(document.querySelectorAll('.g-Recipe__Content ul > li')) - .map(itemNode => ({ name: 'li', node: itemNode })) - -const h1 = document.querySelector('.g-Recipe__Content h1') - -if (h1 !== null) { - itemEntries.push({ name: 'h1', node: h1 }) -} - -const inputs = setupInputs(itemEntries) - -inputs.map(input => { - input.node.oninput = e => { - if (e.target !==null) { - const parsed = number.parse((e.target as HTMLInputElement).value) - - if (parsed !== undefined) { - const factor = parsed.number / input.number - inputs.map(input2 => { - if (input.node !== input2.node) { - input2.node.value = number.prettyPrint(input2.number * factor) - } - }) - } - } - } -}) - -interface InputTag { - name: string; - node: HTMLElement; -} - -interface InputResult { - number: number, - node: HTMLInputElement -} - -function setupInputs(tags: InputTag[]): InputResult[] { - const res: InputResult[] = [] - - tags.forEach(tag => { - const parsed = number.parseInsideText(tag.node.innerText) - - if (parsed !== undefined) { - const numberNode = number.node(tag.name, parsed) - dom.replace(tag.node, numberNode.node) - res.push({ - number: parsed.number, - node: numberNode.numberInput - }) - } - }) - - return res -} - -/* Set up done marks for steps */ - -dom.nodeListToArray(document.querySelectorAll('.g-Recipe__Content ol > li')).forEach(todo => { - todo.onclick = e => { - dom.toggleClassName(todo, 'g-Recipe__Completed') - e.stopPropagation() - } -}) diff --git a/src/number.ml b/src/number.ml new file mode 100644 index 0000000..cdd9ef8 --- /dev/null +++ b/src/number.ml @@ -0,0 +1,57 @@ +type parseInsideTextResult = { before : string; number : float; after : string } + +let execRegex (regex : Js.Re.t) (str : string) : string option Js.Array.t = + match Js.Re.exec_ regex str with + | Some result -> Js.Array.map Js.toOption (Js.Re.captures result) + | None -> [||] + +let parseInsideText (str : string) : parseInsideTextResult option = + match execRegex [%re "/^([^\\d]*)(\\d+)((\\.|,)(\\d+))?(.*)/"] str with + | [| _; Some before; Some intPart; _; _; decPart; Some after |] -> + Some + { + before; + number = + Js.Float.fromString + ( intPart + ^ Belt.Option.mapWithDefault decPart "" (fun str -> "." ^ str) ); + after; + } + | _ -> None + +type parseResult = { number : float; remaining : string } + +let parse (str : string) : parseResult option = + match parseInsideText str with + | Some parseResult -> + if parseResult.before == "" then + Some { number = parseResult.number; remaining = parseResult.after } + else None + | _ -> None + +type numberElement = { element : Dom.element; numberInput : Dom.element } + +let prettyPrint (number : float) : string = + let strNumber = Js.Float.toString number in + match Js.String.split "." strNumber with + | [| intPart; decPart |] -> + intPart ^ "," ^ Js.String.slice ~from:0 ~to_:2 decPart + | _ -> strNumber + +let createElement (tag : string) (content : parseInsideTextResult) : + numberElement = + let numberInput = + DomUtils.h "input" + [| ("class", "g-Number"); ("value", prettyPrint content.number) |] + [| DomUtils.TextChild "" |] + in + { + element = + DomUtils.h tag [||] + [| + DomUtils.TextChild content.before; + DomUtils.ElemChild numberInput; + DomUtils.TextChild content.after; + |]; + numberInput; + } diff --git a/src/number.ts b/src/number.ts deleted file mode 100644 index 6663329..0000000 --- a/src/number.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { h } from './dom' - -interface ParseInsideTextResult { - before: string; - number: number; - after: string; -} - -export function parseInsideText(str: string): ParseInsideTextResult | undefined { - let res = str.match(/^([^\d]*)(\d+)((\.|,)(\d+))?(.*)/) - - if (res !== null && res.length === 7) { - return { - before: res[1], - number: parseFloat(res[2] + '.' + res[5]), - after: res[6] - } - } else { - return undefined; - } -} - -interface ParseResult { - number: number; - remaining: string; -} - -export function parse(str: string): ParseResult | undefined { - let res = str.match(/^(\d+)((\.|,)(\d+))?(.*$)/) - - if (res !== null && res.length === 6) { - return { - number: parseFloat(res[1] + '.' + res[4]), - remaining: res[5] - } - } else { - return undefined; - } -} - -export interface Node { - node: Element; - numberInput: HTMLInputElement; -} - -export function node(tag: string, content: ParseInsideTextResult): Node { - let numberInput = h('input', { - 'class': 'g-Number', - 'value': prettyPrint(content.number) - }) as HTMLInputElement - - return { - node: h(tag, {}, [content.before, numberInput, content.after]), - numberInput: numberInput - } -} - -export function prettyPrint(n: number): string { - const xs = n.toString().split('.') - - if (xs.length == 2) { - return xs[0] + ',' + xs[1].substring(0, 2) - } else { - return xs[0] - } -} -- cgit v1.2.3