| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- package com.weEat.models
- import com.raquo.laminar.api.L._
- import io.laminext.syntax.core._
- import com.weEat.modules.{Overlay,Typeahead}
- import com.weEat.shared.models.UnitType._
- import com.weEat.shared.models._
- import gov.usda.nal.fdc.models.DataType._
- import com.tflucke.typeahead._
- import com.tflucke.typeahead.Dataset.Templates
- import com.tflucke.sortable.{Sortable, SortableOptions, SortableEvent}
- import com.weEat.controllers.{FoodController,USDAController}
- import scala.util.{Try,Success,Failure}
- import org.scalajs.dom.document
- import org.scalajs.dom.{Event,KeyboardEvent,HTMLInputElement,HTMLElement}
- import scala.concurrent.Future
- case class RecipeVar(recipe: Option[RecipeNode])
- extends VarRepresentationOf[RecipeNodeNoId] {
- implicit val ec = com.weEat.shared.ctx
- private def _defaultRecipe = recipe.getOrElse(RecipeNodeNoId(
- "New Recipe",
- 400.0f,
- 100.0f,
- defaultUnitType = UnitType.MASS,
- Seq(),
- Seq(),
- None,
- None,
- None,
- None
- ))
- private val _nameIn = input(idAttr := "name",
- cls := "form-control",
- required := true,
- defaultValue := _defaultRecipe.name
- )
- val name: Signal[String] = _nameIn.value
- private val _servingsIn = input(idAttr := "serv",
- typ := "number",
- cls := "form-control",
- defaultValue := (_defaultRecipe.stdQties / _defaultRecipe.stdQtiesPServing)
- .toString,
- stepAttr := "0.1",
- required := true
- )
- private val _servings = _servingsIn.value.map(_.toFloat)
- // private val _discreetIn = input(idAttr := "discreet",
- // typ := "checkbox",
- // cls := "form-check-input",
- // defaultChecked := (_defaultRecipe.defaultUnitType match {
- // case NUMBER => true
- // case MASS => _defaultRecipe.massPerUnit.isDefined
- // case VOLUME => _defaultRecipe.massPerUnit
- // .zip(_defaultRecipe.density)
- // .isDefined
- // })
- // )
- // private val _discreet = _discreetIn.checked
- // TODO: Try autopopulate ingredients.sum(_.massPerUnit)
- private val _massPerUnitIn = input(idAttr := "massP",
- cls := "form-control",
- typ := "number",
- minAttr := "1",
- stepAttr := "0.1",
- defaultValue := _defaultRecipe.massPerUnit.map(_.toString).getOrElse("")
- )
- val massPerUnit = _massPerUnitIn.value.transitions.map({
- case (None, "") => "100"
- case (Some(""), "") => "100"
- case (Some(_), str) => str
- case (None, str) => str
- }).map(_.toFloatOption)
- private val _volumeUnit = Var[MeasureUnit](Milliliter)
- private val _volumeUnitIn = select(cls := "custom-select",
- MeasureUnit.units.zipWithIndex
- .filter({ case (u, _) => u.typ == VOLUME })
- .map({ case (unit, idx) =>
- // TODO: default selected dynamic
- option(
- value := idx.toString,
- selected := unit.abr == Milliliter.abr,
- unit.abr
- )
- }),
- onChange.mapToValue.map(_.toInt).map(MeasureUnit.units(_)) --> _volumeUnit
- )
- // TODO: Try autopopulate ingredients.averge(_.density)
- private val _volumePerServIn = input(idAttr := "volP",
- cls := "form-control",
- typ := "number",
- minAttr := "0",
- stepAttr := "0.1",
- defaultValue := (_defaultRecipe.defaultUnitType match {
- case VOLUME =>
- (_defaultRecipe.stdQtiesPServing * UnitType.standardQuanity(VOLUME))
- .toString
- case MASS => _defaultRecipe.density
- .map((d) => (_defaultRecipe.stdQtiesPServing / d * UnitType.standardQuanity(MASS)).toString).getOrElse("")
- case NUMBER => _defaultRecipe.massPerUnit.zip(_defaultRecipe.density)
- .map {
- case (mass, density) => (mass / density * UnitType.standardQuanity(NUMBER)).toString
- } getOrElse("")
- })
- )
- private val _volumePerServ = _volumePerServIn.value.map(_.toFloatOption)
- .combineWithFn(_volumeUnit) {
- case (Some(num), unit) => Some((num * unit.conversionRatio).toFloat)
- case (None, _) => None
- }
- private val _sourceIn = input(idAttr := "source",
- cls := "form-control",
- defaultValue := _defaultRecipe.source.getOrElse("")
- )
- val source: Signal[Option[String]] = _sourceIn.value.map((s) => Option.when(s.size > 0)(s))
- private val internalFoodDS = Dataset(
- { _ => Nil},
- Some({ (str: String) => FoodController.query(str.toLowerCase)() }),
- display = { (food: FoodNodeId) => food.name }
- )
- private val usdaFoodDS = Dataset(
- { _ => Nil},
- Some({ str: String =>
- USDAController.getFoodsSearch(str, Seq(
- Branded, Foundation, SRLegacy
- ).map(_.toString))().map(_.foods.map(USDANodeNoId.fromSearchResult))
- }),
- templates = Some(Templates({(x: USDANodeNoId) => x.name}).copy(
- header = Some({ (_: String, _: Seq[USDANodeNoId], _: String) =>
- val html = document.createElement("div").asInstanceOf[HTMLElement]
- html.innerText = "Unoffical Foods"
- html
- })
- )),
- display = {(x: USDANodeNoId) => x.name}
- )
- private val _ingredients = Var[Seq[Ingredient]](_defaultRecipe.ingredients)
- val ingredients = _ingredients.signal
- private val _ingredientSearch = _ingredientInput() { (e) =>
- e.selectable.map(_.data).foreach({ (node) =>
- // TODO: default unit
- _editIngredient(Ingredient.fromFoodNode(node, 0, Gram)) { (in) =>
- _ingredients.update(_ :+ in)
- e.target.asInstanceOf[HTMLInputElement].value = ""
- Future.successful(())
- }
- })
- }
- private def _ingredientInput(defaul: Signal[Option[FoodNode]] = Val(None))(onSelect: (CursorEvent[FoodNode]) => Unit) = input(typ := "text",
- cls := "form-control input-sm",
- value <-- defaul.optionMap(_.name).withDefault(""),
- onMountCallback({(ctx) =>
- val elm = ctx.thisNode
- TypeaheadElement[FoodNode](
- elm.ref.asInstanceOf[HTMLInputElement],
- minLength = 3
- )(Seq(internalFoodDS, usdaFoodDS))
- elm.amend(Typeahead.onSelected[FoodNode] --> onSelect)
- })
- )
- private def _editIngredient(ing: Ingredient)(
- callback: (Ingredient => Future[_])
- ) = {
- val id = Var[Ingredient.IngredientId](ing.id)
- val amount = Var[Float](ing.amount)
- val unit = Var[MeasureUnit](ing.unit)
- Overlay.confirmFuture(ing.food.map({ (food) =>
- val amountIn = input(typ := "number",
- cls := "form-control input-sm",
- minAttr := "0",
- defaultValue := ing.amount.toString,
- onInput.mapToValue.map(_.toFloat) --> amount
- )
- val unitIn = select(cls := "custom-select input-group-append",
- MeasureUnit.units.zipWithIndex.map({ case (unit, idx) =>
- // TODO: default selected dynamic
- option(value := idx.toString,
- selected := unit == ing.unit,
- unit.name
- )
- }),
- onChange.mapToValue.map(_.toInt).map(MeasureUnit.units(_)) --> unit
- )
- div(cls := "row",
- div(cls := "col-12 input-group",
- _ingredientInput(Signal.fromFuture(ing.food)) { (e) =>
- e.selectable.map(_.data).foreach({ (node) =>
- id.set(Ingredient.IngredientId.fromFoodNode(node)) })
- },
- amountIn,
- unitIn
- )
- )
- }),
- id.signal.recoverToTry.map(_.isSuccess) &&
- amount.signal.recoverToTry.map(_.isSuccess) &&
- unit.signal.recoverToTry.map(_.isSuccess)
- ) { () =>
- callback(ing.copy(
- id = id.now(),
- amount = amount.now(),
- unit = unit.now()
- ))
- }
- }
- private def _presentFoodNode(ingredient: Ingredient, idx: Int) =
- li(children <-- Signal.fromFuture(ingredient.food).optionMap({ (food) =>
- Seq(
- span(cls := "ui-icon ui-icon-pencil",
- onClick --> { (e: Event) =>
- _editIngredient(ingredient) { (in) =>
- _ingredients.update(_.updated(idx, in))
- Future.successful(())
- }
- }
- ),
- span(cls := "ui-icon ui-icon-close",
- onClick --> { (e: Event) =>
- _ingredients.update(_.filterNot(_ == ingredient))
- }
- ),
- span(f"${ingredient.amount}%.02f${ingredient.unit.abr} ${food.name}")
- )
- }).withDefault(Nil)
- )
- private val _ENTER_KEY_CODE = 13
- private val _ingredientList = ul(
- listStyleType := "none",
- paddingLeft := "0",
- // Do not use zipWithIndex. It conflicts with the Sortable interface.
- children <-- ingredients.map(_.zipWithIndex.map((_presentFoodNode _).tupled)),
- onMountCallback({(ctx) =>
- Sortable.create(ctx.thisNode.ref, SortableOptions.onUpdate({
- (event: SortableEvent) =>
- _ingredients.update({ (ingredients) =>
- (event.oldIndex.toOption, event.newIndex.toOption) match {
- case (Some(old), Some(ne)) if (old < ne) =>
- moveBackwards(ingredients, old, ne)
- case (Some(old), Some(ne)) =>
- moveBackwards(
- ingredients.reverse,
- ingredients.size - old - 1,
- ingredients.size - ne - 1
- ).reverse
- case (None, Some(ne)) =>
- val (first, second) = ingredients.splitAt(ne)
- (first :+ ???) ++ second
- case (Some(old), None) =>
- _removeFromList(ingredients, old)
- }
- })
- // If the added element is still in the parent, remove it since the
- // Rx.update will have generated a new one.
- Option(event.item.parentElement).map(_.removeChild(event.item))
- def moveBackwards(ingredients: Seq[Ingredient], old: Int, ne: Int) =
- (ingredients.take(old) ++
- ingredients.take(ne+1).drop(old + 1) :+
- ingredients(old)) ++
- ingredients.drop(ne+1)
- }))
- })
- )
- private val _steps = Var[Seq[String]](_defaultRecipe.steps)
- val steps = _steps.signal
- private val _stepIn = input(idAttr := "step",
- cls := "form-control",
- onKeyPress --> { (e: KeyboardEvent) => if (e.keyCode == _ENTER_KEY_CODE) {
- val elm = e.target.asInstanceOf[HTMLInputElement]
- _steps.update(_ :+ elm.value)
- elm.value = ""
- } }
- )
- private def _removeFromList[T](s: Seq[T], idx: Int) =
- s.take(idx) ++ s.drop(idx + 1)
- private val _stepList = ol(
- listStyleType := "none",
- paddingLeft := "0",
- // Do not use zipWithIndex. It conflicts with the Sortable interface.
- children <-- _steps.signal.map(_.zipWithIndex.map((_presentStep _).tupled)),
- onMountCallback({(ctx) =>
- Sortable.create(ctx.thisNode.ref, SortableOptions.onUpdate({
- (event: SortableEvent) =>
- _steps.update({ (steps) =>
- (event.oldIndex.toOption, event.newIndex.toOption) match {
- case (Some(old), Some(ne)) if (old < ne) =>
- moveBackwards(steps, old, ne)
- case (Some(old), Some(ne)) =>
- moveBackwards(
- steps.reverse,
- steps.size - old - 1,
- steps.size - ne - 1
- ).reverse
- case (None, Some(ne)) =>
- val (first, second) = steps.splitAt(ne)
- (first :+ event.item.innerText) ++ second
- case (Some(old), None) =>
- _removeFromList(steps, old)
- }
- })
- // If the added element is still in the parent, remove it since the
- // Rx.update will have generated a new one.
- Option(event.item.parentElement).map(_.removeChild(event.item))
- def moveBackwards(steps: Seq[String], old: Int, ne: Int) =
- (steps.take(old) ++
- steps.take(ne+1).drop(old + 1) :+
- steps(old)) ++
- steps.drop(ne+1)
- }))
- })
- )
- private def _presentStep(step: String, idx: Int) = li(
- span(cls := "ui-icon ui-icon-close",
- onClick --> { (e: Event) =>
- _steps.update(_removeFromList(_, idx))
- }
- ),
- step
- )
- private val _srvAmtXunit = massPerUnit.combineWithFn(_volumePerServ, /* _discreet */ Val(false))({
- case (_, _, true) => Success((1.0f, NUMBER))
- case (Some(mass), _, _) => Success((mass, MASS))
- case (_, Some(volume), _) => Success((volume, VOLUME))
- case (None, None, false) => Failure(new IllegalStateException("None of mass, density, or count are enabled."))
- }).throwFailure
- val defaultUnitType = _srvAmtXunit.map(_._2)
- val stdQtiesPServing = _srvAmtXunit.map((x) => x._1 / x._2.standardQuanity)
- val stdQties = _servings.combineWithFn(stdQtiesPServing)(_ * _)
- val density = massPerUnit.combineWithFn(_volumePerServ)(_.zip(_).map {
- case (m, v) => (m/v).toFloat
- })
- val newInstance: Signal[Try[RecipeNodeNoId]] =
- name.combineWithFn(stdQties, stdQtiesPServing, defaultUnitType,
- ingredients, steps, density, massPerUnit.signal, source) {
- case (
- name, stdQties, stdQtiesPServing, defaultUnitType, ingredients, steps,
- density, massPerUnit, source
- ) => Success(RecipeNodeNoId(
- name, stdQties, stdQtiesPServing, defaultUnitType, ingredients, steps,
- density, massPerUnit, source, _defaultRecipe match {
- case node: RecipeNodeId => Some(node.vid)
- case _ => None
- }
- ))
- }
- protected def renderEditPane(): Node =
- form(cls := "form-group",
- div(cls := "container",
- div(cls := "row",
- div(cls := "col-md-12 input-group",
- div(cls := "input-group-prepend",
- label(cls := "input-group-text", "Name")
- ),
- _nameIn
- )
- ),
- div(cls := "row",
- div(cls := "col-md-2 form-check",
- /*
- div(cls := "form-check",
- _discreetIn,
- label(cls := "form-check-label",
- forId := "discreet",
- "Descrete Servings"
- )
- )*/
- ),
- div(cls := "col-md-3 input-group",
- _servingsIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "Servings")
- )
- ),
- div(cls := "col-md-3 input-group",
- _massPerUnitIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "g/Serving")
- )
- ),
- div(cls := "col-md-4 input-group",
- _volumePerServIn,
- _volumeUnitIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "/Serving"),
- ),
- )
- ),
- div(cls := "row",
- div(cls := "col-md-6",
- h2("Ingredients"),
- _ingredientSearch,
- _ingredientList
- ),
- div(cls := "col-md-6",
- h2("Steps"),
- _stepIn,
- _stepList
- )
- )
- )
- )
- }
|