diff options
| author | Joris | 2017-02-24 09:44:16 +0100 | 
|---|---|---|
| committer | Joris | 2017-02-24 09:44:16 +0100 | 
| commit | f1de0dd7632eb29a40ea1f5cf136ab43ee945926 (patch) | |
| tree | 0e9045ffad0925afeaa6b8cbf07a6e0ff50f38d2 /src/main/scala | |
| parent | 626d5ee7ea4ea4f90637e453e0dc488fc6b1a19e (diff) | |
Add search field
Diffstat (limited to 'src/main/scala')
21 files changed, 422 insertions, 187 deletions
| diff --git a/src/main/scala/reading/Main.scala b/src/main/scala/reading/Main.scala index 6104891..a9fc3e2 100644 --- a/src/main/scala/reading/Main.scala +++ b/src/main/scala/reading/Main.scala @@ -1,13 +1,13 @@  package reading  import scala.scalajs.js.JSApp - +import rx._ +import rx.Ctx.Owner.Unsafe._  import org.scalajs.dom -  import scalacss.Defaults._  import reading.component.style.{ Global => GlobalStyle } -import reading.utils.RxTag +import reading.utils.RxUtils._  object Main extends JSApp {    def main(): Unit = { @@ -16,7 +16,7 @@ object Main extends JSApp {      dom.document.head.appendChild(style)      val _ = dom.document.body.appendChild { -      RxTag { implicit context => +      Rx {          Route.current() match {            case Route.Books(filters) => component.Index(filters)          } diff --git a/src/main/scala/reading/component/Index.scala b/src/main/scala/reading/component/Index.scala index 57f5b4b..78890de 100644 --- a/src/main/scala/reading/component/Index.scala +++ b/src/main/scala/reading/component/Index.scala @@ -1,34 +1,30 @@  package reading.component  import rx._ -import Ctx.Owner.Unsafe._ - -import scalatags.JsDom.all._  import scalacss.Defaults._  import scalacss.ScalatagsCss._ +import scalatags.JsDom.all._ -import reading.Books -import reading.component.style.{ Index => IndexStyle }  import reading.component.index.{ Menu, Header, Books => BooksComponent } -import reading.models.{ Book, Filter } +import reading.component.style.{ Index => IndexStyle } +import reading.models.{ Book, Books, Filter }  object Index { -  def apply(initialFilters: Seq[Filter]): HtmlTag = { +  def apply(initialFilters: Seq[Filter])(implicit ctx: Ctx.Owner): Frag = {      val filters: Var[Seq[Filter]] = Var(initialFilters) -    val books: Rx[Seq[Book]] = Rx { -      if (filters().isEmpty) Books() else Book.filter(Books(), filters()) -    } -    val count: Rx[Int] = Rx(books().length) +    val books: Rx[Seq[Book]] = Rx(Filter.add(Books(), filters())) +    val search: Var[String] = Var("")      val showFiltersMenu: Var[Boolean] = Var(false) +    val searchedBooks: Rx[Seq[Book]] = Rx(Book.filter(books(), search()))      div(        IndexStyle.render,        IndexStyle.page, -      Menu(books, filters, showFiltersMenu), +      Menu(books, filters, search, showFiltersMenu),        div(          IndexStyle.main, -        Header(filters, showFiltersMenu, count), -        BooksComponent(books) +        Header(searchedBooks, filters, search, showFiltersMenu), +        BooksComponent(searchedBooks)        )      )    } diff --git a/src/main/scala/reading/component/index/BookDetail.scala b/src/main/scala/reading/component/index/BookDetail.scala index 7df771b..c42029f 100644 --- a/src/main/scala/reading/component/index/BookDetail.scala +++ b/src/main/scala/reading/component/index/BookDetail.scala @@ -8,7 +8,7 @@ import reading.component.index.style.{ BookDetail => BookStyle }  import reading.models.{ Program, Book }  object BookDetail { -  def apply(book: Book): HtmlTag = +  def apply(book: Book): Frag =      div(        BookStyle.render,        BookStyle.detail, @@ -20,6 +20,8 @@ object BookDetail {        ),        div( +        BookStyle.items, +          if (book.programs.nonEmpty) {            item("classe", book.programs.map(Program.grade(_).prettyPrint).distinct.sorted)          }, @@ -33,11 +35,14 @@ object BookDetail {            item("genre", book.genres.sorted.map(_.prettyPrint))          },          book.period.map { period => -          item("période", Seq(period.prettyPrint)) -        } +          item("période", period.prettyPrint) +        }, +        item("niveau", book.level.prettyPrint)        )      ) +  private def item(key: String, value: String): Frag = item(key, Seq(value)) +    private def item(key: String, values: Seq[String]): Frag =      div(        BookStyle.item, diff --git a/src/main/scala/reading/component/index/Books.scala b/src/main/scala/reading/component/index/Books.scala index 20b308d..c22639f 100644 --- a/src/main/scala/reading/component/index/Books.scala +++ b/src/main/scala/reading/component/index/Books.scala @@ -1,7 +1,6 @@  package reading.component.index  import rx._ -import Ctx.Owner.Unsafe._  import scalatags.JsDom.all._  import scalacss.Defaults._ @@ -10,16 +9,16 @@ import scalacss.ScalatagsCss._  import reading.component.index.style.{ Books => BooksStyle }  import reading.component.widget.Modal  import reading.models.{ Book } -import reading.utils.{ RxTag, RxAttr } +import reading.utils.RxUtils._  object Books { -  def apply(books: Rx[Seq[Book]]): Frag = { +  def apply(books: Rx[Seq[Book]])(implicit ctx: Ctx.Owner): Frag = {      val focus: Var[Option[Book]] = Var(None)      div(        BooksStyle.render, -      RxTag { implicit context => +      Rx {          div(            div(              BooksStyle.books, @@ -31,13 +30,13 @@ object Books {                    BooksStyle.cover,                    src := s"cover/${book.title}.jpg",                    alt := s"${book.title}, ${book.author}", -                  RxAttr(onclick, Rx(() => focus() = Some(book))) +                  onclick := (() => focus() = Some(book))                  )                )              }            ), -          RxTag { implicit context => +          Rx {              focus() match {                case Some(book) => Modal(onClose = focus() = None)(BookDetail(book))                case None => span("") diff --git a/src/main/scala/reading/component/index/FilterUtils.scala b/src/main/scala/reading/component/index/FilterUtils.scala new file mode 100644 index 0000000..d4b24e4 --- /dev/null +++ b/src/main/scala/reading/component/index/FilterUtils.scala @@ -0,0 +1,39 @@ +package reading.component.index + +import rx._ + +import reading.models._ +import reading.Route + +object FilterUtils { +  def remove( +    filters: Var[Seq[Filter]], +    search: Var[String], +    filter: Filter +  ): Unit = { +    val newFilters = Filter.remove(filters.now, filter) +    filters() = newFilters +    search() = "" +    Route.push(Route.Books(newFilters)) +  } + +  def removeAll( +    filters: Var[Seq[Filter]], +    search: Var[String] +  ): Unit = { +    filters() = Nil +    search() = "" +    Route.push(Route.Books(Nil)) +  } + +  def add( +    filters: Var[Seq[Filter]], +    search: Var[String], +    filter: Filter +  ): Unit = { +    val newFilters = filter +: filters.now +    filters() = newFilters +    search() = "" +    Route.push(Route.Books(newFilters)) +  } +} diff --git a/src/main/scala/reading/component/index/Header.scala b/src/main/scala/reading/component/index/Header.scala index cf078ad..50d520e 100644 --- a/src/main/scala/reading/component/index/Header.scala +++ b/src/main/scala/reading/component/index/Header.scala @@ -1,32 +1,39 @@  package reading.component.index  import rx._ -import Ctx.Owner.Unsafe._  import scalatags.JsDom.all._  import scalacss.Defaults._  import scalacss.ScalatagsCss._  import reading.component.index.style.{ Header => HeaderStyle } -import reading.component.widget.Cross +import reading.component.widget.{ Cross, Input }  import reading.component.style.{ Color => C } -import reading.models.Filter -import reading.Route -import reading.utils.{ RxTag, RxAttr } +import reading.models.{ Book, Filter } +import reading.utils.RxUtils._  object Header { -  def apply(filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean], booksCount: Rx[Int]): Frag = { +  def apply( +    books: Rx[Seq[Book]], +    filters: Var[Seq[Filter]], +    search: Var[String], +    showFiltersMenu: Var[Boolean] +  )( +    implicit +    ctx: Ctx.Owner +  ): Frag = {      val filtersCount: Rx[Int] = Rx(filters().length) +    val booksCount: Rx[Int] = books.map(_.length)      div(        HeaderStyle.render,        HeaderStyle.header, -      RxTag { implicit context => +      Rx {          div(            div(              HeaderStyle.showFiltersMenu, -            RxAttr(onclick, Rx(() => showFiltersMenu() = true)), +            onclick := (() => showFiltersMenu() = true),              "Filtrer",              if (filtersCount() > 0) span(HeaderStyle.filtersCount, filtersCount()) else span("")            ), @@ -39,14 +46,14 @@ object Header {                div(                  HeaderStyle.clear, -                RxAttr(onclick, Rx(() => updateFilters(filters, Nil))), +                onclick := (() => FilterUtils.removeAll(filters, search)),                  "Effacer les filtres"                ),                filters().sortBy(_.name).map { filter =>                  div(                    HeaderStyle.filter, -                  RxAttr(onclick, Rx(() => updateFilters(filters, Filter.remove(filters(), filter)))), +                  onclick := (() => FilterUtils.remove(filters, search, filter)),                    span(HeaderStyle.name, filter.name.capitalize),                    Cross(15.px, C.black.value)                  ) @@ -55,17 +62,16 @@ object Header {          )        }, -      RxTag { implicit context => -        div( -          HeaderStyle.booksCount, -          span(s"${booksCount()} livre${if (booksCount() > 1) "s" else ""}") -        ) -      } +      div( +        HeaderStyle.searchAndCount, +        Input(HeaderStyle.search, search, "Rechercher"), +        Rx { +          div( +            HeaderStyle.booksCount, +            span(s"${booksCount()} livre${if (booksCount() > 1) "s" else ""}") +          ) +        } +      )      )    } - -  private def updateFilters(filters: Var[Seq[Filter]], newFilters: Seq[Filter]): Unit = { -    filters() = newFilters -    Route.push(Route.Books(newFilters)) -  }  } diff --git a/src/main/scala/reading/component/index/Menu.scala b/src/main/scala/reading/component/index/Menu.scala index a0aabd1..4c118bd 100644 --- a/src/main/scala/reading/component/index/Menu.scala +++ b/src/main/scala/reading/component/index/Menu.scala @@ -1,7 +1,6 @@  package reading.component.index  import rx._ -import Ctx.Owner.Unsafe._  import scalatags.JsDom.all._  import scalacss.Defaults._ @@ -9,45 +8,60 @@ import scalacss.ScalatagsCss._  import reading.component.index.style.{ Menu => MenuStyle }  import reading.models._ -import reading.utils.{ RxTag, RxAttr } -import reading.Route +import reading.utils.RxUtils._  object Menu { -  def apply(books: Rx[Seq[Book]], filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean]): Frag = -    RxTag { implicit context => +  def apply( +    books: Rx[Seq[Book]], +    filters: Var[Seq[Filter]], +    search: Var[String], +    showFiltersMenu: Var[Boolean] +  )( +    implicit +    ctx: Ctx.Owner +  ): Frag = +    div( +      MenuStyle.render, +      Rx(if (showFiltersMenu()) MenuStyle.show else MenuStyle.empty), +      MenuStyle.menu, + +      div(MenuStyle.background), +        div( -        MenuStyle.render, -        if (showFiltersMenu()) MenuStyle.show else "", -        MenuStyle.menu, +        MenuStyle.content, -        header(showFiltersMenu, filters().length), +        Rx(header(showFiltersMenu, filters().length)),          div(            MenuStyle.groups, -          filters().find(_.kind == FilterKind.Grade) match { -            case Some(grade) => { -              val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName) -              group(books, filters, grade.name, programs.map(Filter.apply(_)), Some(grade)) +          Rx { +            filters().find(_.kind == FilterKind.Grade) match { +              case Some(grade) => +                val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName) +                group(books, filters, search, grade.name, programs.map(Filter.apply(_)), Some(grade)) +              case None => +                group(books, filters, search, "Classe", Grade.values.map(Filter.apply(_)))              } -            case None => group(books, filters, "Classe", Grade.values.map(Filter.apply(_)))            }, -          filters().find(_.kind == FilterKind.GroupedTheme) match { -            case Some(groupedTheme) => { -              val themes = Theme.values.filter(t => Theme.groupedTheme(t).toString() == groupedTheme.nonFormattedName) -              group(books, filters, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme)) +          Rx { +            filters().find(_.kind == FilterKind.GroupedTheme) match { +              case Some(groupedTheme) => +                val themes = Theme.values.filter(t => Theme.groupedTheme(t).toString() == groupedTheme.nonFormattedName) +                group(books, filters, search, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme)) +              case None => +                group(books, filters, search, "Theme", GroupedTheme.values.map(Filter.apply(_)))              } -            case None => group(books, filters, "Theme", GroupedTheme.values.map(Filter.apply(_)))            }, -          group(books, filters, "Genre", Genre.values.sorted.map(Filter.apply(_))), -          group(books, filters, "Niveau", Level.values.map(Filter.apply(_))), -          group(books, filters, "Période", Period.values.map(Filter.apply(_))) +          group(books, filters, search, "Genre", Genre.values.sorted.map(Filter.apply(_))), +          group(books, filters, search, "Niveau", Level.values.map(Filter.apply(_))), +          group(books, filters, search, "Période", Period.values.map(Filter.apply(_)))          ), -        footer(Rx(books().length), filters, showFiltersMenu) +        footer(books, filters, search, showFiltersMenu)        ) -    } +    ) -  def header(showFiltersMenu: Var[Boolean], count: Int): HtmlTag = +  def header(showFiltersMenu: Var[Boolean], count: Int): Frag =      div(        MenuStyle.header,        "Filtrer", @@ -57,78 +71,84 @@ object Menu {    def group(      books: Rx[Seq[Book]],      filters: Var[Seq[Filter]], +    search: Var[String],      name: String,      groupFilters: Seq[Filter],      parentFilter: Option[Filter] = None    )(      implicit -    context: Ctx.Data +    ctx: Ctx.Owner    ): Frag = {      val filtersWithCount = Rx {        groupFilters -        .map(filter => (filter, Book.filter(books(), Seq(filter)).length)) +        .map(filter => (filter, Filter.add(books(), filter).length))          .filter(_._2 > 0)      } -    if (filtersWithCount().isEmpty) -      span("") -    else -      div( -        MenuStyle.filterGroup, +    Rx { +      if (filtersWithCount().isEmpty) +        span("") +      else          div( -          MenuStyle.filterTitle, -          parentFilter.map { filter => -            RxAttr(onclick, Rx(() => updateFilters(filters, Filter.remove(filters(), filter)))) -          }.getOrElse(""), -          if (parentFilter.isDefined) MenuStyle.activeFilter else "", -          name, -          RxTag { implicit context => -            val count = filters().filter(f => groupFilters.exists(Filter.equals(f, _))).length -            if (count > 0) span(MenuStyle.filterTitleCount, count) else span("") -          } -        ), -        div( -          filtersWithCount().map { -            case (filter, count) => { -              val isActive = Filter.contains(filters(), filter) +          div( +            MenuStyle.filterTitle, +            parentFilter.map { filter => +              onclick := (() => FilterUtils.remove(filters, search, filter)) +            }.getOrElse(""), +            if (parentFilter.isDefined) MenuStyle.activeFilter else "", +            name, +            Rx { +              val count = filters().filter(f => groupFilters.exists(f == _)).length +              if (count > 0) span(MenuStyle.filterTitleCount, count) else span("") +            } +          ), +          div( +            filtersWithCount().map { +              case (filter, count) => { +                val isActive = Filter.contains(filters(), filter) -              button( -                MenuStyle.filter, -                if (isActive) MenuStyle.activeFilter else "", -                RxAttr(onclick, Rx(() => updateFilters( -                  filters, -                  if (isActive) Filter.remove(filters(), filter) else filter +: filters() -                ))), -                span( -                  span(filter.name.capitalize), -                  span(MenuStyle.filterCount, count) +                button( +                  MenuStyle.filter, +                  if (isActive) MenuStyle.activeFilter else "", +                  onclick := (() => +                    if (isActive) +                      FilterUtils.remove(filters, search, filter) +                    else +                      FilterUtils.add(filters, search, filter)), +                  span( +                    span(filter.name.capitalize), +                    span(MenuStyle.filterCount, count) +                  )                  ) -              ) +              }              } -          } +          )          ) -      ) -  } - -  private def updateFilters(filters: Var[Seq[Filter]], newFilters: Seq[Filter]): Unit = { -    filters() = newFilters -    Route.push(Route.Books(newFilters)) +    }    } -  def footer(bookCount: Rx[Int], filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean]): HtmlTag = +  def footer( +    books: Rx[Seq[Book]], +    filters: Var[Seq[Filter]], +    search: Var[String], +    showFiltersMenu: Var[Boolean] +  )( +    implicit +    ctx: Ctx.Owner +  ): Frag =      div(        MenuStyle.footer,        div(          MenuStyle.clear, -        RxAttr(onclick, Rx(() => filters() = Nil)), +        onclick := (() => FilterUtils.removeAll(filters, search)),          "Effacer"        ),        div(          MenuStyle.returnToBooks, -        RxAttr(onclick, Rx(() => showFiltersMenu() = false)), +        onclick := (() => showFiltersMenu() = false),          "Afficher", -        RxTag { implicit context => -          span(MenuStyle.bookCount, bookCount()) +        Rx { +          span(MenuStyle.bookCount, books().length)          }        )      ) diff --git a/src/main/scala/reading/component/index/style/BookDetail.scala b/src/main/scala/reading/component/index/style/BookDetail.scala index e8f970e..f432fda 100644 --- a/src/main/scala/reading/component/index/style/BookDetail.scala +++ b/src/main/scala/reading/component/index/style/BookDetail.scala @@ -18,6 +18,10 @@ object BookDetail extends StyleSheet.Inline {      marginBottom(30.px)    ) +  val items = style( +    marginBottom(25.px) +  ) +    val item = style(      lineHeight(25.px),      margin(0.px, 15.px, 15.px), diff --git a/src/main/scala/reading/component/index/style/Header.scala b/src/main/scala/reading/component/index/style/Header.scala index 2260c91..2eb6eb2 100644 --- a/src/main/scala/reading/component/index/style/Header.scala +++ b/src/main/scala/reading/component/index/style/Header.scala @@ -66,6 +66,18 @@ object Header extends StyleSheet.Inline {      height(15.px)    ) +  val searchAndCount = style( +    display.flex, +    flexWrap.wrap, +    alignItems.center, +    Media.mobile(justifyContent.center) +  ) + +  val search = style( +    Media.mobile(display.none), +    Media.desktop(marginRight(30.px)) +  ) +    val booksCount = style(      fontSize(20.px),      color(C.gray.value), diff --git a/src/main/scala/reading/component/index/style/Menu.scala b/src/main/scala/reading/component/index/style/Menu.scala index 12b0646..dd74039 100644 --- a/src/main/scala/reading/component/index/style/Menu.scala +++ b/src/main/scala/reading/component/index/style/Menu.scala @@ -10,9 +10,27 @@ object Menu extends StyleSheet.Inline {    val menu = style(      Media.mobile(display.none), -    color(C.white.value), +    Media.desktop( +      color(C.white.value), +      position.relative, +      width(280.px) +    ) +  ) + +  val background = style( +    Media.desktop( +      position.fixed, +      width(280.px), +      height(100.%%), +      backgroundColor(C.englishWalnut.value), +      boxShadow := "4px 0px 6px -1px rgba(0, 0, 0, 0.2)" +    ) +  ) + +  val content = style(      position.relative, -    width(280.px) +    width(100.%%), +    height(100.%%)    )    val header = style( @@ -50,6 +68,8 @@ object Menu extends StyleSheet.Inline {      )    ) +  val empty = style() +    val groups = style(      Media.mobile(        height :=! "calc(100% - 120px)", @@ -57,8 +77,6 @@ object Menu extends StyleSheet.Inline {      )    ) -  val filterGroup = style() -    val filterTitle = style(      Commons.filter(),      minHeight(50.px), diff --git a/src/main/scala/reading/component/style/Index.scala b/src/main/scala/reading/component/style/Index.scala index 99e4746..e02ebd9 100644 --- a/src/main/scala/reading/component/style/Index.scala +++ b/src/main/scala/reading/component/style/Index.scala @@ -3,7 +3,6 @@ package reading.component.style  import scalacss.Defaults._  import reading.Media -import reading.component.style.{ Color => C }  object Index extends StyleSheet.Inline {    import dsl._ @@ -11,22 +10,11 @@ object Index extends StyleSheet.Inline {    val page = style(      display.flex,      overflowY.scroll, -    height(100.%%), - -    Media.desktop( -      &.before( -        content := "\"\"", -        display.block, -        position.fixed, -        width(280.px), -        height(100.%%), -        backgroundColor(C.englishWalnut.value), -        boxShadow := "4px 0px 6px -1px rgba(0, 0, 0, 0.2)" -      ) -    ) +    height(100.%%)    )    val main = style( -    width(100.%%) +    Media.desktop(width :=! "calc(100% - 280px)"), +    Media.mobile(width(100.%%))    )  } diff --git a/src/main/scala/reading/component/widget/Cross.scala b/src/main/scala/reading/component/widget/Cross.scala index c9e3054..40087a1 100644 --- a/src/main/scala/reading/component/widget/Cross.scala +++ b/src/main/scala/reading/component/widget/Cross.scala @@ -8,7 +8,7 @@ import scalacss.internal.ValueT, ValueT.Color  import reading.component.widget.style.{ Cross => CrossStyle }  object Cross { -  def apply(size: String, color: ValueT[Color]): HtmlTag = +  def apply(size: String, color: ValueT[Color]): Frag =      div(        CrossStyle.render,        CrossStyle.cross, diff --git a/src/main/scala/reading/component/widget/Input.scala b/src/main/scala/reading/component/widget/Input.scala new file mode 100644 index 0000000..7dac47a --- /dev/null +++ b/src/main/scala/reading/component/widget/Input.scala @@ -0,0 +1,44 @@ +package reading.component.widget + +import scalatags.JsDom.all._ + +import org.scalajs.dom.KeyboardEvent +import org.scalajs.dom.html.Input + +import scalacss.Defaults._ +import scalacss.ScalatagsCss._ + +import rx._ + +import reading.component.widget.style.{ Input => InputStyle } + +object Input { +  def apply( +    style: StyleA, +    query: Var[String], +    label: String = "", +    onEnter: => Unit = () +  )( +    implicit +    ctx: Ctx.Owner +  ): Frag = { +    val inputBox = input( +      InputStyle.render, +      InputStyle.input, +      style, +      placeholder := label, +      onkeyup := { (e: KeyboardEvent) => +        val input = e.target.asInstanceOf[Input] +        query() = input.value +        input.value = input.value +        if (e.keyCode == 13) onEnter +      } +    ).render + +    query.trigger { +      inputBox.value = query.now +    } + +    inputBox +  } +} diff --git a/src/main/scala/reading/component/widget/Modal.scala b/src/main/scala/reading/component/widget/Modal.scala index 81d0c78..db1f7e6 100644 --- a/src/main/scala/reading/component/widget/Modal.scala +++ b/src/main/scala/reading/component/widget/Modal.scala @@ -3,29 +3,27 @@ package reading.component.widget  import scala.util.Random  import org.scalajs.dom.raw.HTMLElement -import rx._ -import rx.Ctx.Owner.Unsafe._  import scalacss.Defaults._  import scalacss.ScalatagsCss._  import scalatags.JsDom.all._  import reading.component.widget.style.{ Modal => ModalStyle } -import reading.utils.{ RxAttr }  object Modal { -  def apply(onClose: => Unit)(content: HtmlTag): HtmlTag = { +  def apply(onClose: => Unit)(content: Frag): Frag = {      val modalId = s"modal${Random.nextInt}"      Animate(        id = modalId,        duration = 250,        transition = Transition.easeOut, -      animate = (progress, element) => { -      element.style.opacity = s"$progress" -      element.childNodes(2) match { -        case e: HTMLElement => e.style.transform = s"scale(${0.85 + 0.15 * progress})" +      animate = +      (progress, element) => { +        element.style.opacity = s"$progress" +        element.childNodes(2) match { +          case e: HTMLElement => e.style.transform = s"scale(${0.85 + 0.15 * progress})" +        }        } -    }      )      div( @@ -35,7 +33,7 @@ object Modal {        div(          ModalStyle.curtain, -        RxAttr(onclick, Rx(() => close(modalId, onClose))) +        onclick := (() => close(modalId, onClose))        ),        div( @@ -43,7 +41,7 @@ object Modal {          content,          button(            ModalStyle.close, -          RxAttr(onclick, Rx(() => close(modalId, onClose))), +          onclick := (() => close(modalId, onClose)),            "Fermer"          )        ) diff --git a/src/main/scala/reading/component/widget/style/Input.scala b/src/main/scala/reading/component/widget/style/Input.scala new file mode 100644 index 0000000..967393b --- /dev/null +++ b/src/main/scala/reading/component/widget/style/Input.scala @@ -0,0 +1,16 @@ +package reading.component.widget.style + +import scalacss.Defaults._ + +import reading.component.style.{ Color => C } + +object Input extends StyleSheet.Inline { +  import dsl._ + +  val input = style( +    border(1.px, solid, C.mickado.value), +    borderRadius(2.px), +    padding(10.px), +    &.hover(borderColor(C.gray.value)) +  ) +} diff --git a/src/main/scala/reading/component/widget/style/Modal.scala b/src/main/scala/reading/component/widget/style/Modal.scala index 1872344..faf325d 100644 --- a/src/main/scala/reading/component/widget/style/Modal.scala +++ b/src/main/scala/reading/component/widget/style/Modal.scala @@ -55,8 +55,11 @@ object Modal extends StyleSheet.Inline {    )    val close = style( -    Button.simple, -    marginTop(20.px), -    marginBottom(30.px) +    Media.desktop(display.none), +    Media.mobile( +      Button.simple, +      marginTop(20.px), +      marginBottom(30.px) +    )    )  } diff --git a/src/main/scala/reading/models/Book.scala b/src/main/scala/reading/models/Book.scala index 7d72f23..6f4d8dd 100644 --- a/src/main/scala/reading/models/Book.scala +++ b/src/main/scala/reading/models/Book.scala @@ -16,6 +16,9 @@ case class Book(  }  object Book { -  def filter(books: Seq[Book], filters: Seq[Filter]): Seq[Book] = -    books.filter(b => filters.forall(_.filter(b))) +  def filter(books: Seq[Book], search: String = ""): Seq[Book] = +    books.filter { book => +      (Search(book.title, search) +        || Search(book.author, search)) +    }  } diff --git a/src/main/scala/reading/Books.scala b/src/main/scala/reading/models/Books.scala index eb4722a..43ed2b8 100644 --- a/src/main/scala/reading/Books.scala +++ b/src/main/scala/reading/models/Books.scala @@ -1,6 +1,5 @@ -package reading +package reading.models -import reading.models.{ Book, Period, Theme, Genre, Program, Level }  import Period._  import Theme._  import Genre._ @@ -1090,7 +1089,7 @@ object Books {      ),      Book( -      title = "Caïus", +      title = "L’affaire Caïus",        author = "Henry WINTERFELD",        year = "2014",        parts = 2, diff --git a/src/main/scala/reading/models/Filter.scala b/src/main/scala/reading/models/Filter.scala index d14ca63..7ec6340 100644 --- a/src/main/scala/reading/models/Filter.scala +++ b/src/main/scala/reading/models/Filter.scala @@ -5,6 +5,16 @@ trait Filter {    def kind: FilterKind    def nonFormattedName: String    def name: String + +  override def equals(that: Any): Boolean = +    that match { +      case that: Filter => +        this.kind == that.kind && this.name == that.name +      case _ => +        false +    } + +  override def hashCode: Int = this.kind.hashCode + this.nonFormattedName.hashCode  }  object Filter { @@ -23,15 +33,33 @@ object Filter {      }    def contains(filters: Seq[Filter], filter: Filter): Boolean = -    filters.find(equals(_, filter)).nonEmpty - -  def equals(f1: Filter, f2: Filter): Boolean = -    f1.kind == f2.kind && f1.name == f2.name +    filters.find(_ == filter).nonEmpty    def remove(fs: Seq[Filter], rf: Filter): Seq[Filter] =      fs.filterNot { f => -      (equals(f, rf) +      (f == rf          || rf.kind == FilterKind.Grade && f.kind == FilterKind.Program          || rf.kind == FilterKind.GroupedTheme && f.kind == FilterKind.Theme)      } + +  val onBooks: Map[Filter, Seq[Book]] = +    Seq( +      Grade.values.map(Filter.apply(_)), +      Program.values.map(Filter.apply(_)), +      Theme.values.map(Filter.apply(_)), +      GroupedTheme.values.map(Filter.apply(_)), +      Genre.values.map(Filter.apply(_)), +      Level.values.map(Filter.apply(_)), +      Period.values.map(Filter.apply(_)) +    ) +      .flatten +      .map(f => (f, Books().filter(f.filter))) +      .toMap + +  def add(books: Seq[Book], filters: Seq[Filter]): Seq[Book] = +    filters.foldLeft(books)(add) + +  def add(books: Seq[Book], filter: Filter): Seq[Book] = +    books.intersect(onBooks.getOrElse(filter, Nil)) +  } diff --git a/src/main/scala/reading/models/Search.scala b/src/main/scala/reading/models/Search.scala new file mode 100644 index 0000000..5ef97c1 --- /dev/null +++ b/src/main/scala/reading/models/Search.scala @@ -0,0 +1,16 @@ +package reading.models + +object Search { +  def apply(text: String, search: String): Boolean = +    format(text).contains(format(search)) + +  private def format(str: String): String = +    str +      .toLowerCase +      .replace('’', '\'') +      .replaceAll("[èéêë]", "e") +      .replaceAll("[ûù]", "u") +      .replaceAll("[ïî]", "i") +      .replaceAll("[àâ]", "a") +      .replaceAll("ô", "o") +} diff --git a/src/main/scala/reading/utils/Rx.scala b/src/main/scala/reading/utils/Rx.scala index 76d05eb..a5b56ee 100644 --- a/src/main/scala/reading/utils/Rx.scala +++ b/src/main/scala/reading/utils/Rx.scala @@ -1,44 +1,85 @@  package reading.utils +import java.util.concurrent.atomic.AtomicReference + +import scala.annotation.tailrec +import scala.language.implicitConversions  import scala.util.{ Failure, Success }  import org.scalajs.dom.Element - -import scalatags.JsDom.all._  import rx._ +import scalacss.Defaults.StyleA +import scalatags.JsDom.all._ -import Ctx.Owner.Unsafe._ +object RxUtils { -object RxTag { -  def apply(r: Ctx.Data => HtmlTag): HtmlTag = -    rxMod(Rx(r(implicitly[Ctx.Data]))) +  implicit def rxFrag[T](n: Rx[T])(implicit f: T => Frag, ctx: Ctx.Owner): Frag = { -  private def rxMod(r: Rx[HtmlTag]): HtmlTag = { -    def rSafe = r.toTry match { -      case Success(v) => v.render -      case Failure(e) => span(e.toString, backgroundColor := "red").render +    @tailrec def clearChildren(node: org.scalajs.dom.Node): Unit = { +      if (node.firstChild != null) { +        node.removeChild(node.firstChild) +        clearChildren(node) +      }      } -    var last = rSafe -    r.trigger { -      val newLast = rSafe -      Option(last.parentElement).foreach { -        _.replaceChild(newLast, last) + +    def fSafe: Frag = n match { +      case r: Rx.Dynamic[T] => r.toTry match { +        case Success(v) => v.render +        case Failure(e) => span(e.getMessage, backgroundColor := "red").render        } +      case v: Var[T] => v.now.render +    } + +    var last = fSafe.render + +    val container = span(last).render + +    n.triggerLater { +      val newLast = fSafe.render +      //Rx[Seq[T]] can generate multiple children per propagate, so use clearChildren instead of replaceChild +      clearChildren(container) +      container.appendChild(newLast)        last = newLast      } -    span( -      bindNode(last) -    ) +    bindNode(container)    } -} -object RxAttr { -  def apply[Builder, T: AttrValue](attr: scalatags.generic.Attr, v: Rx[T]) = { -    val attrValue = new AttrValue[Rx[T]] { -      def apply(t: Element, a: Attr, r: Rx[T]): Unit = { -        val _ = r.trigger { implicitly[AttrValue[T]].apply(t, a, r.now) } +  implicit def RxAttrValue[T: AttrValue](implicit ctx: Ctx.Owner) = new AttrValue[Rx.Dynamic[T]] { +    def apply(t: Element, a: Attr, r: Rx.Dynamic[T]): Unit = { +      r.trigger { implicitly[AttrValue[T]].apply(t, a, r.now) } +      () +    } +  } + +  implicit def RxStyleValue[T: StyleValue](implicit ctx: Ctx.Owner) = new StyleValue[Rx.Dynamic[T]] { +    def apply(t: Element, s: Style, r: Rx.Dynamic[T]): Unit = { +      r.trigger { implicitly[StyleValue[T]].apply(t, s, r.now) } +      () +    } +  } + +  implicit class bindRxStyle(rx: Rx[StyleA])(implicit ctx: Ctx.Owner) extends Modifier { +    def applyTo(container: Element) = { +      val atomicReference = new AtomicReference(rx.now) +      applyStyle(container, atomicReference.get()) +      rx.triggerLater { +        val current = rx.now +        val previous = atomicReference.getAndSet(current) +        removeStyle(container, previous) +        applyStyle(container, current) +        ()        } +      ()      } -    scalatags.generic.AttrPair(attr, v, attrValue) + +    private def removeStyle(container: Element, style: StyleA): Unit = +      style.classNameIterator.foreach { className => +        container.classList.remove(className.value) +      } + +    private def applyStyle(container: Element, style: StyleA): Unit = +      style.classNameIterator.foreach { className => +        container.classList.add(className.value) +      }    }  } | 
